NavisworksTransport/UnitTests/Integration/NavisworksTestAutomationClient.cs
tian ac454196f1 feat: 集成测试补 T16 剖面盒导出(25/25 通过)
- 服务端新增 export-section-box 端点:mode=bbox(指定包围盒,测试可控)/ mode=activeView(UI 剖面盒)
- 新增 SectionBoxExportAutomationTests:导出 NWD 文件存在/对象数/文件大小/隐藏节点断言
- 客户端 ExportSectionBoxAsync 方法
2026-08-04 18:44:01 +08:00

388 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>
/// 路径编辑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);
}
/// <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();
}
}
}