189 lines
14 KiB
PowerShell
189 lines
14 KiB
PowerShell
param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'))
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$newtonsoftPath = Join-Path $env:USERPROFILE '.nuget\packages\newtonsoft.json\13.0.4\lib\netstandard2.0\Newtonsoft.Json.dll'
|
|
if (Test-Path -LiteralPath $newtonsoftPath) { [Reflection.Assembly]::LoadFrom($newtonsoftPath) | Out-Null }
|
|
$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath))
|
|
|
|
function Assert-Equal($Expected, $Actual, [string]$Message) {
|
|
if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" }
|
|
}
|
|
|
|
function Assert-True($Actual, [string]$Message) {
|
|
if (-not $Actual) { throw $Message }
|
|
}
|
|
|
|
function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) {
|
|
if ([Math]::Abs($Expected - $Actual) -gt 0.000000001) {
|
|
throw "$Message Expected=$Expected Actual=$Actual"
|
|
}
|
|
}
|
|
|
|
function Get-RequiredType([string]$Name) {
|
|
return $assembly.GetType($Name, $true)
|
|
}
|
|
|
|
function Text-FromCodePoints([int[]]$CodePoints) {
|
|
return (($CodePoints | ForEach-Object { [Char]::ConvertFromUtf32($_) }) -join '')
|
|
}
|
|
|
|
$visualization = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization.'
|
|
$styleType = Get-RequiredType ($visualization + 'IeeeFigureStyle')
|
|
|
|
foreach ($expectation in @(
|
|
@{ Name = 'FigureWidthPoints'; Value = 515.52 },
|
|
@{ Name = 'FigureHeightPoints'; Value = 374.4 },
|
|
@{ Name = 'RawColor'; Value = '#4D4D4D' },
|
|
@{ Name = 'BSplineColor'; Value = '#0072B2' },
|
|
@{ Name = 'BezierColor'; Value = '#D55E00' },
|
|
@{ Name = 'QuinticColor'; Value = '#009E73' },
|
|
@{ Name = 'LimitColor'; Value = '#CC79A7' }
|
|
)) {
|
|
$field = $styleType.GetField($expectation.Name)
|
|
if ($null -eq $field) { throw "IeeeFigureStyle must expose $($expectation.Name)." }
|
|
if ($expectation.Value -is [double]) {
|
|
Assert-Near $expectation.Value $field.GetValue($null) "IeeeFigureStyle $($expectation.Name) must match the fixed report style."
|
|
}
|
|
else {
|
|
Assert-Equal $expectation.Value $field.GetValue($null) "IeeeFigureStyle $($expectation.Name) must match the fixed report style."
|
|
}
|
|
}
|
|
|
|
$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.'
|
|
$comparison = $root + 'Comparison.'
|
|
$facade = $root + 'Facade.'
|
|
$coarse = 'MultiWheelC.TrajectoryPlanning.CoarsePath.'
|
|
$mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.'
|
|
$test = $root + 'Test.'
|
|
|
|
$modelType = Get-RequiredType ($visualization + 'SmoothingFigureModel')
|
|
$builderType = Get-RequiredType ($visualization + 'SmoothingFigureModelBuilder')
|
|
$figureDefinitionType = Get-RequiredType ($visualization + 'SmoothingFigureDefinition')
|
|
$figureSetBuilderType = Get-RequiredType ($visualization + 'SmoothingFigureSetBuilder')
|
|
$rendererType = Get-RequiredType ($visualization + 'SmoothingSvgRenderer')
|
|
$csvWriterType = Get-RequiredType ($visualization + 'SmoothingCsvWriter')
|
|
$comparisonResultType = Get-RequiredType ($comparison + 'PathSmoothingComparisonResult')
|
|
$comparisonRequestType = Get-RequiredType ($comparison + 'PathSmoothingComparisonRequest')
|
|
$comparisonServiceType = Get-RequiredType ($facade + 'PathSmoothingComparisonService')
|
|
$scenarioFactoryType = Get-RequiredType ($test + 'SmoothingScenarioFactory')
|
|
$gridMapType = Get-RequiredType ($mapping + 'PlanningGridMap')
|
|
$poseType = Get-RequiredType ($coarse + 'Pose2D')
|
|
|
|
$buildMethod = $builderType.GetMethod('Build', [Type[]]@($comparisonResultType, $gridMapType, $poseType, $poseType, [string], [string]))
|
|
if ($null -eq $buildMethod) { throw 'SmoothingFigureModelBuilder must expose Build(comparison, map, start, goal, scenarioId, scenarioLabel).' }
|
|
Assert-Equal $modelType $buildMethod.ReturnType 'SmoothingFigureModelBuilder.Build must return the immutable figure model.'
|
|
$figureSetBuildMethod = $figureSetBuilderType.GetMethod('Build', [Type[]]@($modelType))
|
|
if ($null -eq $figureSetBuildMethod) { throw 'SmoothingFigureSetBuilder must expose Build(SmoothingFigureModel).' }
|
|
$renderMethod = $rendererType.GetMethod('Render', [Type[]]@($figureDefinitionType))
|
|
if ($null -eq $renderMethod) { throw 'SmoothingSvgRenderer must expose Render(SmoothingFigureDefinition).' }
|
|
$writeMethod = $csvWriterType.GetMethod('Write', [Type[]]@($modelType))
|
|
if ($null -eq $writeMethod) { throw 'SmoothingCsvWriter must expose Write(SmoothingFigureModel).' }
|
|
|
|
$fixturePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'
|
|
$fixtureRequests = $scenarioFactoryType.GetMethod('CreateFixtureRequests', [Type[]]@([string])).Invoke($null, @((Resolve-Path $fixturePath).Path))
|
|
$comparisonRequest = $fixtureRequests[0]
|
|
Assert-Equal $comparisonRequestType $comparisonRequest.GetType() 'The figure test must consume a real immutable comparison request.'
|
|
$comparisonResult = [Activator]::CreateInstance($comparisonServiceType).Compare($comparisonRequest, [Threading.CancellationToken]::None)
|
|
$coarsePath = $comparisonRequest.SmoothingRequest.CoarsePath
|
|
$start = [Activator]::CreateInstance($poseType, @($coarsePath[0].X, $coarsePath[0].Y, $coarsePath[0].Heading))
|
|
$last = $coarsePath[$coarsePath.Count - 1]
|
|
$goal = [Activator]::CreateInstance($poseType, @($last.X, $last.Y, $last.Heading))
|
|
$builder = [Activator]::CreateInstance($builderType)
|
|
$rawLabel = Text-FromCodePoints @(0x539F, 0x59CB, 0x7C97, 0x8DEF, 0x5F84)
|
|
$bSplineLabel = (Text-FromCodePoints @(0x4E09, 0x6B21)) + ' B ' + (Text-FromCodePoints @(0x6837, 0x6761))
|
|
$bezierLabel = (Text-FromCodePoints @(0x5C40, 0x90E8, 0x4E09, 0x6B21)) + ' B' + [Char]::ConvertFromUtf32(0x00E9) + 'zier'
|
|
$quinticLabel = Text-FromCodePoints @(0x5206, 0x6BB5, 0x4E94, 0x6B21)
|
|
$scenarioId = (Text-FromCodePoints @(0x573A, 0x666F)) + '-escape'
|
|
$scenarioLabel = (Text-FromCodePoints @(0x8DEF, 0x5F84)) + ' <A&>'
|
|
$model = $buildMethod.Invoke($builder, @($comparisonResult, $comparisonRequest.SmoothingRequest.Map, $start, $goal, $scenarioId, $scenarioLabel))
|
|
|
|
$figureSet = $figureSetBuildMethod.Invoke([Activator]::CreateInstance($figureSetBuilderType), @($model))
|
|
$figures = @($figureSet.Figures)
|
|
$expectedStems = @('01-coarse-path-overview', '02-all-paths-comparison', '03-cubic-bspline-overview', '04-local-cubic-bezier-overview', '05-piecewise-quintic-overview', '06-curvature-comparison')
|
|
Assert-Equal ($expectedStems -join ',') (($figures | ForEach-Object { $_.FileStem }) -join ',') 'Figure set must publish the stable six-file order.'
|
|
Assert-True (-not $figures[1].ShowsMapContext) 'All-path comparison must exclude obstacles and endpoint decorations.'
|
|
Assert-True ($figures[2].ShowsMapContext -and $figures[2].Series[0].Opacity -lt 1) 'Individual smoother views must retain faded coarse-path context.'
|
|
Assert-Near (($figures[0].WorldXMaxMeters - $figures[0].WorldXMinMeters) / ($figures[0].WorldYMaxMeters - $figures[0].WorldYMinMeters)) ($figures[0].PlotWidthPoints / $figures[0].PlotHeightPoints) 'Overhead figures must preserve equal X/Y scale.'
|
|
|
|
$renderer = [Activator]::CreateInstance($rendererType)
|
|
$svgByStem = @{}
|
|
foreach ($figure in $figures) { $svgByStem[$figure.FileStem] = $renderMethod.Invoke($renderer, @($figure)) }
|
|
$svg = (($expectedStems | ForEach-Object { $svgByStem[$_] }) -join "`n")
|
|
Assert-True ($svg -match '<svg[^>]+viewBox="0 0 515\.52 374\.4"') 'SVG must use the fixed physical view box in typographic points.'
|
|
Assert-True $svg.Contains(($scenarioLabel -replace '&', '&' -replace '<', '<' -replace '>', '>')) 'SVG text must XML-escape scenario labels.'
|
|
Assert-True $svg.Contains('font-family="SimSun"') 'SVG must emit explicit SimSun Chinese text runs.'
|
|
Assert-True $svg.Contains('font-family="Times New Roman"') 'SVG must emit explicit Times New Roman Latin/math text runs.'
|
|
foreach ($color in @('#4D4D4D', '#0072B2', '#D55E00', '#009E73', '#CC79A7')) { Assert-True $svg.Contains($color) "SVG must retain the fixed color $color." }
|
|
Assert-True $svg.Contains('class="trajectory-point"') 'All visible trajectory samples must render as point markers.'
|
|
Assert-True (-not $svg.Contains('stroke-dasharray')) 'Trajectory reports must not use dashed path styles.'
|
|
Assert-True (-not $svg.Contains('<path')) 'Trajectory reports must not join samples with SVG path elements.'
|
|
Assert-True $svgByStem['02-all-paths-comparison'].Contains('X (m)') 'Overhead comparison must label the X coordinate in metres.'
|
|
Assert-True $svgByStem['06-curvature-comparison'].Contains('s (m)') 'Curvature comparison must label arc length in metres.'
|
|
$curvatureUnit = [Char]::ConvertFromUtf32(0x03BA) + ' (m' + [Char]::ConvertFromUtf32(0x207B) + [Char]::ConvertFromUtf32(0x00B9) + ')'
|
|
Assert-True $svgByStem['06-curvature-comparison'].Contains($curvatureUnit) 'Curvature comparison must label curvature units.'
|
|
|
|
$priorCulture = [Globalization.CultureInfo]::CurrentCulture
|
|
try {
|
|
[Globalization.CultureInfo]::CurrentCulture = [Globalization.CultureInfo]::GetCultureInfo('de-DE')
|
|
[byte[]]$csvBytes = $writeMethod.Invoke([Activator]::CreateInstance($csvWriterType), @($model))
|
|
}
|
|
finally {
|
|
[Globalization.CultureInfo]::CurrentCulture = $priorCulture
|
|
}
|
|
Assert-Equal 0xEF $csvBytes[0] 'CSV must begin with the UTF-8 BOM.'
|
|
Assert-Equal 0xBB $csvBytes[1] 'CSV must begin with the UTF-8 BOM.'
|
|
Assert-Equal 0xBF $csvBytes[2] 'CSV must begin with the UTF-8 BOM.'
|
|
$csv = [Text.Encoding]::UTF8.GetString($csvBytes, 3, $csvBytes.Length - 3)
|
|
$header = 'ScenarioId,Method,Status,PathLengthMeters,MaximumAbsoluteVehicleCurvaturePerMeter,RootMeanSquareVehicleCurvaturePerMeter,TotalAbsoluteCurvatureVariationPerMeter,CurvatureVariationEnergy,MinimumBodyClearanceMeters,MedianElapsedMilliseconds,TimingSampleCount,IsDeterministic,RetryCount,AcceptedStrength'
|
|
Assert-True $csv.StartsWith($header + "`r`n") 'CSV must preserve the specified stable header order.'
|
|
Assert-True $csv.Contains($scenarioId) 'CSV must preserve UTF-8 Chinese scenario content.'
|
|
Assert-True ($csv -match ('(?m)^' + [Regex]::Escape($scenarioId) + ',[^,]+,[^,]+,\d+\.\d+')) 'CSV numeric values must use invariant decimal points.'
|
|
|
|
$entryType = Get-RequiredType ($comparison + 'PathSmoothingComparisonEntry')
|
|
$methodType = Get-RequiredType ($root + 'SmoothingMethod')
|
|
$statusType = Get-RequiredType ($root + 'PathSmoothingStatus')
|
|
$metricsType = Get-RequiredType ($root + 'PathQualityMetrics')
|
|
$pointType = Get-RequiredType ($root + 'SmoothedPathPoint')
|
|
$segmentType = Get-RequiredType ($root + 'SmoothedPathSegment')
|
|
$binding = [Reflection.BindingFlags]::Static -bor [Reflection.BindingFlags]::NonPublic
|
|
$candidateFactory = $entryType.GetMethods($binding) | Where-Object { $_.Name -eq 'CreateCandidate' } | Select-Object -First 1
|
|
if ($null -eq $candidateFactory) { throw 'Comparison entry must retain the internal candidate factory used by report diagnostics.' }
|
|
$resultCtor = $comparisonResultType.GetConstructors([Reflection.BindingFlags]::Instance -bor [Reflection.BindingFlags]::NonPublic) |
|
|
Where-Object { $_.GetParameters().Count -eq 5 } | Select-Object -First 1
|
|
if ($null -eq $resultCtor) { throw 'Comparison result must retain its immutable internal constructor.' }
|
|
$infeasible = [Enum]::Parse($statusType, 'Infeasible')
|
|
$failed = [Enum]::Parse($statusType, 'Failed')
|
|
$bSpline = [Enum]::Parse($methodType, 'CubicBSpline')
|
|
$bezier = [Enum]::Parse($methodType, 'LocalCubicBezier')
|
|
$quintic = [Enum]::Parse($methodType, 'PiecewiseQuintic')
|
|
$raw = $comparisonResult.RawPathBaseline
|
|
$timing = $comparisonResult.Entries[0].Timing
|
|
$infeasibleEntry = $candidateFactory.Invoke($null, [object[]]@($bSpline, $infeasible, [Activator]::CreateInstance($metricsType), $timing, 'synthetic-infeasible', 'rejected geometry', 1, [double]0.5, $raw.Path, $raw.Segments))
|
|
$emptyPoints = [Array]::CreateInstance($pointType, 0)
|
|
$emptySegments = [Array]::CreateInstance($segmentType, 0)
|
|
$failedEntry = $candidateFactory.Invoke($null, [object[]]@($bezier, $failed, [Activator]::CreateInstance($metricsType), $timing, 'synthetic-failure', 'numeric failure', 0, [double]0.0, $emptyPoints, $emptySegments))
|
|
$quinticEntry = $comparisonResult.Entries | Where-Object { $_.Method.ToString() -eq $quintic.ToString() } | Select-Object -First 1
|
|
$syntheticEntries = [Array]::CreateInstance($entryType, 3)
|
|
$syntheticEntries.SetValue($infeasibleEntry, 0)
|
|
$syntheticEntries.SetValue($failedEntry, 1)
|
|
$syntheticEntries.SetValue($quinticEntry, 2)
|
|
$syntheticResult = $resultCtor.Invoke([object[]]@($raw, $syntheticEntries, $null, $false, 'synthetic report diagnostics'))
|
|
$syntheticModel = $buildMethod.Invoke($builder, @($syntheticResult, $comparisonRequest.SmoothingRequest.Map, $start, $goal, 'diagnostics', 'diagnostics'))
|
|
$infeasibleSeries = $syntheticModel.Series | Where-Object { $_.Key -eq 'bspline' } | Select-Object -First 1
|
|
$failedSeries = $syntheticModel.Series | Where-Object { $_.Key -eq 'bezier' } | Select-Object -First 1
|
|
Assert-True $infeasibleSeries.IsCurveVisible 'A complete rejected candidate must remain visible in the report model.'
|
|
Assert-True ($infeasibleSeries.ViolationMarkers.Count -gt 0) 'A rejected candidate must expose at least one violation cross marker.'
|
|
Assert-True (-not $failedSeries.IsCurveVisible) 'A numerical failure with no geometry must not produce a fake curve.'
|
|
$syntheticFigureSet = $figureSetBuildMethod.Invoke([Activator]::CreateInstance($figureSetBuilderType), @($syntheticModel))
|
|
$syntheticSvg = $renderMethod.Invoke($renderer, @($syntheticFigureSet.Figures[2]))
|
|
Assert-True $syntheticSvg.Contains('data-series="bspline"') 'Rejected candidate geometry must remain visible in the SVG.'
|
|
Assert-True $syntheticSvg.Contains('violation-cross') 'Rejected candidate SVG geometry must include violation crosses.'
|
|
Assert-True $syntheticSvg.Contains('Infeasible') 'Legend must retain the rejected candidate status.'
|
|
Assert-True (-not $syntheticSvg.Contains('data-series="bezier"')) 'Numerical failure must not emit fake trajectory samples.'
|
|
$syntheticCsvBytes = $writeMethod.Invoke([Activator]::CreateInstance($csvWriterType), @($syntheticModel))
|
|
$syntheticCsv = [Text.Encoding]::UTF8.GetString($syntheticCsvBytes, 3, $syntheticCsvBytes.Length - 3)
|
|
Assert-True $syntheticCsv.Contains('CubicBSpline,Infeasible') 'CSV must retain the rejected candidate status.'
|
|
Assert-True $syntheticCsv.Contains('LocalCubicBezier,Failed') 'CSV must retain a numerical failure row.'
|
|
|
|
Write-Output 'Path smoothing SVG/CSV checks passed.'
|