NavisworksTransport/UnitTests/Integration/NavisworksTestAutomationClient.cs
tian f38bae137a feat: F1 批处理端到端测试 + batch-queue-status 端点
服务端:
- 新增 GET /api/test/batch-queue-status(查询队列项状态:
  id/status/errorMessage/collisionCount/detectionRecordId,供测试轮询)
- BatchQueueManager 增加 internal GetDatabase() 访问器

客户端:
- NavisworksTestAutomationClient 新增 BatchQueueStatusAsync /
  BatchQueueProcessAsync

测试:
- BatchQueueF1ProcessAutomationTests(TestCategory=NavisworksIntegrationF1,
  不在默认全量内):add(虚拟物体 F1 场景)→ process → 轮询 status
  至 Completed → 断言碰撞报告。单独跑约 30 秒。

AGENTS.md:记录 F1 测试用法(bat 参数含 = 会截断,用 FullyQualifiedName~)
2026-08-06 00:29:58 +08:00

401 lines
16 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>
/// 批处理队列状态查询(最近 limit 项id/status/errorMessage/collisionCount 等)。
/// </summary>
public async Task<JObject> BatchQueueStatusAsync(int limit = 10)
{
string requestUri = "/api/test/batch-queue-status?limit=" + limit.ToString(CultureInfo.InvariantCulture);
return await GetJsonAsync(requestUri, 30).ConfigureAwait(false);
}
/// <summary>
/// 触发批处理队列后台执行F1 剖面盒自动检测等)。
/// </summary>
public async Task<JObject> BatchQueueProcessAsync()
{
return await PostJsonAsync("/api/test/batch-queue-process", 30).ConfigureAwait(false);
}
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);
}
/// <summary>
/// 剖面盒导出(指定包围盒模式),返回导出文件信息。
/// </summary>
public async Task<JObject> ExportSectionBoxAsync(
string filePath,
double x1, double y1, double z1,
double x2, double y2, double z2)
{
string requestUri = string.Format(
CultureInfo.InvariantCulture,
"/api/test/export-section-box?mode=bbox&x1={0}&y1={1}&z1={2}&x2={3}&y2={4}&z2={5}&path={6}",
x1, y1, z1, x2, y2, z2,
Uri.EscapeDataString(filePath));
return await PostJsonAsync(requestUri, 120).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();
}
}
}