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
{
}
}
}
}