NavisworksTransport/UnitTests/Integration/AnimationFrameProbeAutomationTests.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

150 lines
7.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>
/// 动画逐帧状态验证集成测试(覆盖计划 T04
///
/// 通过 probe-animation-frames 端点生成动画后 seek 到多个进度点,
/// 断言物体跟踪位置(业务跟踪点 = 原始包围盒中心)沿路径移动:
/// - Ground起点/终点 XZ 匹配路径点,高度恒定(平面链不改变高度),中间帧在起终点之间
/// - Hoisting高度剖面起吊地面 → 悬挂升高 → 下降地面)+ 水平平移
///
/// 注比较使用模型单位Floor2 为英尺);物体跟踪点 XZ 与路径点一致Y 为中心高度(非路径点高度)。
/// </summary>
[TestClass]
[TestCategory("NavisworksIntegration")]
public class AnimationFrameProbeAutomationTests
{
private const double PositionTolerance = 1e-2;
private const int TestFrameRate = 15;
private const double TestDurationSeconds = 5.0;
[TestMethod]
[Timeout(240000)]
public async Task GroundVirtualObject_FramesFollowPathStartToEnd()
{
using (var client = new NavisworksTestAutomationClient())
{
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
JObject response = await client.ProbeAnimationFramesAsync(
"Ground",
120,
frameRate: TestFrameRate,
durationSeconds: TestDurationSeconds).ConfigureAwait(false);
Assert.IsTrue(response.Value<bool>("ok"), "帧探针失败: " + (string)response["error"]);
JObject data = (JObject)response["data"];
Assert.AreEqual("自动测试_Ground", (string)data["route"]["name"], "探针路径不匹配");
Assert.IsTrue((int)data["totalFrames"] > 0, "总帧数应大于 0");
JObject pathStart = (JObject)data["pathStart"];
JObject pathEnd = (JObject)data["pathEnd"];
JArray probes = (JArray)data["probes"];
AssertProbeCount(probes, 5);
// probe 0物体起点 = 路径起点XZ 匹配Y 为中心高度)
var p0 = (JObject)probes[0]["position"];
AssertNear((double)pathStart["x"], (double)p0["x"], PositionTolerance, "起点 X 不匹配路径起点");
AssertNear((double)pathStart["z"], (double)p0["z"], PositionTolerance, "起点 Z 不匹配路径起点");
// probe 1.0:物体终点 = 路径终点
var p1 = (JObject)probes[probes.Count - 1]["position"];
AssertNear((double)pathEnd["x"], (double)p1["x"], PositionTolerance, "终点 X 不匹配路径终点");
AssertNear((double)pathEnd["z"], (double)p1["z"], PositionTolerance, "终点 Z 不匹配路径终点");
// Ground 平面链:高度恒定(物体中心高度不变)
AssertNear((double)p0["y"], (double)p1["y"], PositionTolerance, "Ground 路径高度应恒定");
// 中间帧在起终点之间(水平距离大于 0 且小于全程)
var pm = (JObject)probes[probes.Count / 2]["position"];
double startToMid = HorizontalDistance(p0, pm);
double startToEnd = HorizontalDistance(p0, p1);
Assert.IsTrue(startToMid > PositionTolerance, "中间帧不应等于起点");
Assert.IsTrue(startToMid < startToEnd - PositionTolerance, "中间帧应在起终点之间");
// yaw 应为有限值
foreach (var probe in probes)
{
Assert.IsFalse(double.IsNaN((double)probe["yawDegrees"]), "yaw 不应为 NaN");
}
}
}
[TestMethod]
[Timeout(240000)]
public async Task HoistingVirtualObject_FramesFollowLiftProfile()
{
using (var client = new NavisworksTestAutomationClient())
{
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
JObject response = await client.ProbeAnimationFramesAsync(
"Hoisting",
120,
frameRate: TestFrameRate,
durationSeconds: TestDurationSeconds).ConfigureAwait(false);
Assert.IsTrue(response.Value<bool>("ok"), "帧探针失败: " + (string)response["error"]);
JObject data = (JObject)response["data"];
Assert.AreEqual("自动测试_Hoisting", (string)data["route"]["name"], "探针路径不匹配");
Assert.IsTrue((int)data["totalFrames"] > 0, "总帧数应大于 0");
JObject pathStart = (JObject)data["pathStart"];
JObject pathEnd = (JObject)data["pathEnd"];
JArray probes = (JArray)data["probes"];
AssertProbeCount(probes, 5);
var p0 = (JObject)probes[0]["position"];
var pm = (JObject)probes[probes.Count / 2]["position"];
var p1 = (JObject)probes[probes.Count - 1]["position"];
// 起吊点/落地点XZ 匹配路径起点/终点(地面)
AssertNear((double)pathStart["x"], (double)p0["x"], PositionTolerance, "起吊点 X 不匹配");
AssertNear((double)pathStart["z"], (double)p0["z"], PositionTolerance, "起吊点 Z 不匹配");
AssertNear((double)pathEnd["x"], (double)p1["x"], PositionTolerance, "落地点 X 不匹配");
AssertNear((double)pathEnd["z"], (double)p1["z"], PositionTolerance, "落地点 Z 不匹配");
// 吊装剖面:中间帧高度高于起吊/落地(悬挂段),结束后回落
Assert.IsTrue((double)pm["y"] > (double)p0["y"] + PositionTolerance, "中间帧应高于起吊点(悬挂升高)");
AssertNear((double)p0["y"], (double)p1["y"], PositionTolerance, "落地后高度应回落到起吊高度");
// 水平平移:中间帧 X 在起点与终点之间
double startX = (double)p0["x"];
double endX = (double)p1["x"];
double midX = (double)pm["x"];
Assert.IsTrue(
midX > Math.Min(startX, endX) - PositionTolerance &&
midX < Math.Max(startX, endX) + PositionTolerance,
"中间帧 X 应在起终点之间(水平平移)");
}
}
private static void AssertProbeCount(JArray probes, int expected)
{
Assert.IsNotNull(probes, "缺少 probes");
Assert.AreEqual(expected, probes.Count, $"探针点数应为 {expected}0/0.25/0.5/0.75/1.0");
}
private static double HorizontalDistance(JObject a, JObject b)
{
double dx = (double)a["x"] - (double)b["x"];
double dz = (double)a["z"] - (double)b["z"];
return Math.Sqrt(dx * dx + dz * dz);
}
private static void AssertNear(double expected, double actual, double tolerance, string message)
{
Assert.IsTrue(
Math.Abs(expected - actual) <= tolerance,
$"{message}:期望 {expected:F3},实际 {actual:F3},容差 {tolerance}");
}
}
}