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 { /// /// 路径导入与有效性集成测试。 /// 验证完整流程:JSON/XML 导入 → Rail 属性映射 → 长度计算 → 切换路径 → 完整动画/碰撞流程。 /// /// 背景:原单测 PathPersistenceTests 验证 Rail 属性映射时,导入必经 RecalculateRoute /// 触发 Rail 长度计算(读取 Navisworks Point3D,要求 API 初始化),而单元测试进程不是 /// Navisworks 宿主无法初始化 API,导致长度计算必然失败(异常被容错吞掉,测试“假通过”)。 /// 按“依赖 Navisworks 的验证归入集成测试”原则,改为在本测试中于 Navisworks 进程内 /// 完整验证导入链路(含单测无法覆盖的长度计算正确性),单测中的对应测试已删除。 /// [TestClass] [TestCategory("NavisworksIntegration")] public class PathImportValidityAutomationTests { private const int TestFrameRate = 15; private const double TestDurationSeconds = 5.0; // 模型内已验证可用的 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 buildContent) { using (var client = new NavisworksTestAutomationClient()) { await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false); string tempDir = CreateTempDir(); try { string filePath = Path.Combine(tempDir, fileName); // PathPoints.Id 是全局主键:路径点 Id 必须随机,避免与历史残留/其他测试文件冲突(p1/p2 固定会主键冲突) string routeId = "import-" + Guid.NewGuid().ToString("N"); string point1Id = Guid.NewGuid().ToString("N"); string point2Id = Guid.NewGuid().ToString("N"); File.WriteAllText(filePath, buildContent(routeName, routeId, point1Id, point2Id), 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("ok"), "切换导入路径失败: " + (string)selectResponse["error"]); Assert.AreEqual(routeName, (string)((JObject)selectResponse["data"])["selected"]["name"], "切换的路径不匹配"); // 3. 完整动画/碰撞流程验证路径有效性 JObject animationResponse = await client.RunCollisionTestAsync( "Rail", 240, routeName: routeName, frameRate: TestFrameRate, durationSeconds: TestDurationSeconds).ConfigureAwait(false); Assert.IsTrue(animationResponse.Value("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("ok"), "导入失败: " + (string)response["error"]); JObject data = (JObject)response["data"]; Assert.IsNotNull(data, "缺少导入结果 data"); // 服务端返回的米→模型单位因子,用于把输入坐标距离换算为期望的米长度(不硬编码文档单位) double metersToModelUnits = data.Value("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, string routeId, string point1Id, string point2Id) { 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"": ""{0}"", ""name"": ""{1}"", ""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"": ""{2}"", ""name"": ""start"", ""type"": ""StartPoint"", ""index"": 0, ""x"": {4}, ""y"": {5}, ""z"": {6}, ""created"": ""2026-08-02T21:30:00"" }}, {{ ""id"": ""{3}"", ""name"": ""end"", ""type"": ""EndPoint"", ""index"": 1, ""x"": {7}, ""y"": {8}, ""z"": {9}, ""created"": ""2026-08-02T21:30:00"" }} ] }} ] }} }}", routeId, routeName, point1Id, point2Id, StartX, StartY, StartZ, EndX, EndY, EndZ); } private static string BuildRailXml(string routeName, string routeId, string point1Id, string point2Id) { return string.Format( System.Globalization.CultureInfo.InvariantCulture, @" ", routeId, routeName, point1Id, point2Id, 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 { } } } }