- 服务端新增 export-section-box 端点:mode=bbox(指定包围盒,测试可控)/ mode=activeView(UI 剖面盒) - 新增 SectionBoxExportAutomationTests:导出 NWD 文件存在/对象数/文件大小/隐藏节点断言 - 客户端 ExportSectionBoxAsync 方法
388 lines
16 KiB
C#
388 lines
16 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 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();
|
||
}
|
||
}
|
||
}
|