测试新增: - 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)
244 lines
9.6 KiB
C#
244 lines
9.6 KiB
C#
using System;
|
||
using System.Globalization;
|
||
using System.Net.Http;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||
using Newtonsoft.Json.Linq;
|
||
|
||
namespace NavisworksTransport.UnitTests.Integration
|
||
{
|
||
internal sealed class NavisworksTestAutomationClient : IDisposable
|
||
{
|
||
private readonly HttpClient _httpClient;
|
||
|
||
public NavisworksTestAutomationClient()
|
||
{
|
||
_httpClient = new HttpClient
|
||
{
|
||
BaseAddress = new Uri("http://127.0.0.1:18777"),
|
||
// 全局不设超时:每个请求用 CancellationToken 单独控制(动画/碰撞类请求可能远大于 20s)
|
||
Timeout = Timeout.InfiniteTimeSpan
|
||
};
|
||
}
|
||
|
||
public async Task EnsureServiceReadyAsync(TimeSpan timeout)
|
||
{
|
||
DateTime deadlineUtc = DateTime.UtcNow.Add(timeout);
|
||
Exception lastError = null;
|
||
|
||
while (DateTime.UtcNow < deadlineUtc)
|
||
{
|
||
try
|
||
{
|
||
JObject pingResponse = await GetJsonAsync("/api/test/ping").ConfigureAwait(false);
|
||
if (pingResponse.Value<bool?>("ok") == true)
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
lastError = ex;
|
||
}
|
||
|
||
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
|
||
}
|
||
|
||
Assert.Fail(
|
||
"Navisworks 测试服务未就绪。请先用 start-navisworks.bat 启动 Navisworks,并确保插件面板已加载。最后错误: {0}",
|
||
lastError?.Message ?? "unknown");
|
||
}
|
||
|
||
public async Task<JObject> RunVirtualCollisionTestAsync(string pathType, int timeoutSeconds)
|
||
{
|
||
return await RunVirtualCollisionTestCoreAsync(null, pathType, timeoutSeconds).ConfigureAwait(false);
|
||
}
|
||
|
||
public async Task<JObject> RunVirtualCollisionTestAsync(string routeName, string pathType, int timeoutSeconds)
|
||
{
|
||
return await RunVirtualCollisionTestCoreAsync(routeName, pathType, timeoutSeconds).ConfigureAwait(false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 运行碰撞测试(虚拟或真实物体)。
|
||
/// useVirtualObject=false 时必须提供 animatedObjectPath(ModelPath 定位,格式如 0/0/769/0)。
|
||
/// frameRate/durationSeconds 可覆盖动画参数加速测试(如 15 FPS / 5 秒)。
|
||
/// </summary>
|
||
public async Task<JObject> RunCollisionTestAsync(
|
||
string pathType,
|
||
int timeoutSeconds,
|
||
string routeName = null,
|
||
bool useVirtualObject = true,
|
||
string animatedObjectPath = null,
|
||
int? frameRate = null,
|
||
double? durationSeconds = null)
|
||
{
|
||
var queryParts = new System.Collections.Generic.List<string>
|
||
{
|
||
"pathType=" + Uri.EscapeDataString(pathType),
|
||
"timeoutSeconds=" + timeoutSeconds.ToString(CultureInfo.InvariantCulture),
|
||
"useVirtualObject=" + (useVirtualObject ? "true" : "false")
|
||
};
|
||
|
||
if (!string.IsNullOrWhiteSpace(routeName))
|
||
{
|
||
queryParts.Add("routeName=" + Uri.EscapeDataString(routeName));
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(animatedObjectPath))
|
||
{
|
||
queryParts.Add("animatedObjectPath=" + Uri.EscapeDataString(animatedObjectPath));
|
||
}
|
||
|
||
if (frameRate.HasValue)
|
||
{
|
||
queryParts.Add("frameRate=" + frameRate.Value.ToString(CultureInfo.InvariantCulture));
|
||
}
|
||
|
||
if (durationSeconds.HasValue)
|
||
{
|
||
queryParts.Add("durationSeconds=" + durationSeconds.Value.ToString(CultureInfo.InvariantCulture));
|
||
}
|
||
|
||
string requestUri = "/api/test/run-virtual-collision-test?" + string.Join("&", queryParts);
|
||
return await PostJsonAsync(requestUri, timeoutSeconds + 30).ConfigureAwait(false);
|
||
}
|
||
|
||
private async Task<JObject> RunVirtualCollisionTestCoreAsync(string routeName, string pathType, int timeoutSeconds)
|
||
{
|
||
string requestUri = string.Format(
|
||
CultureInfo.InvariantCulture,
|
||
"/api/test/run-virtual-collision-test?routeName={0}&pathType={1}&timeoutSeconds={2}",
|
||
Uri.EscapeDataString(routeName ?? string.Empty),
|
||
Uri.EscapeDataString(pathType),
|
||
timeoutSeconds);
|
||
|
||
return await PostJsonAsync(requestUri).ConfigureAwait(false);
|
||
}
|
||
|
||
public async Task<JObject> ImportRouteFileAsync(string filePath, bool overwrite = false)
|
||
{
|
||
string requestUri = string.Format(
|
||
"/api/test/import-route-file?path={0}&overwrite={1}",
|
||
Uri.EscapeDataString(filePath),
|
||
overwrite ? "true" : "false");
|
||
|
||
return await PostJsonAsync(requestUri).ConfigureAwait(false);
|
||
}
|
||
|
||
public async Task<JObject> SelectRouteAsync(string routeName)
|
||
{
|
||
string requestUri = string.Format(
|
||
"/api/test/select-route?name={0}",
|
||
Uri.EscapeDataString(routeName));
|
||
|
||
return await PostJsonAsync(requestUri).ConfigureAwait(false);
|
||
}
|
||
|
||
public async Task<JObject> AnalyzeAutoPathGridAsync(string pathType)
|
||
{
|
||
string requestUri = string.Format(
|
||
"/api/test/analyze-auto-path-grid?pathType={0}",
|
||
Uri.EscapeDataString(pathType));
|
||
|
||
return await PostJsonAsync(requestUri).ConfigureAwait(false);
|
||
}
|
||
|
||
public async Task<JObject> RunAutoPathAsync(
|
||
string pathType,
|
||
string routeName,
|
||
double objectLengthInMeters,
|
||
double objectWidthInMeters,
|
||
double safetyMarginInMeters,
|
||
double gridSizeInMeters)
|
||
{
|
||
string requestUri = string.Format(
|
||
CultureInfo.InvariantCulture,
|
||
"/api/test/run-auto-path?pathType={0}&routeName={1}&objectLengthInMeters={2}&objectWidthInMeters={3}&safetyMarginInMeters={4}&gridSizeInMeters={5}",
|
||
Uri.EscapeDataString(pathType),
|
||
Uri.EscapeDataString(routeName),
|
||
objectLengthInMeters,
|
||
objectWidthInMeters,
|
||
safetyMarginInMeters,
|
||
gridSizeInMeters);
|
||
|
||
return await PostJsonAsync(requestUri).ConfigureAwait(false);
|
||
}
|
||
|
||
private async Task<JObject> GetJsonAsync(string requestUri, int timeoutSeconds = 30)
|
||
{
|
||
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)))
|
||
using (HttpResponseMessage response = await _httpClient.GetAsync(requestUri, cts.Token).ConfigureAwait(false))
|
||
{
|
||
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||
response.EnsureSuccessStatusCode();
|
||
return JObject.Parse(content);
|
||
}
|
||
}
|
||
|
||
private async Task<JObject> PostJsonAsync(string requestUri, int timeoutSeconds = 60)
|
||
{
|
||
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)))
|
||
using (HttpResponseMessage response = await _httpClient
|
||
.PostAsync(requestUri, new StringContent(string.Empty), cts.Token)
|
||
.ConfigureAwait(false))
|
||
{
|
||
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||
response.EnsureSuccessStatusCode();
|
||
return JObject.Parse(content);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 动画逐帧状态探针:生成动画后 seek 到指定进度点,返回各点物体跟踪位置/yaw。
|
||
/// frameRate/durationSeconds 可覆盖动画参数加速测试。
|
||
/// </summary>
|
||
public async Task<JObject> ProbeAnimationFramesAsync(
|
||
string pathType,
|
||
int timeoutSeconds,
|
||
string probeProgress = "0,0.25,0.5,0.75,1.0",
|
||
int? frameRate = null,
|
||
double? durationSeconds = null)
|
||
{
|
||
string requestUri = string.Format(
|
||
CultureInfo.InvariantCulture,
|
||
"/api/test/probe-animation-frames?pathType={0}&probeProgress={1}&timeoutSeconds={2}",
|
||
Uri.EscapeDataString(pathType),
|
||
Uri.EscapeDataString(probeProgress),
|
||
timeoutSeconds);
|
||
|
||
if (frameRate.HasValue)
|
||
{
|
||
requestUri += "&frameRate=" + frameRate.Value.ToString(CultureInfo.InvariantCulture);
|
||
}
|
||
|
||
if (durationSeconds.HasValue)
|
||
{
|
||
requestUri += "&durationSeconds=" + durationSeconds.Value.ToString(CultureInfo.InvariantCulture);
|
||
}
|
||
|
||
return await PostJsonAsync(requestUri, timeoutSeconds + 30).ConfigureAwait(false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 枚举模型物体(按名称过滤),返回 DisplayName/PathId/包围盒中心/祖先链。
|
||
/// </summary>
|
||
public async Task<JObject> ListModelItemsAsync(string name = null, int maxResults = 200)
|
||
{
|
||
string requestUri = "/api/test/list-model-items?maxResults=" + maxResults.ToString(CultureInfo.InvariantCulture);
|
||
if (!string.IsNullOrWhiteSpace(name))
|
||
{
|
||
requestUri += "&name=" + Uri.EscapeDataString(name);
|
||
}
|
||
|
||
return await GetJsonAsync(requestUri).ConfigureAwait(false);
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
_httpClient.Dispose();
|
||
}
|
||
}
|
||
}
|