feat: 集成测试补 T16 剖面盒导出(25/25 通过)

- 服务端新增 export-section-box 端点:mode=bbox(指定包围盒,测试可控)/ mode=activeView(UI 剖面盒)
- 新增 SectionBoxExportAutomationTests:导出 NWD 文件存在/对象数/文件大小/隐藏节点断言
- 客户端 ExportSectionBoxAsync 方法
This commit is contained in:
tian 2026-08-04 18:44:01 +08:00
parent 8e5d9e845f
commit ac454196f1
5 changed files with 187 additions and 0 deletions

View File

@ -84,6 +84,7 @@
<Compile Include="UnitTests\Integration\PathEditingAutomationTests.cs" />
<Compile Include="UnitTests\Integration\RealObjectAnimationAutomationTests.cs" />
<Compile Include="UnitTests\Integration\RouteGridDiagnosticsAutomationTests.cs" />
<Compile Include="UnitTests\Integration\SectionBoxExportAutomationTests.cs" />
<Compile Include="UnitTests\Integration\VirtualCollisionAutomationTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\HostCoordinateAdapterTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalRailPoseBuilderTests.cs" />

View File

@ -293,6 +293,23 @@ namespace NavisworksTransport.UnitTests.Integration
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)))

View File

@ -0,0 +1,83 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
namespace NavisworksTransport.UnitTests.Integration
{
/// <summary>
/// 剖面盒导出集成测试(覆盖计划 T16
///
/// 验证 SectionBoxExporter 的指定包围盒导出链路:
/// - 导出 NWD 文件成功(文件存在且非空)
/// - 包围盒内对象数 > 0、文件大小 > 0
/// - 隐藏节点计算完成hiddenNodeCount >= 0
///
/// 使用指定包围盒模式bbox不依赖 UI 剖面盒状态;包围盒覆盖模型 Hoisting/Rail 区域。
/// </summary>
[TestClass]
[TestCategory("NavisworksIntegration")]
public class SectionBoxExportAutomationTests
{
// 模型单位:覆盖 Hoisting/Rail 区域的包围盒(已验证含 57 个对象)
private const double MinX = -240, MinY = 10, MinZ = -20;
private const double MaxX = -170, MaxY = 30, MaxZ = 25;
[TestMethod]
[Timeout(240000)]
public async Task SectionBoxBBoxExport_ProducesValidNwd()
{
using (var client = new NavisworksTestAutomationClient())
{
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
string tempDir = Path.Combine(Path.GetTempPath(), "NavisworksTransportIntegrationTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
string exportPath = Path.Combine(tempDir, "sectionbox-export.nwd");
JObject response = await client.ExportSectionBoxAsync(
exportPath,
MinX, MinY, MinZ, MaxX, MaxY, MaxZ).ConfigureAwait(false);
Assert.IsTrue(response.Value<bool>("ok"), "剖面盒导出失败: " + (string)response["error"]);
JObject data = (JObject)response["data"];
// 文件已生成且非空
Assert.IsFalse(string.IsNullOrWhiteSpace((string)data["filePath"]), "导出文件路径为空");
Assert.IsTrue(File.Exists(exportPath), "导出文件不存在: " + exportPath);
Assert.IsTrue((long)data["fileSize"] > 0, "导出文件大小应大于 0");
Assert.IsTrue(new FileInfo(exportPath).Length > 0, "导出文件实际为空");
// 包围盒内对象与隐藏节点
Assert.IsTrue((int)data["objectCount"] > 0, "包围盒内应找到对象");
Assert.IsTrue((int)data["hiddenNodeCount"] >= 0, "隐藏节点数不能为负");
Assert.AreEqual("bbox", (string)data["mode"], "导出模式应为 bbox");
}
finally
{
SafeDelete(tempDir);
}
}
}
private static void SafeDelete(string directory)
{
if (!Directory.Exists(directory))
{
return;
}
try
{
Directory.Delete(directory, true);
}
catch
{
}
}
}
}

View File

