- 删除 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 通过
211 lines
10 KiB
C#
211 lines
10 KiB
C#
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
|
||
{
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|