Files
ParkingRobot/ClumsyPilot/tests/verify_path_smoothing_service.ps1
T

224 lines
13 KiB
PowerShell

param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'))
$ErrorActionPreference = 'Stop'
$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath))
function Assert-True($Actual, [string]$Message) {
if (-not $Actual) { throw $Message }
}
function Assert-Equal($Expected, $Actual, [string]$Message) {
if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" }
}
function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) {
if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) {
throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance"
}
}
function Get-RequiredType([string]$Name) {
return $assembly.GetType($Name, $true)
}
function New-EmptyMap {
$mapRequest = [Activator]::CreateInstance($mapRequestType)
$mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000))
$mapRequest.ResolutionMm = [single]50
$mapRequest.AllowExplicitEmptyMap = $true
$map = [Activator]::CreateInstance($mapFactoryType).Create($mapRequest).Map
Assert-True ($null -ne $map) 'Service test must create an explicit empty planning map.'
return $map
}
function New-Vehicle {
$vehicle = [Activator]::CreateInstance($vehicleType)
$vehicle.LengthMeters = [double]0.20
$vehicle.WidthMeters = [double]0.20
$vehicle.SafetyMarginMeters = [double]0.0
$vehicle.MaximumCurvaturePerMeter = [double]100.0
return $vehicle
}
function New-CoarsePoint(
[double]$X,
[double]$Y,
[double]$ArcLength,
$Direction,
[double]$BodyClearance = 1.0,
[bool]$IsGearSwitch = $false) {
return [Activator]::CreateInstance($coarsePointType, @(
$X, $Y, [double]0.0, [double]0.0, $ArcLength, $Direction,
[double]0.0, $BodyClearance, $IsGearSwitch, $coarseAnchor))
}
function New-Configuration($Method = $cubicBSpline) {
$configuration = [Activator]::CreateInstance($configurationType)
$configuration.Method = $Method
return $configuration
}
function New-Request([object[]]$Points, $Configuration) {
$typedPoints = [Array]::CreateInstance($coarsePointType, $Points.Count)
for ($index = 0; $index -lt $Points.Count; $index++) {
$typedPoints.SetValue($Points[$index], $index)
}
$segments = [Array]::CreateInstance($coarseSegmentType, 1)
$segments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(
0, $forward, 0, ($Points.Count - 1), $false, $false)), 0)
return [Activator]::CreateInstance($requestType, @($typedPoints, $segments, (New-EmptyMap), (New-Vehicle), $Configuration))
}
function New-StraightRequest($Configuration) {
return New-Request @(
(New-CoarsePoint 0.5 0.5 0.0 $forward),
(New-CoarsePoint 1.5 0.5 1.0 $forward)) $Configuration
}
function New-InfeasibleRequest($Configuration) {
# Zero declared movement clearance makes every non-linear B-spline displacement retryably infeasible,
# while the empty map still permits the independently revalidated coarse-path fallback.
return New-Request @(
(New-CoarsePoint 0.5 0.5 0.0 $forward 0.0),
(New-CoarsePoint 1.0 0.5 0.5 $forward 0.0),
(New-CoarsePoint 1.0 1.0 1.0 $forward 0.0),
(New-CoarsePoint 1.5 1.0 1.5 $forward 0.0)) $Configuration
}
function Invoke-Smooth($Request, [Threading.CancellationToken]$CancellationToken = [Threading.CancellationToken]::None) {
return $smoothMethod.Invoke($service, @($Request, $CancellationToken))
}
function Assert-NoGeometry($Result, [string]$Message) {
Assert-Equal 0 $Result.Path.Count "$Message A non-published result must not expose a path."
Assert-Equal 0 $Result.Segments.Count "$Message A non-published result must not expose segments."
}
function Assert-InvalidInputBeforeRetry($Configuration, [string]$CaseName) {
$result = Invoke-Smooth (New-StraightRequest $Configuration)
Assert-Equal 'InvalidInput' $result.Status.ToString() "$CaseName must be rejected as invalid input."
Assert-NoGeometry $result $CaseName
Assert-Equal 0 $result.Diagnostics.RetryCount "$CaseName must be rejected before any smoothing retry."
Assert-Near 0.0 $result.Diagnostics.AcceptedStrength 0.0 "$CaseName must not accept a smoothing strength."
}
$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.'
$facade = $root + 'Facade.'
$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.'
$mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.'
$serviceType = Get-RequiredType ($facade + 'PathSmoothingService')
$requestType = Get-RequiredType ($root + 'PathSmoothingRequest')
$resultType = Get-RequiredType ($root + 'PathSmoothingResult')
$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration')
$methodType = Get-RequiredType ($root + 'SmoothingMethod')
$coarsePointType = Get-RequiredType ($coarsePath + 'CoarsePathPoint')
$coarseSegmentType = Get-RequiredType ($coarsePath + 'PathSegment')
$directionType = Get-RequiredType ($coarsePath + 'TravelDirection')
$coarsePointSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource')
$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters')
$boundsType = Get-RequiredType ($mapping + 'MapBoundsMm')
$mapRequestType = Get-RequiredType ($mapping + 'PlanningMapRequest')
$mapFactoryType = Get-RequiredType ($mapping + 'PlanningMapFactory')
Assert-True $serviceType.IsPublic 'PathSmoothingService must be public.'
$service = [Activator]::CreateInstance($serviceType)
$smoothMethod = $serviceType.GetMethod('Smooth', [Type[]]@($requestType, [Threading.CancellationToken]))
Assert-True ($null -ne $smoothMethod) 'PathSmoothingService must expose Smooth(PathSmoothingRequest, CancellationToken).'
Assert-Equal $resultType $smoothMethod.ReturnType 'PathSmoothingService Smooth must return PathSmoothingResult.'
$forward = [Enum]::Parse($directionType, 'Forward')
$cubicBSpline = [Enum]::Parse($methodType, 'CubicBSpline')
$localCubicBezier = [Enum]::Parse($methodType, 'LocalCubicBezier')
$piecewiseQuintic = [Enum]::Parse($methodType, 'PiecewiseQuintic')
$coarseAnchor = [Enum]::Parse($coarsePointSourceType, 'Start')
# The public method registry must retain every stable enum-to-algorithm mapping.
foreach ($method in @($cubicBSpline, $localCubicBezier, $piecewiseQuintic)) {
$result = Invoke-Smooth (New-StraightRequest (New-Configuration $method))
Assert-Equal 'Success' $result.Status.ToString() "A valid straight path must succeed for $method."
Assert-Equal $method.ToString() $result.Method.ToString() "The result must retain the selected $method method."
Assert-True ($result.Path.Count -gt 0) "A successful $method result must publish geometry."
Assert-True $result.Diagnostics.Metrics.IsFeasible "A successful $method result must publish feasible diagnostics."
}
# Every configuration scalar is checked before the options snapshot or retry runner starts.
$invalidConfigurationCases = @(
[PSCustomObject]@{ Name = 'NaN output spacing'; Mutate = { param($c) $c.OutputSpacingMeters = [double]::NaN } },
[PSCustomObject]@{ Name = 'zero output spacing'; Mutate = { param($c) $c.OutputSpacingMeters = [double]0.0 } },
[PSCustomObject]@{ Name = 'infinite collision step'; Mutate = { param($c) $c.MaximumCollisionCheckStepMeters = [double]::PositiveInfinity } },
[PSCustomObject]@{ Name = 'zero collision step'; Mutate = { param($c) $c.MaximumCollisionCheckStepMeters = [double]0.0 } },
[PSCustomObject]@{ Name = 'NaN clearance reserve'; Mutate = { param($c) $c.MinimumClearanceReserveMeters = [double]::NaN } },
[PSCustomObject]@{ Name = 'negative clearance reserve'; Mutate = { param($c) $c.MinimumClearanceReserveMeters = [double]-0.01 } },
[PSCustomObject]@{ Name = 'NaN smoothing strength'; Mutate = { param($c) $c.SmoothingStrength = [double]::NaN } },
[PSCustomObject]@{ Name = 'zero smoothing strength'; Mutate = { param($c) $c.SmoothingStrength = [double]0.0 } },
[PSCustomObject]@{ Name = 'NaN B-spline scale'; Mutate = { param($c) $c.CubicBSpline.EndpointTangentScale = [double]::NaN } },
[PSCustomObject]@{ Name = 'zero B-spline scale'; Mutate = { param($c) $c.CubicBSpline.EndpointTangentScale = [double]0.0 } },
[PSCustomObject]@{ Name = 'zero Bezier threshold'; Mutate = { param($c) $c.LocalCubicBezier.CornerHeadingThresholdRadians = [double]0.0 } },
[PSCustomObject]@{ Name = 'over-pi Bezier threshold'; Mutate = { param($c) $c.LocalCubicBezier.CornerHeadingThresholdRadians = [Math]::PI + 0.01 } },
[PSCustomObject]@{ Name = 'NaN Bezier window'; Mutate = { param($c) $c.LocalCubicBezier.MaximumWindowLengthMeters = [double]::NaN } },
[PSCustomObject]@{ Name = 'zero Bezier window'; Mutate = { param($c) $c.LocalCubicBezier.MaximumWindowLengthMeters = [double]0.0 } },
[PSCustomObject]@{ Name = 'infinite Bezier handle scale'; Mutate = { param($c) $c.LocalCubicBezier.HandleLengthRatio = [double]::PositiveInfinity } },
[PSCustomObject]@{ Name = 'zero Bezier handle scale'; Mutate = { param($c) $c.LocalCubicBezier.HandleLengthRatio = [double]0.0 } },
[PSCustomObject]@{ Name = 'NaN quintic knot spacing'; Mutate = { param($c) $c.PiecewiseQuintic.KnotSpacingMeters = [double]::NaN } },
[PSCustomObject]@{ Name = 'zero quintic knot spacing'; Mutate = { param($c) $c.PiecewiseQuintic.KnotSpacingMeters = [double]0.0 } },
[PSCustomObject]@{ Name = 'infinite minimum quintic knot spacing'; Mutate = { param($c) $c.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]::PositiveInfinity } },
[PSCustomObject]@{ Name = 'zero minimum quintic knot spacing'; Mutate = { param($c) $c.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]0.0 } },
[PSCustomObject]@{ Name = 'quintic knot spacing below minimum'; Mutate = { param($c) $c.PiecewiseQuintic.KnotSpacingMeters = [double]0.05; $c.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]0.10 } }
)
foreach ($case in $invalidConfigurationCases) {
$configuration = New-Configuration
& $case.Mutate $configuration
Assert-InvalidInputBeforeRetry $configuration $case.Name
}
$unknownMethodConfiguration = New-Configuration ([Enum]::ToObject($methodType, 99))
Assert-InvalidInputBeforeRetry $unknownMethodConfiguration 'unknown smoothing method'
$invalidCoarseConfiguration = New-Configuration
$invalidCoarseRequest = New-Request @(
(New-CoarsePoint ([double]::NaN) 0.5 0.0 $forward),
(New-CoarsePoint 1.5 0.5 1.0 $forward)) $invalidCoarseConfiguration
$invalidCoarseResult = Invoke-Smooth $invalidCoarseRequest
Assert-Equal 'InvalidInput' $invalidCoarseResult.Status.ToString() 'A non-finite coarse path coordinate must be invalid input.'
Assert-NoGeometry $invalidCoarseResult 'Invalid coarse path'
Assert-Equal 0 $invalidCoarseResult.Diagnostics.RetryCount 'Invalid coarse input must be rejected before retries.'
$cancelledConfiguration = New-Configuration
$cancellationSource = [Threading.CancellationTokenSource]::new()
$cancellationSource.Cancel()
try {
$cancelledResult = Invoke-Smooth (New-StraightRequest $cancelledConfiguration) $cancellationSource.Token
Assert-Equal 'Cancelled' $cancelledResult.Status.ToString() 'Pre-cancelled smoothing must return the explicit cancellation result.'
Assert-NoGeometry $cancelledResult 'Cancelled smoothing'
Assert-Equal 0 $cancelledResult.Diagnostics.RetryCount 'Cancellation before execution must not start retries.'
}
finally {
$cancellationSource.Dispose()
}
$withoutFallbackConfiguration = New-Configuration
$withoutFallbackConfiguration.AllowFallbackToCoarsePath = $false
$withoutFallbackResult = Invoke-Smooth (New-InfeasibleRequest $withoutFallbackConfiguration)
Assert-Equal 'Infeasible' $withoutFallbackResult.Status.ToString() 'A retryably infeasible candidate without fallback must remain infeasible.'
Assert-NoGeometry $withoutFallbackResult 'Infeasible smoothing without fallback'
Assert-Equal 3 $withoutFallbackResult.Diagnostics.RetryCount 'Infeasible smoothing must exhaust the four configured strengths.'
Assert-Near 0.0 $withoutFallbackResult.Diagnostics.AcceptedStrength 0.0 'Infeasible smoothing must not accept a strength.'
$withFallbackConfiguration = New-Configuration
$withFallbackConfiguration.AllowFallbackToCoarsePath = $true
$withFallbackResult = Invoke-Smooth (New-InfeasibleRequest $withFallbackConfiguration)
Assert-Equal 'FallbackToCoarsePath' $withFallbackResult.Status.ToString() 'A verified coarse path must return explicit fallback status.'
Assert-Equal 'CubicBSpline' $withFallbackResult.Method.ToString() 'Fallback must retain the originally selected method.'
Assert-True ($withFallbackResult.Path.Count -gt 0) 'A verified fallback must publish the revalidated coarse geometry.'
Assert-True $withFallbackResult.Diagnostics.Metrics.IsFeasible 'Fallback must publish feasible shared-geometry diagnostics.'
foreach ($point in $withFallbackResult.Path) {
Assert-Equal 'CoarsePathFallback' $point.Source.ToString() 'Every fallback point must be explicitly labeled as coarse-path fallback.'
}
Assert-Equal $withoutFallbackResult.Diagnostics.RetryCount $withFallbackResult.Diagnostics.RetryCount 'Fallback must preserve retry diagnostics from the failed method.'
Assert-Near $withoutFallbackResult.Diagnostics.AcceptedStrength $withFallbackResult.Diagnostics.AcceptedStrength 0.0 'Fallback must preserve the failed method accepted-strength diagnostic.'
Assert-Equal $withoutFallbackResult.Diagnostics.TerminationReason $withFallbackResult.Diagnostics.TerminationReason 'Fallback must preserve the failed method termination reason.'
Write-Output 'Path smoothing service checks passed.'