89 lines
7.3 KiB
PowerShell
89 lines
7.3 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-False($Actual, [string]$Message) { if ($Actual) { throw $Message } }
|
|
function Assert-Null($Actual, [string]$Message) { if ($null -ne $Actual) { throw $Message } }
|
|
function Find-NonPublicInstanceMethod($Type, [string]$Name, [Type[]]$ParameterTypes) {
|
|
foreach ($candidate in $Type.GetMethods([Reflection.BindingFlags]'Instance,NonPublic')) {
|
|
if ($candidate.Name -ne $Name) { continue }
|
|
$parameters = $candidate.GetParameters()
|
|
if ($parameters.Length -ne $ParameterTypes.Length) { continue }
|
|
$matches = $true
|
|
for ($index = 0; $index -lt $parameters.Length; $index++) {
|
|
if ($parameters[$index].ParameterType -ne $ParameterTypes[$index]) { $matches = $false; break }
|
|
}
|
|
if ($matches) { return $candidate }
|
|
}
|
|
return $null
|
|
}
|
|
$ns = 'MultiWheelC.TrajectoryPlanning.Mapping.'
|
|
$boundsType = $assembly.GetType($ns + 'MapBoundsMm', $true)
|
|
$obstacleType = $assembly.GetType($ns + 'IMapObstacle', $true)
|
|
$sourceType = $assembly.GetType($ns + 'IMapObstacleSource', $true)
|
|
$circleType = $assembly.GetType($ns + 'CircleObstacle', $true)
|
|
$manualType = $assembly.GetType($ns + 'ManualObstacleSource', $true)
|
|
$twoLegInputType = $assembly.GetType($ns + 'TwoLegProjectionInput', $true)
|
|
$twoLegType = $assembly.GetType($ns + 'TwoLegObstacleSource', $true)
|
|
$requestType = $assembly.GetType($ns + 'PlanningMapRequest', $true)
|
|
$factoryType = $assembly.GetType($ns + 'PlanningMapFactory', $true)
|
|
$mapResultType = $assembly.GetType($ns + 'PlanningMapBuildResult', $true)
|
|
$mapBuildStatusType = $assembly.GetType($ns + 'PlanningMapBuildStatus', $true)
|
|
$operationBudgetType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget', $false)
|
|
$operationStopReasonType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationStopReason', $false)
|
|
Assert-True ($operationBudgetType -ne $null) 'PlanningOperationBudget must exist for shared planning cancellation and timeout handling.'
|
|
Assert-True ($operationStopReasonType -ne $null) 'PlanningOperationStopReason must exist for shared planning cancellation and timeout handling.'
|
|
Assert-True $operationStopReasonType.IsEnum 'PlanningOperationStopReason must be an enum.'
|
|
foreach ($expectedStopReason in @('None', 'Cancelled', 'TimedOut')) {
|
|
Assert-True ($operationStopReasonType.GetEnumNames() -contains $expectedStopReason) "PlanningOperationStopReason must contain $expectedStopReason."
|
|
}
|
|
Assert-True ($mapResultType.GetProperty('Status') -ne $null) 'PlanningMapBuildResult must expose an explicit build status.'
|
|
foreach ($expectedMapStatus in @('Success', 'Failed', 'Cancelled', 'TimedOut')) {
|
|
Assert-True ($mapBuildStatusType.GetEnumNames() -contains $expectedMapStatus) "PlanningMapBuildStatus must contain $expectedMapStatus."
|
|
}
|
|
$bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]2000, [single]0, [single]2000))
|
|
$obstacles = [Array]::CreateInstance($obstacleType, 1)
|
|
$obstacles.SetValue([Activator]::CreateInstance($circleType, @([single]500, [single]500, [single]100)), 0)
|
|
$manual = [Activator]::CreateInstance($manualType, @('manual', [long]1, $true, $obstacles))
|
|
$twoLegInput = [Activator]::CreateInstance($twoLegInputType, @($true, [single]1000, [single]1000, [double]([Math]::PI / 2), [single]100, [single]0, [single]-100, [single]0, [single]40, 'test snapshot'))
|
|
$twoLeg = [Activator]::CreateInstance($twoLegType, @('two-leg', [long]1, $false, $twoLegInput))
|
|
$sources = [Array]::CreateInstance($sourceType, 2); $sources.SetValue($twoLeg, 0); $sources.SetValue($manual, 1)
|
|
function New-Request($sourceArray, [bool]$allowEmpty) { $request = [Activator]::CreateInstance($requestType); $request.Bounds = $bounds; $request.ResolutionMm = [single]50; $request.ObstacleSources = $sourceArray; $request.AllowExplicitEmptyMap = $allowEmpty; return $request }
|
|
$factory = [Activator]::CreateInstance($factoryType)
|
|
$budgetConstructor = $operationBudgetType.GetConstructor([Reflection.BindingFlags]'Instance,NonPublic', $null,
|
|
@([Threading.CancellationToken], [TimeSpan]), $null)
|
|
Assert-True ($budgetConstructor -ne $null) 'PlanningOperationBudget must expose its internal cancellation and timeout constructor.'
|
|
$createWithBudget = Find-NonPublicInstanceMethod $factoryType 'Create' @($requestType, $operationBudgetType)
|
|
Assert-True ($createWithBudget -ne $null) 'PlanningMapFactory must expose an internal budget-aware Create overload.'
|
|
$cancelledSource = New-Object Threading.CancellationTokenSource
|
|
$cancelledSource.Cancel()
|
|
$cancelledBudget = $budgetConstructor.Invoke(@($cancelledSource.Token, [TimeSpan]::FromSeconds(1)))
|
|
$cancelled = $createWithBudget.Invoke($factory, @((New-Request $sources $false), $cancelledBudget))
|
|
Assert-Equal 'Cancelled' $cancelled.Status.ToString() 'Cancelled map construction must retain the cancellation status.'
|
|
Assert-False $cancelled.Succeeded 'Cancelled map construction must not succeed.'
|
|
Assert-Null $cancelled.Map 'Cancelled map construction must not publish a map.'
|
|
Assert-Equal 'None' $cancelled.CacheHit.ToString() 'Cancelled map construction must not publish a cache hit.'
|
|
$first = $factory.Create((New-Request $sources $false))
|
|
Assert-True $first.Succeeded 'Mixed source request must build.'
|
|
Assert-True $first.Map.PlanningReady 'Applied source geometry must make the map ready.'
|
|
Assert-True ($first.Map.IsOccupiedWorld(1.0, 1.1)) 'TwoLeg must project detection-time local coordinates into world metres.'
|
|
$second = $factory.Create((New-Request $sources $false))
|
|
Assert-Equal 'Input' $second.CacheHit.ToString() 'Same snapshot must hit complete input cache.'
|
|
Assert-True ([object]::ReferenceEquals($first.Map, $second.Map)) 'Same snapshot must return the exact immutable map object.'
|
|
$manualVersionTwo = [Activator]::CreateInstance($manualType, @('manual', [long]2, $true, $obstacles))
|
|
$sourcesVersionTwo = [Array]::CreateInstance($sourceType, 2); $sourcesVersionTwo.SetValue($manualVersionTwo, 0); $sourcesVersionTwo.SetValue($twoLeg, 1)
|
|
$third = $factory.Create((New-Request $sourcesVersionTwo $false))
|
|
Assert-Equal 'Occupancy' $third.CacheHit.ToString() 'Version change with equal occupancy must reuse occupancy buffers.'
|
|
Assert-True ($third.Map.SnapshotId -gt $first.Map.SnapshotId) 'Occupancy reuse must still issue a fresh snapshot id.'
|
|
$emptySources = [Array]::CreateInstance($sourceType, 0)
|
|
$emptyBlocked = $factory.Create((New-Request $emptySources $false))
|
|
Assert-True (-not $emptyBlocked.Map.PlanningReady) 'Implicit empty map must be blocked.'
|
|
$emptyAllowed = $factory.Create((New-Request $emptySources $true))
|
|
Assert-True $emptyAllowed.Map.PlanningReady 'Explicit empty map must be accepted.'
|
|
$invalidManual = [Activator]::CreateInstance($manualType, @('invalid', [long]0, $true, $null))
|
|
$invalidSources = [Array]::CreateInstance($sourceType, 1); $invalidSources.SetValue($invalidManual, 0)
|
|
$invalid = $factory.Create((New-Request $invalidSources $false))
|
|
Assert-True (-not $invalid.Succeeded) 'Required invalid source must reject the full build.'
|
|
Write-Output 'Planning map factory checks passed.'
|