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("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 RunVirtualCollisionTestAsync(string pathType, int timeoutSeconds) { return await RunVirtualCollisionTestCoreAsync(null, pathType, timeoutSeconds).ConfigureAwait(false); } public async Task RunVirtualCollisionTestAsync(string routeName, string pathType, int timeoutSeconds) { return await RunVirtualCollisionTestCoreAsync(routeName, pathType, timeoutSeconds).ConfigureAwait(false); } /// /// 运行碰撞测试(虚拟或真实物体)。 /// useVirtualObject=false 时必须提供 animatedObjectPath(ModelPath 定位,格式如 0/0/769/0)。 /// frameRate/durationSeconds 可覆盖动画参数加速测试(如 15 FPS / 5 秒)。 /// public async Task 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 { "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 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 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 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 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 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); } /// /// 获取路径网格诊断(网格参数/路径点所在网格可通行性/段诊断)。 /// public async Task 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); } /// /// 查询碰撞检测记录(验证持久化:动画参数/关联路径正确落库)。 /// public async Task GetDetectionRecordAsync(int id) { string requestUri = "/api/test/detection-record?id=" + id.ToString(CultureInfo.InvariantCulture); return await GetJsonAsync(requestUri, 30).ConfigureAwait(false); } /// /// 导出路径到文件(xml/json),返回导出文件路径。 /// public async Task 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); } /// /// 动画播放控制序列(prepare→start→pause 验证→resume→finished)。 /// public async Task 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); } /// /// 路径分析(效率/安全评分)。 /// public async Task 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); } /// /// 批量任务队列添加(不执行队列处理)。 /// public async Task 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); } /// /// 批处理队列状态查询(最近 limit 项:id/status/errorMessage/collisionCount 等)。 /// public async Task BatchQueueStatusAsync(int limit = 10) { string requestUri = "/api/test/batch-queue-status?limit=" + limit.ToString(CultureInfo.InvariantCulture); return await GetJsonAsync(requestUri, 30).ConfigureAwait(false); } /// /// 触发批处理队列后台执行(F1 剖面盒自动检测等)。 /// public async Task BatchQueueProcessAsync() { return await PostJsonAsync("/api/test/batch-queue-process", 30).ConfigureAwait(false); } public async Task 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); } /// /// 剖面盒导出(指定包围盒模式),返回导出文件信息。 /// public async Task 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 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 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); } } /// /// 动画逐帧状态探针:生成动画后 seek 到指定进度点,返回各点物体跟踪位置/yaw。 /// frameRate/durationSeconds 可覆盖动画参数加速测试。 /// public async Task 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); } /// /// 枚举模型物体(按名称过滤),返回 DisplayName/PathId/包围盒中心/祖先链。 /// public async Task 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(); } } }