NavisworksTransport/UnitTests/Integration/NavisworksTestAutomationClient.cs
tian 8e5d9e845f feat: 集成测试补 T11-T15(P0+P1+P2 全部完成,24/24 通过)
- T11 路径编辑(PathEditingAutomationTests):删除点/更新位置/正交化/去环
- T12 路径导出(ExportImportRoundTripAutomationTests):XML/JSON 导出→导入回环
- T13 播放控制(AnimationPlaybackControlAutomationTests):pause 验证帧停止/resume 前进
- T14 批量队列(BatchQueueAutomationTests):队列添加/计数递增
- T15 路径分析(PathAnalysisAutomationTests):评分结构验证
- 服务端新增 5 端点:export-route-file/edit-route/animation-playback-control/batch-queue-add/analyze-path
- 产品 bug 修复:PathDatabase.CollisionResults 表已废弃→GetCollisionCount 改查 CollisionReports、
  SaveCollisions 异常保护;路径编辑真删点走 PathRoute.RemovePoint(RemovePathPoint 仅 3D 标记)

验证:集成测试 24/24 通过,完整集 74.6s(15fps/5s 动画)
2026-08-04 18:39:16 +08:00

371 lines
15 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.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 时必须提供 animatedObjectPathModelPath 定位,格式如 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 strategy = null)
{
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);
if (!string.IsNullOrWhiteSpace(strategy))
{
requestUri += "&strategy=" + Uri.EscapeDataString(strategy);
}
return await PostJsonAsync(requestUri, 120).ConfigureAwait(false);
}
/// <summary>
/// 获取路径网格诊断(网格参数/路径点所在网格可通行性/段诊断)。
/// </summary>
public async Task<JObject> GetRouteGridDiagnosticsAsync(string routeName = null)
{
string requestUri = "/api/test/route-grid-diagnostics";
if (!string.IsNullOrWhiteSpace(routeName))
{
requestUri += "?routeName=" + Uri.EscapeDataString(routeName);
}
return await GetJsonAsync(requestUri, 60).ConfigureAwait(false);
}
/// <summary>
/// 查询碰撞检测记录(验证持久化:动画参数/关联路径正确落库)。
/// </summary>
public async Task<JObject> GetDetectionRecordAsync(int id)
{
string requestUri = "/api/test/detection-record?id=" + id.ToString(CultureInfo.InvariantCulture);
return await GetJsonAsync(requestUri, 30).ConfigureAwait(false);
}
/// <summary>
/// 导出路径到文件xml/json返回导出文件路径。
/// </summary>
public async Task<JObject> ExportRouteFileAsync(string routeName, string format, string exportPath)
{
string requestUri = string.Format(
"/api/test/export-route-file?routeName={0}&format={1}&path={2}",
Uri.EscapeDataString(routeName),
Uri.EscapeDataString(format),
Uri.EscapeDataString(exportPath));
return await PostJsonAsync(requestUri, 60).ConfigureAwait(false);
}
/// <summary>
/// 动画播放控制序列prepare→start→pause 验证→resume→finished
/// </summary>
public async Task<JObject> AnimationPlaybackControlAsync(
string pathType,
int timeoutSeconds,
int? frameRate = null,
double? durationSeconds = null)
{
string requestUri = "/api/test/animation-playback-control?pathType=" + Uri.EscapeDataString(pathType)
+ "&timeoutSeconds=" + timeoutSeconds.ToString(CultureInfo.InvariantCulture);
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>
/// 路径分析(效率/安全评分)。
/// </summary>
public async Task<JObject> AnalyzePathAsync(string routeName, string strategy = null)
{
string requestUri = "/api/test/analyze-path?routeName=" + Uri.EscapeDataString(routeName);
if (!string.IsNullOrWhiteSpace(strategy))
{
requestUri += "&strategy=" + Uri.EscapeDataString(strategy);
}
return await PostJsonAsync(requestUri, 120).ConfigureAwait(false);
}
/// <summary>
/// 批量任务队列添加(不执行队列处理)。
/// </summary>
public async Task<JObject> BatchQueueAddAsync(string routeName, int? frameRate = null, double? durationSeconds = null)
{
string requestUri = "/api/test/batch-queue-add?routeName=" + Uri.EscapeDataString(routeName);
if (frameRate.HasValue)
{
requestUri += "&frameRate=" + frameRate.Value.ToString(CultureInfo.InvariantCulture);
}
if (durationSeconds.HasValue)
{
requestUri += "&durationSeconds=" + durationSeconds.Value.ToString(CultureInfo.InvariantCulture);
}
return await PostJsonAsync(requestUri, 60).ConfigureAwait(false);
}
/// <summary>
/// 路径编辑remove-point/update-point/orthogonalize/remove-loops
/// </summary>
public async Task<JObject> EditRouteAsync(
string action,
string routeName,
int? pointIndex = null,
double? x = null,
double? y = null,
double? z = null)
{
string requestUri = "/api/test/edit-route?action=" + Uri.EscapeDataString(action)
+ "&routeName=" + Uri.EscapeDataString(routeName);
if (pointIndex.HasValue)
{
requestUri += "&pointIndex=" + pointIndex.Value.ToString(CultureInfo.InvariantCulture);
}
if (x.HasValue) requestUri += "&x=" + x.Value.ToString(CultureInfo.InvariantCulture);
if (y.HasValue) requestUri += "&y=" + y.Value.ToString(CultureInfo.InvariantCulture);
if (z.HasValue) requestUri += "&z=" + z.Value.ToString(CultureInfo.InvariantCulture);
return await PostJsonAsync(requestUri, 60).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();
}
}
}