NavisworksTransport/UnitTests/Integration/PathImportValidityAutomationTests.cs
tian c9c29c7170 refactor: 依赖 Navisworks 的路径导入验证从单测迁移至集成测试
- 删除 UnitTests/Core/PathPersistenceTests.cs:单测进程无法初始化 Navisworks API,
  Rail/Hoisting 路径导入必经的长度计算(Point3D 访问)必然失败,测试'假通过'且每次产生 ERROR 日志噪音
- 新增集成测试 PathImportValidityAutomationTests:验证完整流程
  JSON/XML 导入 → Rail 属性映射 → 长度计算(含文档单位换算)→ 切换路径 → 完整动画/碰撞流程
- TestAutomationHttpService 新增 /api/test/import-route-file 端点(overwrite 语义保证可重复执行)
- NavisworksTestAutomationClient 新增 ImportRouteFileAsync/SelectRouteAsync/routeName 重载
- 修正 auto-test-routes.xml Rail 属性为有效枚举值(UnderRail/RailCenterLine,与原路径一致)

验证:单测 255/255 且 API must be initialized 错误日志归零;集成测试 6/6 通过
2026-08-02 21:19:23 +08:00

211 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
namespace NavisworksTransport.UnitTests.Integration
{
/// <summary>
/// 路径导入与有效性集成测试。
/// 验证完整流程JSON/XML 导入 → Rail 属性映射 → 长度计算 → 切换路径 → 完整动画/碰撞流程。
///
/// 背景:原单测 PathPersistenceTests 验证 Rail 属性映射时,导入必经 RecalculateRoute
/// 触发 Rail 长度计算(读取 Navisworks Point3D要求 API 初始化),而单元测试进程不是
/// Navisworks 宿主无法初始化 API导致长度计算必然失败异常被容错吞掉测试“假通过”
/// 按“依赖 Navisworks 的验证归入集成测试”原则,改为在本测试中于 Navisworks 进程内
/// 完整验证导入链路(含单测无法覆盖的长度计算正确性),单测中的对应测试已删除。
/// </summary>
[TestClass]
[TestCategory("NavisworksIntegration")]
public class PathImportValidityAutomationTests
{
// 模型内已验证可用的 Rail 坐标(与 resources/auto-test-routes.xml 自动测试_Rail 同源)
private const double StartX = -207.591;
private const double StartY = 29.482;
private const double StartZ = 3.504;
private const double EndX = -195.556;
private const double EndY = 27.776;
private const double EndZ = 3.308;
private const double RailNormalOffset = 0.25;
// 输入两点坐标的模型单位直线距离(服务端按文档单位换算为米)
private static readonly double ExpectedLengthModelUnits = Math.Sqrt(
(EndX - StartX) * (EndX - StartX) +
(EndY - StartY) * (EndY - StartY) +
(EndZ - StartZ) * (EndZ - StartZ));
[TestMethod]
[Timeout(360000)]
public async Task ImportRailRoute_FromJson_PropertiesLengthAndAnimation_AreValid()
{
await RunImportValidityFlowAsync("集成测试_导入_Rail_JSON", "rail-import.json", BuildRailJson).ConfigureAwait(false);
}
[TestMethod]
[Timeout(360000)]
public async Task ImportRailRoute_FromXml_PropertiesLengthAndAnimation_AreValid()
{
await RunImportValidityFlowAsync("集成测试_导入_Rail_XML", "rail-import.xml", BuildRailXml).ConfigureAwait(false);
}
private static async Task RunImportValidityFlowAsync(
string routeName,
string fileName,
Func<string, string> buildContent)
{
using (var client = new NavisworksTestAutomationClient())
{
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
string tempDir = CreateTempDir();
try
{
string filePath = Path.Combine(tempDir, fileName);
File.WriteAllText(filePath, buildContent(routeName), new UTF8Encoding(false));
// 1. 导入(在 Navisworks 进程内执行 PathDataManager 导入,覆盖模式保证可重复执行)
JObject importResponse = await client.ImportRouteFileAsync(filePath, overwrite: true).ConfigureAwait(false);
AssertImportResult(importResponse, routeName);
// 2. 切换导入路径为当前路径
JObject selectResponse = await client.SelectRouteAsync(routeName).ConfigureAwait(false);
Assert.IsTrue(selectResponse.Value<bool>("ok"), "切换导入路径失败: " + (string)selectResponse["error"]);
Assert.AreEqual(routeName, (string)((JObject)selectResponse["data"])["selected"]["name"], "切换的路径不匹配");
// 3. 完整动画/碰撞流程验证路径有效性
JObject animationResponse = await client.RunVirtualCollisionTestAsync(routeName, "Rail", 240).ConfigureAwait(false);
Assert.IsTrue(animationResponse.Value<bool>("ok"), "导入路径动画流程失败: " + (string)animationResponse["error"]);
JObject data = (JObject)animationResponse["data"];
JObject route = (JObject)data["route"];
Assert.AreEqual(routeName, (string)route["name"], "动画流程使用的路径不匹配");
JObject animation = (JObject)data["animation"];
Assert.AreEqual("Finished", (string)animation["currentState"], "动画未完成");
Assert.IsTrue((int)animation["totalFrames"] > 0, "动画总帧数应大于 0");
}
finally
{
SafeDelete(tempDir);
}
}
}
private static void AssertImportResult(JObject response, string expectedRouteName)
{
Assert.IsTrue(response.Value<bool>("ok"), "导入失败: " + (string)response["error"]);
JObject data = (JObject)response["data"];
Assert.IsNotNull(data, "缺少导入结果 data");
// 服务端返回的米→模型单位因子,用于把输入坐标距离换算为期望的米长度(不硬编码文档单位)
double metersToModelUnits = data.Value<double?>("metersToModelUnits") ?? 1.0;
double expectedLengthMeters = ExpectedLengthModelUnits / metersToModelUnits;
JArray routes = (JArray)data["routes"];
Assert.IsNotNull(routes, "缺少 routes");
Assert.AreEqual(1, routes.Count, "应导入 1 条路径");
JObject route = (JObject)routes[0];
Assert.AreEqual(expectedRouteName, (string)route["name"], "导入路径名称不匹配");
Assert.AreEqual("Rail", (string)route["pathType"], "导入路径类型不匹配");
Assert.AreEqual(2, (int)route["pointCount"], "导入路径点数不匹配");
Assert.AreEqual("UnderRail", (string)route["railMountMode"], "railMountMode 映射不匹配");
Assert.AreEqual("RailCenterLine", (string)route["railPathDefinitionMode"], "railPathDefinitionMode 映射不匹配");
Assert.AreEqual(RailNormalOffset, (double)route["railNormalOffset"], 1e-6, "railNormalOffset 映射不匹配");
Assert.AreEqual(expectedLengthMeters, (double)route["totalLengthInMeters"], 1e-3, "导入路径长度计算不匹配");
JObject normal = (JObject)route["railPreferredNormal"];
Assert.IsNotNull(normal, "缺少 railPreferredNormal");
Assert.AreEqual(-0.2, (double)normal["x"], 1e-6, "railPreferredNormal.x 映射不匹配");
Assert.AreEqual(0.5, (double)normal["y"], 1e-6, "railPreferredNormal.y 映射不匹配");
Assert.AreEqual(0.8, (double)normal["z"], 1e-6, "railPreferredNormal.z 映射不匹配");
}
private static string BuildRailJson(string routeName)
{
return string.Format(
System.Globalization.CultureInfo.InvariantCulture,
@"{{
""PathPlanningData"": {{
""version"": ""1.0"",
""generator"": ""integration-test"",
""timestamp"": ""2026-08-02T21:30:00"",
""ProjectInfo"": {{ ""name"": ""test"", ""description"": ""test"", ""units"": ""meters"", ""coordinateSystem"": ""Global"" }},
""Routes"": [
{{
""id"": ""import-route-json-001"",
""name"": ""{0}"",
""description"": """",
""pathType"": ""Rail"",
""railMountMode"": ""UnderRail"",
""railPathDefinitionMode"": ""RailCenterLine"",
""railNormalOffset"": 0.25,
""railPreferredNormal"": {{ ""x"": -0.2, ""y"": 0.5, ""z"": 0.8 }},
""totalLength"": 10.0,
""objectLimits"": {{ ""maxLength"": 0, ""maxWidth"": 0, ""maxHeight"": 0, ""safetyMargin"": 0 }},
""gridSize"": 1.0,
""liftHeight"": 0.0,
""created"": ""2026-08-02T21:30:00"",
""points"": [
{{ ""id"": ""p1"", ""name"": ""start"", ""type"": ""StartPoint"", ""index"": 0, ""x"": {1}, ""y"": {2}, ""z"": {3}, ""created"": ""2026-08-02T21:30:00"" }},
{{ ""id"": ""p2"", ""name"": ""end"", ""type"": ""EndPoint"", ""index"": 1, ""x"": {4}, ""y"": {5}, ""z"": {6}, ""created"": ""2026-08-02T21:30:00"" }}
]
}}
]
}}
}}",
routeName,
StartX, StartY, StartZ,
EndX, EndY, EndZ);
}
private static string BuildRailXml(string routeName)
{
return string.Format(
System.Globalization.CultureInfo.InvariantCulture,
@"<?xml version=""1.0"" encoding=""UTF-8""?>
<PathPlanningData xmlns=""http://www.3ds.com/delmia/pathplanning"" version=""1.0"" generator=""integration-test"" timestamp=""2026-08-02T21:30:00"">
<ProjectInfo name=""test"" description=""test"" units=""meters"" coordinateSystem=""Global"" />
<Routes>
<Route id=""import-route-xml-001"" name=""{0}"" description="""" pathType=""Rail"" railMountMode=""UnderRail"" railPathDefinitionMode=""RailCenterLine"" railNormalOffset=""0.25"" railPreferredNormalX=""-0.2"" railPreferredNormalY=""0.5"" railPreferredNormalZ=""0.8"" totalLength=""10.0"" maxObjectLength=""0.0"" maxObjectWidth=""0.0"" maxObjectHeight=""0.0"" safetyMargin=""0.0"" gridSize=""1.0"" liftHeight=""0.0"" created=""2026-08-02T21:30:00"">
<Points>
<Point id=""p1"" name=""start"" type=""StartPoint"" index=""0"" x=""{1}"" y=""{2}"" z=""{3}"" created=""2026-08-02T21:30:00"" />
<Point id=""p2"" name=""end"" type=""EndPoint"" index=""1"" x=""{4}"" y=""{5}"" z=""{6}"" created=""2026-08-02T21:30:00"" />
</Points>
</Route>
</Routes>
</PathPlanningData>",
routeName,
StartX, StartY, StartZ,
EndX, EndY, EndZ);
}
private static string CreateTempDir()
{
string tempDir = Path.Combine(Path.GetTempPath(), "NavisworksTransportIntegrationTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
return tempDir;
}
private static void SafeDelete(string directory)
{
if (!Directory.Exists(directory))
{
return;
}
try
{
Directory.Delete(directory, true);
}
catch
{
}
}
}
}