NavisworksTransport/UnitTests/Integration/RealObjectAnimationAutomationTests.cs
tian 44214baa00 feat: 集成测试补 T05/T06 + 动画加速与多项基础设施修复
测试新增:
- T05 Free 自由路径(FreePathAutomationTests:完整动画 + 逐帧位置=路径点)
- T06 碰撞报告内容断言(CollisionReportAssertions 接入虚拟/真实物体测试)

服务端增强:
- list-model-items 端点(枚举物体:DisplayName/PathId/包围盒中心/祖先链)
- run-virtual-collision-test 支持 frameRate/durationSeconds(测试动画加速:15fps/5s)
- 测试执行串行锁(SemaphoreSlim,防客户端超时后任务占用 UI 线程并发污染)
- 标准路径清单/XML 增加自动测试_Free

Bug 修复:
- AnimationControlViewModel.InvalidateLastGeneratedReport(跨测试碰撞报告残留)
- 真实物体身份校验 ReferenceEquals→InstanceGuid(NW ModelItem 实例不保证同引用)
- 真实物体动态解析(ModelPath 跨会话不稳定,禁止硬编码 PathId)
- PathImportValidity 测试点 Id 随机化(PathPoints.Id 全局主键,p1/p2 固定会冲突)
- start-navisworks.ps1:启动前清理 AutoSave(避免恢复弹窗拦截)、文件关联打开模型、dismiss 兜底、启动 15s→4.4s

验证:集成测试 13/13 通过,完整集 6min→59.6s(动画 15fps/5s)
2026-08-04 18:13:26 +08:00

142 lines
6.4 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.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
namespace NavisworksTransport.UnitTests.Integration
{
/// <summary>
/// 真实物体动画集成测试(覆盖计划 T01-T03高风险区真实物体姿态链
///
/// 验证点:
/// - 通过 list-model-items 动态解析真实物体ModelPath(PathId) 跨会话不稳定,禁止硬编码)
/// - 同名多实例时按“离路径起点最近”选择(如模型中有多个 42" Diameter 管道)
/// - 真实物体沿三类路径生成动画并完整播放fragment 代表姿态 / 真实尺寸)
///
/// 路径起点坐标来自 resources/auto-test-routes.xml模型坐标稳定不随会话变化
/// </summary>
[TestClass]
[TestCategory("NavisworksIntegration")]
public class RealObjectAnimationAutomationTests
{
private const int TestFrameRate = 15;
private const double TestDurationSeconds = 5.0;
// 路径起点(模型单位,来自 auto-test-routes.xml
private static readonly (double X, double Y, double Z) GroundStart = (-152.706, 14.833, -5.443);
private static readonly (double X, double Y, double Z) HoistingStart = (-221.750, 10.833, -14.097);
private static readonly (double X, double Y, double Z) RailStart = (-207.591, 29.482, 3.504);
[TestMethod]
[Timeout(360000)]
public async Task GroundRealObject_Animation_Completes()
{
await AssertRealObjectFlowAsync("Ground", "42\" Diameter", GroundStart).ConfigureAwait(false);
}
[TestMethod]
[Timeout(360000)]
public async Task HoistingRealObject_Animation_Completes()
{
await AssertRealObjectFlowAsync("Hoisting", "25\" x 25\"", HoistingStart).ConfigureAwait(false);
}
[TestMethod]
[Timeout(360000)]
public async Task RailRealObject_Animation_Completes()
{
await AssertRealObjectFlowAsync("Rail", "42\" Diameter", RailStart).ConfigureAwait(false);
}
private static async Task AssertRealObjectFlowAsync(
string pathType,
string objectDisplayName,
(double X, double Y, double Z) routeStart)
{
using (var client = new NavisworksTestAutomationClient())
{
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
// 动态解析真实物体 PathId离路径起点最近的同名实例
string animatedObjectPath = await ResolveRealObjectPathIdAsync(
client, objectDisplayName, routeStart).ConfigureAwait(false);
JObject response = await client.RunCollisionTestAsync(
pathType,
240,
useVirtualObject: false,
animatedObjectPath: animatedObjectPath,
frameRate: TestFrameRate,
durationSeconds: TestDurationSeconds).ConfigureAwait(false);
Assert.IsTrue(response.Value<bool>("ok"), "真实物体碰撞测试 HTTP 接口返回失败: " + (string)response["error"]);
JObject data = (JObject)response["data"];
Assert.IsNotNull(data, "缺少测试结果 data");
JObject animatedObject = (JObject)data["animatedObject"];
Assert.IsNotNull(animatedObject, "缺少 animatedObject");
Assert.AreEqual("RealObject", (string)animatedObject["mode"], "当前测试应使用真实物体模式");
Assert.IsFalse(string.IsNullOrWhiteSpace((string)animatedObject["displayName"]), "真实物体显示名为空");
JObject route = (JObject)data["route"];
Assert.IsNotNull(route, "缺少 route");
Assert.AreEqual(pathType, (string)route["pathType"], "返回的路径类型不匹配");
JObject animation = (JObject)data["animation"];
Assert.IsNotNull(animation, "缺少 animation");
Assert.AreEqual("Finished", (string)animation["currentState"], "动画未完成");
Assert.IsTrue((int)animation["totalFrames"] > 0, "动画总帧数应大于 0");
Assert.IsTrue(animation["detectionRecordId"] != null && (int)animation["detectionRecordId"] > 0, "检测记录 ID 无效");
CollisionReportAssertions.AssertReportStructure(data, route);
}
}
/// <summary>
/// 枚举同名物体并选择离参考点(路径起点)最近的实例,返回其 PathId。
/// ModelPath 跨会话不稳定,必须动态解析而非硬编码。
/// </summary>
private static async Task<string> ResolveRealObjectPathIdAsync(
NavisworksTestAutomationClient client,
string displayName,
(double X, double Y, double Z) referencePoint)
{
JObject response = await client.ListModelItemsAsync(displayName, 100).ConfigureAwait(false);
Assert.IsTrue(response.Value<bool>("ok"), "枚举模型物体失败: " + (string)response["error"]);
JObject data = (JObject)response["data"];
JArray items = data["items"] as JArray;
Assert.IsNotNull(items, "缺少 items");
Assert.IsTrue(items.Count > 0, $"未找到物体: {displayName}");
string bestPathId = null;
double bestDistanceSq = double.MaxValue;
foreach (JToken token in items)
{
JObject item = (JObject)token;
JObject center = (JObject)item["boundingBoxCenter"];
string pathId = (string)item["pathId"];
if (center == null || string.IsNullOrWhiteSpace(pathId))
{
continue;
}
double dx = (double)center["x"] - referencePoint.X;
double dy = (double)center["y"] - referencePoint.Y;
double dz = (double)center["z"] - referencePoint.Z;
double distanceSq = dx * dx + dy * dy + dz * dz;
if (distanceSq < bestDistanceSq)
{
bestDistanceSq = distanceSq;
bestPathId = pathId;
}
}
Assert.IsNotNull(bestPathId, $"无法定位物体(无包围盒中心): {displayName}");
return bestPathId;
}
}
}