@ -54,6 +54,7 @@
- [x] **T13 动画播放控制**2026-08-04`AnimationPlaybackControlAutomationTests`)— prepare→start→pause 验证→resume→finished
- [x] **T14 批量任务队列**2026-08-04`BatchQueueAutomationTests`)— 队列添加/计数递增(轻量版,不执行批量动画)
- [x] **T15 路径分析**2026-08-04`PathAnalysisAutomationTests`)— 评分结构验证;顺带修复产品 bugCollisionResults 表已废弃导致路径分析 no such table
- [x] **T16 剖面盒导出**2026-08-04`SectionBoxExportAutomationTests`)— 指定包围盒模式导出 NWD对象数/文件大小/隐藏节点),不依赖 UI 剖面盒状态
## 4. 测试服务支撑能力
@ -74,6 +75,7 @@
| animation-playback-control | ✅ 已实现 | 播放控制序列T13 |
| batch-queue-add | ✅ 已实现 | 批量队列添加T14 |
| analyze-path | ✅ 已实现 | 路径分析评分T15 |
| export-section-box | ✅ 已实现 | 剖面盒导出 NWDbbox/activeView 模式T16 |
| 其余待扩展 | — | 批量执行、TimeLiner 集成等后续迭代补充 |
## 5. 完成记录
@ -88,3 +90,4 @@
| 2026-08-04 | `44214ba` | T05/T06 + 基础修复Free 路径、报告断言、动态解析、串行锁、脚本、加速 15fps/5s |
| 2026-08-04 | 待提交 | T07-T10障碍绕行、多策略、网格诊断、持久化+ GetDetectionRecordById + detection-record 端点P0+P1 完成18/18 |
| 2026-08-04 | 待提交 | T11-T15路径编辑/导出/播放控制/批量队列/路径分析)+ 服务端 5 端点 + 产品 bug 修复PathDatabase CollisionResults→CollisionReports、RemovePoint 链路P0+P1+P2 全部完成,集成测试 24/24完整集 74.6s |
| 2026-08-04 | 待提交 | T16 剖面盒导出SectionBoxExportAutomationTests + export-section-box 端点bbox 模式25/25 |

View File

@ -298,6 +298,14 @@ namespace NavisworksTransport.Core.Services
return;
}
if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/export-section-box", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => ExportSectionBoxPayload(request.Query));
await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
return;
}
if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/export-debug-snapshot", StringComparison.OrdinalIgnoreCase))
{
@ -697,6 +705,81 @@ namespace NavisworksTransport.Core.Services
/// <summary>
/// 路径分析(效率/安全评分),集成测试验证分析链路与评分结构。
/// </summary>
/// <summary>
/// 剖面盒导出T16mode=bbox 用指定包围盒导出 NWD集成测试可控不依赖 UI 剖面盒状态);
/// mode=activeView 用当前视图剖面盒(需 UI 已激活 Box 模式)。
/// </summary>
private static object ExportSectionBoxPayload(Dictionary<string, string> query)
{
Document document = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (document == null)
{
throw new InvalidOperationException("当前没有活动文档");
}
string mode = GetOptionalQueryValue(query, "mode") ?? "bbox";
string filePath = GetOptionalQueryValue(query, "path");
if (string.IsNullOrWhiteSpace(filePath))
{
filePath = Path.Combine(Path.GetTempPath(), $"sectionbox-export-{Guid.NewGuid():N}.nwd");
}
var exporter = new SectionBoxExporter();
if (string.Equals(mode, "bbox", StringComparison.OrdinalIgnoreCase))
{
double x1 = ParseRequiredDouble(query, "x1");
double y1 = ParseRequiredDouble(query, "y1");
double z1 = ParseRequiredDouble(query, "z1");
double x2 = ParseRequiredDouble(query, "x2");
double y2 = ParseRequiredDouble(query, "y2");
double z2 = ParseRequiredDouble(query, "z2");
var bounds = new BoundingBox3D(
new Point3D(Math.Min(x1, x2), Math.Min(y1, y2), Math.Min(z1, z2)),
new Point3D(Math.Max(x1, x2), Math.Max(y1, y2), Math.Max(z1, z2)));
var traversal = exporter.GetObjectsAndHiddenItems(document, bounds);
if (traversal.ObjectsInBox.Count == 0)
{
throw new InvalidOperationException("指定包围盒内没有找到对象");
}
string exportedPath = exporter.ExportToNwd(document, traversal, filePath);
if (string.IsNullOrEmpty(exportedPath) || !File.Exists(exportedPath))
{
throw new InvalidOperationException($"NWD 导出失败: {filePath}");
}
LogManager.Info($"[测试HTTP] 剖面盒导出完成(bbox): 对象={traversal.ObjectsInBox.Count}, 隐藏={traversal.ItemsToHide.Count}, 文件={exportedPath}");
return new
{
mode = "bbox",
filePath = exportedPath,
objectCount = traversal.ObjectsInBox.Count,
hiddenNodeCount = traversal.ItemsToHide.Count,
fileSize = new FileInfo(exportedPath).Length
};
}
// activeView 模式:依赖当前视图剖面盒
var result = exporter.ExportSectionBoxFromActiveView(document, filePath);
if (!result.Success)
{
throw new InvalidOperationException(result.ErrorMessage ?? "剖面盒导出失败");
}
return new
{
mode = "activeView",
filePath = result.FilePath,
objectCount = result.ObjectCount,
hiddenNodeCount = result.HiddenNodeCount,
fileSize = result.FileSize
};
}
/// <summary>
/// 路径编辑集成测试端点T11action=remove-point/update-point/orthogonalize/remove-loops。
/// 直接操作临时测试路径,验证 PathPlanningManager 编辑链路。