测试新增: - 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)
142 lines
6.4 KiB
C#
142 lines
6.4 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|