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.'