Add Navisworks automation integration tests
This commit is contained in:
parent
61e6b79299
commit
b849bd9aed
24
AGENTS.md
24
AGENTS.md
@ -67,6 +67,30 @@
|
||||
|
||||
不要并行执行编译和部署。否则很容易把旧 DLL 部署到插件目录。
|
||||
|
||||
### 并行执行边界
|
||||
|
||||
后续会话中的 AI 助手必须把命令分成两类:
|
||||
|
||||
- **允许并行的纯读取操作**
|
||||
- `rg`
|
||||
- `Get-Content`
|
||||
- `ls / Get-ChildItem`
|
||||
- 读取日志
|
||||
- 查询状态
|
||||
- **禁止并行的产出型/宿主相关操作**
|
||||
- `run-unit-tests.bat`
|
||||
- `compile.bat`
|
||||
- `deploy-plugin.bat`
|
||||
- 启动/关闭 Navisworks
|
||||
- 会占用 DLL、修改 `bin/obj`、写插件部署目录、依赖上一步产物的任何命令
|
||||
|
||||
硬约束:
|
||||
|
||||
- 只有“纯读取、无副作用、且互不依赖”的命令才允许并行
|
||||
- 只要命令会生成、覆盖、部署、锁定文件、启动宿主进程,必须串行执行
|
||||
- 对本仓库,默认把 `run-unit-tests -> compile -> deploy -> start Navisworks` 视为**单通道流水线**
|
||||
- 不允许把这条流水线放进任何并行工具里,即使只是为了节省时间
|
||||
|
||||
### 插件部署目录
|
||||
|
||||
- `C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\`
|
||||
|
||||
@ -42,6 +42,9 @@
|
||||
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions">
|
||||
<HintPath>packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Numerics" />
|
||||
@ -57,6 +60,8 @@
|
||||
<Compile Include="UnitTests\Core\PathPlanningManagerHoistingCompletionTests.cs" />
|
||||
<Compile Include="UnitTests\Core\PathPersistenceTests.cs" />
|
||||
<Compile Include="UnitTests\Core\PathRouteCloneTests.cs" />
|
||||
<Compile Include="UnitTests\Integration\NavisworksTestAutomationClient.cs" />
|
||||
<Compile Include="UnitTests\Integration\VirtualCollisionAutomationTests.cs" />
|
||||
<Compile Include="UnitTests\CoordinateSystem\HostCoordinateAdapterTests.cs" />
|
||||
<Compile Include="UnitTests\CoordinateSystem\CanonicalRailPoseBuilderTests.cs" />
|
||||
<Compile Include="UnitTests\CoordinateSystem\CanonicalPlanarPoseBuilderTests.cs" />
|
||||
|
||||
@ -150,6 +150,7 @@
|
||||
<Compile Include="src\Core\Services\TimeTagService.cs" />
|
||||
<Compile Include="src\Core\Services\TimeTagExporter.cs" />
|
||||
<Compile Include="src\Core\Services\TimeTagTimeLinerIntegration.cs" />
|
||||
<Compile Include="src\Core\Services\TestAutomationHttpService.cs" />
|
||||
<!-- Commands - Command Pattern Framework (for testing) -->
|
||||
<Compile Include="src\Commands\IPathPlanningCommand.cs" />
|
||||
<Compile Include="src\Commands\CommandBase.cs" />
|
||||
|
||||
85
UnitTests/Integration/NavisworksTestAutomationClient.cs
Normal file
85
UnitTests/Integration/NavisworksTestAutomationClient.cs
Normal file
@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
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"),
|
||||
Timeout = TimeSpan.FromSeconds(20)
|
||||
};
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
string requestUri = string.Format(
|
||||
"/api/test/run-virtual-collision-test?pathType={0}&timeoutSeconds={1}",
|
||||
Uri.EscapeDataString(pathType),
|
||||
timeoutSeconds);
|
||||
|
||||
return await PostJsonAsync(requestUri).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<JObject> GetJsonAsync(string requestUri)
|
||||
{
|
||||
using (HttpResponseMessage response = await _httpClient.GetAsync(requestUri).ConfigureAwait(false))
|
||||
{
|
||||
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return JObject.Parse(content);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<JObject> PostJsonAsync(string requestUri)
|
||||
{
|
||||
using (HttpResponseMessage response = await _httpClient.PostAsync(requestUri, new StringContent(string.Empty)).ConfigureAwait(false))
|
||||
{
|
||||
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return JObject.Parse(content);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
68
UnitTests/Integration/VirtualCollisionAutomationTests.cs
Normal file
68
UnitTests/Integration/VirtualCollisionAutomationTests.cs
Normal file
@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace NavisworksTransport.UnitTests.Integration
|
||||
{
|
||||
[TestClass]
|
||||
[TestCategory("NavisworksIntegration")]
|
||||
public class VirtualCollisionAutomationTests
|
||||
{
|
||||
private const int DefaultCollisionTimeoutSeconds = 240;
|
||||
|
||||
[TestMethod]
|
||||
[Timeout(360000)]
|
||||
public async Task GroundVirtualCollision_AutoTestRoute_Completes()
|
||||
{
|
||||
await AssertVirtualCollisionTestAsync("Ground").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Timeout(360000)]
|
||||
public async Task HoistingVirtualCollision_AutoTestRoute_Completes()
|
||||
{
|
||||
await AssertVirtualCollisionTestAsync("Hoisting").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Timeout(360000)]
|
||||
public async Task RailVirtualCollision_AutoTestRoute_Completes()
|
||||
{
|
||||
await AssertVirtualCollisionTestAsync("Rail").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task AssertVirtualCollisionTestAsync(string pathType)
|
||||
{
|
||||
using (var client = new NavisworksTestAutomationClient())
|
||||
{
|
||||
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
|
||||
|
||||
JObject response = await client.RunVirtualCollisionTestAsync(pathType, DefaultCollisionTimeoutSeconds).ConfigureAwait(false);
|
||||
Assert.IsTrue(response.Value<bool>("ok"), "测试 HTTP 接口返回失败: " + (string)response["error"]);
|
||||
|
||||
JObject data = (JObject)response["data"];
|
||||
Assert.IsNotNull(data, "缺少测试结果 data");
|
||||
|
||||
Assert.AreEqual(pathType, (string)data["requestedPathType"], "请求的路径类型不匹配");
|
||||
|
||||
JObject route = (JObject)data["route"];
|
||||
Assert.IsNotNull(route, "缺少 route");
|
||||
Assert.AreEqual(pathType, (string)route["pathType"], "返回的路径类型不匹配");
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace((string)route["name"]), "路径名为空");
|
||||
|
||||
JObject animatedObject = (JObject)data["animatedObject"];
|
||||
Assert.IsNotNull(animatedObject, "缺少 animatedObject");
|
||||
Assert.AreEqual("VirtualObject", (string)animatedObject["mode"], "当前集成测试应使用虚拟物体");
|
||||
|
||||
JObject animation = (JObject)data["animation"];
|
||||
Assert.IsNotNull(animation, "缺少 animation");
|
||||
Assert.AreEqual("Finished", (string)animation["currentState"], "动画未完成");
|
||||
Assert.IsTrue((int)animation["totalFrames"] > 0, "动画总帧数应大于 0");
|
||||
Assert.IsTrue(animation["detectionRecordId"] != null && (int)animation["detectionRecordId"] > 0, "检测记录 ID 无效");
|
||||
|
||||
Assert.IsNotNull(data["report"], "缺少碰撞报告");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
run-ground-virtual-collision-test.bat
Normal file
7
run-ground-virtual-collision-test.bat
Normal file
@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\run-ground-virtual-collision-test.ps1" %*
|
||||
|
||||
if errorlevel 1 exit /b 1
|
||||
exit /b 0
|
||||
@ -41,7 +41,7 @@ if errorlevel 1 (
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%VSTEST_PATH% "bin\x64\Release\NavisworksTransport.UnitTests.dll" /Platform:x64 /TestAdapterPath:%TEST_ADAPTER_PATH%
|
||||
%VSTEST_PATH% "bin\x64\Release\NavisworksTransport.UnitTests.dll" /Platform:x64 /TestAdapterPath:%TEST_ADAPTER_PATH% /TestCaseFilter:"TestCategory!=NavisworksIntegration"
|
||||
if errorlevel 1 (
|
||||
echo Unit tests failed!
|
||||
exit /b 1
|
||||
|
||||
154
scripts/run-ground-virtual-collision-test.ps1
Normal file
154
scripts/run-ground-virtual-collision-test.ps1
Normal file
@ -0,0 +1,154 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ModelPath,
|
||||
[int]$ServiceStartupTimeoutSeconds = 90,
|
||||
[int]$TestTimeoutSeconds = 240
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$repoRoot = Split-Path -Parent $scriptDirectory
|
||||
$startScriptPath = Join-Path $scriptDirectory 'start-navisworks.ps1'
|
||||
$defaultsFilePath = Join-Path $scriptDirectory 'test-automation.defaults.json'
|
||||
$testApiBaseUrl = 'http://127.0.0.1:18777'
|
||||
|
||||
function Resolve-DefaultModelPath {
|
||||
if (-not (Test-Path $defaultsFilePath)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$defaults = Get-Content $defaultsFilePath -Raw | ConvertFrom-Json
|
||||
$defaultModelPath = [string]$defaults.defaultModelPath
|
||||
if ([string]::IsNullOrWhiteSpace($defaultModelPath)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return [System.IO.Path]::GetFullPath($defaultModelPath)
|
||||
}
|
||||
|
||||
function Wait-TestApiReady {
|
||||
param(
|
||||
[string]$BaseUrl,
|
||||
[int]$TimeoutSeconds
|
||||
)
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
$lastError = $null
|
||||
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
try {
|
||||
$pingResponse = Invoke-RestMethod -Uri ($BaseUrl + '/api/test/ping') -TimeoutSec 10
|
||||
if ($pingResponse.ok) {
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$lastError = $_.Exception.Message
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($lastError)) {
|
||||
throw "测试 HTTP 服务未在 $TimeoutSeconds 秒内就绪。"
|
||||
}
|
||||
|
||||
throw "测试 HTTP 服务未在 $TimeoutSeconds 秒内就绪。最后错误: $lastError"
|
||||
}
|
||||
|
||||
function Wait-GroundRouteReady {
|
||||
param(
|
||||
[string]$BaseUrl,
|
||||
[int]$TimeoutSeconds
|
||||
)
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
$lastError = $null
|
||||
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
try {
|
||||
$routesResponse = Invoke-RestMethod -Uri ($BaseUrl + '/api/test/routes') -TimeoutSec 10
|
||||
if ($routesResponse.ok -and $routesResponse.data -and $routesResponse.data.totalRouteCount -gt 0) {
|
||||
$groundRoute = $routesResponse.data.suggestedAutoTestRoutes.ground
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$groundRoute)) {
|
||||
return [string]$groundRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$lastError = $_.Exception.Message
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($lastError)) {
|
||||
throw "默认地面测试路径未在 $TimeoutSeconds 秒内就绪。"
|
||||
}
|
||||
|
||||
throw "默认地面测试路径未在 $TimeoutSeconds 秒内就绪。最后错误: $lastError"
|
||||
}
|
||||
|
||||
function Save-JsonResult {
|
||||
param(
|
||||
[object]$Payload
|
||||
)
|
||||
|
||||
$logRoot = Join-Path $env:ProgramData 'Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\logs\test-automation'
|
||||
New-Item -ItemType Directory -Path $logRoot -Force | Out-Null
|
||||
|
||||
$fileName = 'ground-virtual-collision-test-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.json'
|
||||
$outputPath = Join-Path $logRoot $fileName
|
||||
$json = $Payload | ConvertTo-Json -Depth 16
|
||||
[System.IO.File]::WriteAllText($outputPath, $json, [System.Text.UTF8Encoding]::new($false))
|
||||
return $outputPath
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ModelPath)) {
|
||||
$ModelPath = Resolve-DefaultModelPath
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
|
||||
$ModelPath = [System.IO.Path]::GetFullPath($ModelPath)
|
||||
if (-not (Test-Path $ModelPath)) {
|
||||
throw "模型文件不存在: $ModelPath"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host '启动 Navisworks...'
|
||||
if ([string]::IsNullOrWhiteSpace($ModelPath)) {
|
||||
& $startScriptPath
|
||||
}
|
||||
else {
|
||||
& $startScriptPath $ModelPath
|
||||
}
|
||||
|
||||
Write-Host '等待测试 HTTP 服务就绪...'
|
||||
Wait-TestApiReady -BaseUrl $testApiBaseUrl -TimeoutSeconds $ServiceStartupTimeoutSeconds
|
||||
|
||||
Write-Host '等待默认地面测试路径就绪...'
|
||||
$groundRouteName = Wait-GroundRouteReady -BaseUrl $testApiBaseUrl -TimeoutSeconds $ServiceStartupTimeoutSeconds
|
||||
|
||||
Write-Host ('切换到默认地面测试路径: {0}' -f $groundRouteName)
|
||||
$selectRouteResponse = Invoke-RestMethod -Method Post -Uri ($testApiBaseUrl + '/api/test/select-default-route?pathType=Ground') -TimeoutSec 15
|
||||
if (-not $selectRouteResponse.ok) {
|
||||
throw '切换默认地面测试路径失败。'
|
||||
}
|
||||
|
||||
Write-Host '执行地面虚拟物体碰撞测试...'
|
||||
$requestUrl = $testApiBaseUrl + "/api/test/run-ground-collision-test?useVirtualObject=true&timeoutSeconds=$TestTimeoutSeconds"
|
||||
$testResult = Invoke-RestMethod -Method Post -Uri $requestUrl -TimeoutSec ($TestTimeoutSeconds + 30)
|
||||
|
||||
$outputFilePath = Save-JsonResult -Payload $testResult
|
||||
|
||||
Write-Host ('结果已保存: {0}' -f $outputFilePath)
|
||||
if ($testResult.ok -and $testResult.data) {
|
||||
Write-Host ('路径: {0}' -f $testResult.data.route.name)
|
||||
Write-Host ('动画状态: {0}' -f $testResult.data.animation.currentState)
|
||||
Write-Host ('检测记录ID: {0}' -f $testResult.data.animation.detectionRecordId)
|
||||
if ($testResult.data.report) {
|
||||
Write-Host ('碰撞数: {0}' -f $testResult.data.report.totalCollisions)
|
||||
Write-Host ('截图数: {0}' -f $testResult.data.report.screenshotCount)
|
||||
}
|
||||
}
|
||||
167
scripts/start-navisworks.ps1
Normal file
167
scripts/start-navisworks.ps1
Normal file
@ -0,0 +1,167 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Position = 0)]
|
||||
[string]$ModelPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$repoRoot = Split-Path -Parent $scriptDirectory
|
||||
$defaultsFilePath = Join-Path $scriptDirectory 'test-automation.defaults.json'
|
||||
|
||||
function Resolve-RoamerPath {
|
||||
$candidates = New-Object System.Collections.Generic.List[string]
|
||||
$appPathKeys = @(
|
||||
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Roamer.exe',
|
||||
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\Roamer.exe'
|
||||
)
|
||||
|
||||
foreach ($key in $appPathKeys) {
|
||||
try {
|
||||
$value = (Get-ItemProperty -Path $key -ErrorAction Stop).'(default)'
|
||||
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||
$candidates.Add($value)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
$wellKnownPaths = @(
|
||||
(Join-Path ${env:ProgramFiles} 'Autodesk\Navisworks Manage 2026\Roamer.exe'),
|
||||
(Join-Path ${env:ProgramFiles(x86)} 'Autodesk\Navisworks Manage 2026\Roamer.exe'),
|
||||
(Join-Path ${env:ProgramFiles} 'Autodesk\Navisworks Simulate 2026\Roamer.exe'),
|
||||
(Join-Path ${env:ProgramFiles(x86)} 'Autodesk\Navisworks Simulate 2026\Roamer.exe')
|
||||
) | Where-Object { $_ }
|
||||
|
||||
foreach ($path in $wellKnownPaths) {
|
||||
$candidates.Add($path)
|
||||
}
|
||||
|
||||
foreach ($candidate in $candidates | Select-Object -Unique) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($candidate) -and (Test-Path $candidate)) {
|
||||
return [System.IO.Path]::GetFullPath($candidate)
|
||||
}
|
||||
}
|
||||
|
||||
throw '找不到 Roamer.exe。请确认 Navisworks Manage 2026 已安装。'
|
||||
}
|
||||
|
||||
function Resolve-DefaultModelPath {
|
||||
if (-not (Test-Path $defaultsFilePath)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
$defaults = Get-Content $defaultsFilePath -Raw | ConvertFrom-Json
|
||||
$defaultModelPath = [string]$defaults.defaultModelPath
|
||||
if ([string]::IsNullOrWhiteSpace($defaultModelPath)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$resolvedPath = [System.IO.Path]::GetFullPath($defaultModelPath)
|
||||
if (-not (Test-Path $resolvedPath)) {
|
||||
throw "默认模型文件不存在: $resolvedPath"
|
||||
}
|
||||
|
||||
return $resolvedPath
|
||||
}
|
||||
catch {
|
||||
throw "读取默认测试配置失败: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Dismiss-RecoveryPromptIfPresent {
|
||||
param(
|
||||
[int]$TimeoutSeconds = 20
|
||||
)
|
||||
|
||||
Add-Type -AssemblyName System.Windows.Forms | Out-Null
|
||||
$shell = New-Object -ComObject WScript.Shell
|
||||
$windowTitles = @(
|
||||
'是否从自动保存重新载入?',
|
||||
'是否从自动保存重新载入?'
|
||||
)
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
foreach ($windowTitle in $windowTitles) {
|
||||
try {
|
||||
if ($shell.AppActivate($windowTitle)) {
|
||||
Start-Sleep -Milliseconds 300
|
||||
[System.Windows.Forms.SendKeys]::SendWait('%N')
|
||||
Write-Host '已自动选择“否”以跳过自动保存恢复窗口。'
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ModelPath)) {
|
||||
$ModelPath = Resolve-DefaultModelPath
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
|
||||
$ModelPath = [System.IO.Path]::GetFullPath($ModelPath)
|
||||
if (-not (Test-Path $ModelPath)) {
|
||||
throw "模型文件不存在: $ModelPath"
|
||||
}
|
||||
}
|
||||
|
||||
$roamerPath = Resolve-RoamerPath
|
||||
$existingProcess = Get-Process Roamer -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
$env:TRANSPORTPLUGIN_TEST_AUTOMATION = '1'
|
||||
|
||||
if ($existingProcess) {
|
||||
Write-Host ("Navisworks 已在运行,PID={0}" -f $existingProcess.Id)
|
||||
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
|
||||
Start-Process -FilePath $roamerPath -ArgumentList @($ModelPath) | Out-Null
|
||||
Write-Host ("已请求 Navisworks 打开模型: {0}" -f $ModelPath)
|
||||
}
|
||||
|
||||
Dismiss-RecoveryPromptIfPresent
|
||||
|
||||
Write-Host '提示: 当前测试 HTTP 服务在插件 DockPane OnLoaded 中启动,首次测试前请确保插件面板已打开。'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$arguments = @()
|
||||
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
|
||||
$arguments += $ModelPath
|
||||
}
|
||||
|
||||
$startProcessParams = @{
|
||||
FilePath = $roamerPath
|
||||
PassThru = $true
|
||||
}
|
||||
|
||||
if ($arguments.Count -gt 0) {
|
||||
$startProcessParams.ArgumentList = $arguments
|
||||
}
|
||||
|
||||
$process = Start-Process @startProcessParams
|
||||
Write-Host ("Navisworks 启动中,PID={0}" -f $process.Id)
|
||||
|
||||
$deadline = (Get-Date).AddSeconds(30)
|
||||
do {
|
||||
Start-Sleep -Milliseconds 500
|
||||
$running = Get-Process -Id $process.Id -ErrorAction SilentlyContinue
|
||||
} while (-not $running -and (Get-Date) -lt $deadline)
|
||||
|
||||
if (-not $running) {
|
||||
throw 'Navisworks 启动超时,30 秒内未检测到 Roamer.exe 进程。'
|
||||
}
|
||||
|
||||
Dismiss-RecoveryPromptIfPresent
|
||||
|
||||
Write-Host ("Navisworks 已启动: {0}" -f $roamerPath)
|
||||
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
|
||||
Write-Host ("已打开模型参数: {0}" -f $ModelPath)
|
||||
}
|
||||
|
||||
Write-Host '提示: 当前测试 HTTP 服务在插件 DockPane OnLoaded 中启动,首次测试前请确保插件面板已打开。'
|
||||
4
scripts/test-automation.defaults.json
Normal file
4
scripts/test-automation.defaults.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"defaultModelPath": "C:\\Users\\Tellme\\Documents\\NavisworksTransport\\模型\\Floor2_mobile_yup.nwd",
|
||||
"defaultRouteNames": []
|
||||
}
|
||||
@ -7,6 +7,7 @@ using System.Windows.Forms.Integration;
|
||||
using NavisworksTransport.Core;
|
||||
using NavisworksTransport.Core.Animation;
|
||||
using NavisworksTransport.Core.Config;
|
||||
using NavisworksTransport.Core.Services;
|
||||
using NavisworksTransport.Utils;
|
||||
using NavisworksTransport.Utils.CoordinateSystem;
|
||||
using NavisApplication = Autodesk.Navisworks.Api.Application;
|
||||
@ -382,6 +383,9 @@ namespace NavisworksTransport
|
||||
// 订阅文档事件
|
||||
SubscribeToDocumentEvents();
|
||||
|
||||
// 启动本地测试自动化 HTTP 控制面
|
||||
TestAutomationHttpService.Instance.Start();
|
||||
|
||||
// 检查是否已有活动文档且包含模型
|
||||
var activeDoc = NavisApplication.ActiveDocument;
|
||||
if (activeDoc != null && activeDoc.Models != null && activeDoc.Models.Count > 0)
|
||||
@ -413,6 +417,9 @@ namespace NavisworksTransport
|
||||
// 清理管理器
|
||||
CleanupManagers();
|
||||
|
||||
// 停止本地测试自动化 HTTP 控制面
|
||||
TestAutomationHttpService.Instance.Stop();
|
||||
|
||||
base.OnUnloading();
|
||||
}
|
||||
|
||||
|
||||
1296
src/Core/Services/TestAutomationHttpService.cs
Normal file
1296
src/Core/Services/TestAutomationHttpService.cs
Normal file
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,7 @@ using Autodesk.Navisworks.Api.Clash;
|
||||
using NavisworksTransport.Core;
|
||||
using NavisworksTransport.Core.Models;
|
||||
using NavisworksTransport.Core.Config;
|
||||
using NavisworksTransport.Core.Services;
|
||||
using NavisworksTransport.Commands;
|
||||
using NavisworksTransport.UI.WPF.Collections;
|
||||
using NavisworksTransport.UI.WPF.Views;
|
||||
@ -488,6 +489,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最近一次自动生成的碰撞报告。
|
||||
/// 仅用于测试自动化读取结果,不参与业务流程分支。
|
||||
/// </summary>
|
||||
public CollisionReportResult LastGeneratedReport => _lastGeneratedReport;
|
||||
|
||||
public bool HasGeneratedCollisionReport => _lastGeneratedReport != null;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
@ -1994,11 +2003,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
if (clashResults == null || clashResults.Count == 0)
|
||||
{
|
||||
LogManager.Info("🎉 恭喜!本次动画未检测到任何碰撞,路径规划非常安全!");
|
||||
System.Windows.MessageBox.Show(
|
||||
DialogHelper.ShowAutoClosingMessageBox(
|
||||
"🎉 恭喜!本次动画仿真过程中未发现任何碰撞!\n\n您的物流路径规划非常成功,物体可以安全通行。",
|
||||
"仿真完成 - 无碰撞",
|
||||
System.Windows.MessageBoxButton.OK,
|
||||
System.Windows.MessageBoxImage.Information);
|
||||
System.Windows.MessageBoxImage.Information,
|
||||
autoCloseMilliseconds: 4000);
|
||||
}
|
||||
|
||||
// 统一流程:刷新历史列表并显示报告(无论有无碰撞)
|
||||
@ -2928,6 +2937,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
{
|
||||
try
|
||||
{
|
||||
if (DialogHelper.IsTestAutomationModeEnabled)
|
||||
{
|
||||
LogManager.Info("[碰撞分析] 自动测试模式下跳过预计算碰撞分析");
|
||||
SaveCollisionDetectionRecord();
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 获取预计算碰撞结果(从PathAnimationManager,现在缓存帧中已包含碰撞结果)
|
||||
var allResults = _pathAnimationManager?.AllCollisionResults;
|
||||
if (allResults == null || allResults.Count == 0)
|
||||
@ -3159,6 +3175,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
|
||||
string routeId = CurrentPathRoute?.Id ?? "";
|
||||
|
||||
if (!DialogHelper.IsTestAutomationModeEnabled)
|
||||
{
|
||||
// 🔥 检查是否已有相同配置的检测记录
|
||||
var existingRecordResult = CheckExistingDetectionRecord();
|
||||
if (existingRecordResult.HasValue)
|
||||
@ -3181,6 +3199,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
// 用户选择重新生成,继续创建新记录
|
||||
LogManager.Info($"[检测记录] 用户选择重新生成,创建新记录");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogManager.Info("[检测记录] 自动测试模式下跳过重复检测记录检查,直接创建新记录");
|
||||
}
|
||||
|
||||
int recordId = CreateCollisionDetectionRecordSnapshot("动画生成", bindToAnimationManager: true);
|
||||
|
||||
@ -3445,6 +3468,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
/// </summary>
|
||||
private (int id, UserChoice userChoice)? ShowDuplicateConfigDialog(CollisionDetectionRecord record)
|
||||
{
|
||||
if (TestAutomationHttpService.ShouldAutoChooseCreateNewDetectionRecord)
|
||||
{
|
||||
_lastReusedRecordId = null;
|
||||
LogManager.Info($"[检测记录] 测试自动化已接管重复配置对话框,自动选择重新生成新记录 (ExistingId={record.Id})");
|
||||
return (record.Id, UserChoice.CreateNew);
|
||||
}
|
||||
|
||||
var message = $"检测配置已存在!\n\n" +
|
||||
$"找到一条相同配置的历史检测记录:\n" +
|
||||
$"• 测试名称:{record.TestName ?? "未命名"}\n" +
|
||||
|
||||
@ -551,11 +551,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
_documentRotationHintShown.Add(documentKey);
|
||||
_documentRotationHintPending.Remove(documentKey);
|
||||
LogManager.Info($"[模型整体旋转提示] {hintMessage}");
|
||||
System.Windows.MessageBox.Show(
|
||||
DialogHelper.ShowAutoClosingMessageBox(
|
||||
hintMessage,
|
||||
"坐标系提示",
|
||||
System.Windows.MessageBoxButton.OK,
|
||||
System.Windows.MessageBoxImage.Information);
|
||||
System.Windows.MessageBoxImage.Information,
|
||||
autoCloseMilliseconds: 4000);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -3,8 +3,10 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Threading;
|
||||
using System.Windows.Media;
|
||||
using Autodesk.Navisworks.Api;
|
||||
using NavisworksTransport.Core.Services;
|
||||
using NavisworksTransport.UI.WPF.ViewModels;
|
||||
using NavisworksTransport.Utils;
|
||||
|
||||
@ -34,6 +36,7 @@ namespace NavisworksTransport.UI.WPF.Views
|
||||
{
|
||||
InitializeComponent();
|
||||
ExcludedObjects = new List<ModelItem>();
|
||||
Loaded += CollisionAnalysisDialog_Loaded;
|
||||
|
||||
// 更新统计信息
|
||||
UpdateStatsText(hotspots.Count, totalCollisions);
|
||||
@ -62,6 +65,27 @@ namespace NavisworksTransport.UI.WPF.Views
|
||||
LogManager.Info($"[碰撞分析] 自动选中 {autoSelectedCount}/{viewModels.Count} 个高频碰撞物体(>100次或>50%)");
|
||||
}
|
||||
|
||||
private void CollisionAnalysisDialog_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!TestAutomationHttpService.ShouldAutoConfirmCollisionAnalysisDialogs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
if (!IsLoaded || !IsVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LogManager.Info("[碰撞分析] 测试自动化已接管分析窗口,自动选择继续生成");
|
||||
ModelHighlightHelper.ClearCategory(PrecomputedHighlightCategory);
|
||||
SelectedAction = CollisionAnalysisAction.Continue;
|
||||
Close();
|
||||
}), DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新统计文本
|
||||
/// </summary>
|
||||
|
||||
@ -2,6 +2,7 @@ using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Threading;
|
||||
using Autodesk.Navisworks.Api;
|
||||
|
||||
namespace NavisworksTransport.Utils
|
||||
@ -11,6 +12,14 @@ namespace NavisworksTransport.Utils
|
||||
/// </summary>
|
||||
public static class DialogHelper
|
||||
{
|
||||
private const string TestAutomationEnvironmentVariable = "TRANSPORTPLUGIN_TEST_AUTOMATION";
|
||||
|
||||
public static bool IsTestAutomationModeEnabled =>
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable(TestAutomationEnvironmentVariable),
|
||||
"1",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// 从 UserControl 获取父窗口并设置为对话框 Owner
|
||||
/// </summary>
|
||||
@ -162,6 +171,159 @@ namespace NavisworksTransport.Utils
|
||||
return MessageBox.Show(messageBoxText, caption, button, icon);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示自动关闭的提示窗口,避免在自动化场景中阻塞流程。
|
||||
/// </summary>
|
||||
public static void ShowAutoClosingMessageBox(
|
||||
string messageBoxText,
|
||||
string caption,
|
||||
MessageBoxImage icon = MessageBoxImage.Information,
|
||||
int autoCloseMilliseconds = 4000)
|
||||
{
|
||||
if (IsTestAutomationModeEnabled)
|
||||
{
|
||||
LogManager.Info($"[DialogHelper] 自动测试模式下跳过自动关闭提示框: {caption}");
|
||||
return;
|
||||
}
|
||||
|
||||
var owner = FindOwnerWindow();
|
||||
|
||||
var window = new Window
|
||||
{
|
||||
Title = caption,
|
||||
Width = 460,
|
||||
SizeToContent = SizeToContent.Height,
|
||||
ResizeMode = ResizeMode.NoResize,
|
||||
WindowStartupLocation = owner != null ? WindowStartupLocation.CenterOwner : WindowStartupLocation.CenterScreen,
|
||||
ShowInTaskbar = false,
|
||||
Topmost = true,
|
||||
Content = BuildAutoClosingDialogContent(messageBoxText, icon, autoCloseMilliseconds)
|
||||
};
|
||||
|
||||
if (owner != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
window.Owner = owner;
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
LogManager.Warning($"[DialogHelper] 设置自动关闭提示框 Owner 失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetWin32Owner(window);
|
||||
}
|
||||
|
||||
var closeTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(Math.Max(500, autoCloseMilliseconds))
|
||||
};
|
||||
closeTimer.Tick += (_, __) =>
|
||||
{
|
||||
closeTimer.Stop();
|
||||
if (window.IsVisible)
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
};
|
||||
|
||||
window.Closed += (_, __) => closeTimer.Stop();
|
||||
closeTimer.Start();
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private static FrameworkElement BuildAutoClosingDialogContent(
|
||||
string messageBoxText,
|
||||
MessageBoxImage icon,
|
||||
int autoCloseMilliseconds)
|
||||
{
|
||||
string iconGlyph;
|
||||
switch (icon)
|
||||
{
|
||||
case MessageBoxImage.Warning:
|
||||
iconGlyph = "!";
|
||||
break;
|
||||
case MessageBoxImage.Error:
|
||||
iconGlyph = "X";
|
||||
break;
|
||||
case MessageBoxImage.Question:
|
||||
iconGlyph = "?";
|
||||
break;
|
||||
default:
|
||||
iconGlyph = "i";
|
||||
break;
|
||||
}
|
||||
|
||||
var root = new Grid
|
||||
{
|
||||
Margin = new Thickness(18)
|
||||
};
|
||||
root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
|
||||
var iconText = new TextBlock
|
||||
{
|
||||
Text = iconGlyph,
|
||||
FontSize = 22,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Margin = new Thickness(0, 0, 14, 0),
|
||||
VerticalAlignment = VerticalAlignment.Top
|
||||
};
|
||||
Grid.SetColumn(iconText, 0);
|
||||
Grid.SetRow(iconText, 0);
|
||||
root.Children.Add(iconText);
|
||||
|
||||
var messageText = new TextBlock
|
||||
{
|
||||
Text = messageBoxText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
MinWidth = 320
|
||||
};
|
||||
Grid.SetColumn(messageText, 1);
|
||||
Grid.SetRow(messageText, 0);
|
||||
root.Children.Add(messageText);
|
||||
|
||||
var footerPanel = new DockPanel
|
||||
{
|
||||
Margin = new Thickness(0, 16, 0, 0),
|
||||
LastChildFill = false
|
||||
};
|
||||
Grid.SetColumn(footerPanel, 0);
|
||||
Grid.SetColumnSpan(footerPanel, 2);
|
||||
Grid.SetRow(footerPanel, 1);
|
||||
|
||||
var autoCloseHint = new TextBlock
|
||||
{
|
||||
Text = $"此提示将在 {Math.Max(1, autoCloseMilliseconds / 1000)} 秒后自动关闭。",
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
DockPanel.SetDock(autoCloseHint, Dock.Left);
|
||||
footerPanel.Children.Add(autoCloseHint);
|
||||
|
||||
var closeButton = new Button
|
||||
{
|
||||
Content = "确定",
|
||||
MinWidth = 72,
|
||||
Padding = new Thickness(14, 4, 14, 4),
|
||||
Margin = new Thickness(16, 0, 0, 0)
|
||||
};
|
||||
closeButton.Click += (_, __) =>
|
||||
{
|
||||
Window parentWindow = Window.GetWindow(closeButton);
|
||||
parentWindow?.Close();
|
||||
};
|
||||
DockPanel.SetDock(closeButton, Dock.Right);
|
||||
footerPanel.Children.Add(closeButton);
|
||||
|
||||
root.Children.Add(footerPanel);
|
||||
return root;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Navisworks 主窗口句柄(Win32)
|
||||
/// </summary>
|
||||
|
||||
7
start-navisworks.bat
Normal file
7
start-navisworks.bat
Normal file
@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\start-navisworks.ps1" %*
|
||||
|
||||
if errorlevel 1 exit /b 1
|
||||
exit /b 0
|
||||
Loading…
Reference in New Issue
Block a user