92 lines
9.7 KiB
PowerShell
92 lines
9.7 KiB
PowerShell
param([Parameter(Mandatory)][string]$EvidenceDir,[string]$FinalSource=(Join-Path $PSScriptRoot 'exercise-final.ps1'),[string]$PlaygroundSource=(Join-Path $PSScriptRoot 'exercise-playground.ps1'))
|
|
$ErrorActionPreference='Stop'
|
|
New-Item -ItemType Directory -Path $EvidenceDir -Force|Out-Null
|
|
$checks=[Collections.Generic.List[object]]::new()
|
|
function Probe([string]$Id,[bool]$Passed,$Actual){$checks.Add(@{id=$Id;passed=$Passed;actual=$Actual})}
|
|
function Check([string]$Id,[bool]$Passed,[string]$Actual){if(!$Passed){throw "$Id : $Actual"}}
|
|
try {
|
|
$ast=[Management.Automation.Language.Parser]::ParseFile($FinalSource,[ref]$null,[ref]$null)
|
|
$loop=$ast.Find({param($node)$node-is[Management.Automation.Language.ForEachStatementAst]-and$node.Variable.VariablePath.UserPath-eq'suite'},$true)
|
|
$suites=@(& ([scriptblock]::Create($loop.Condition.Extent.Text)))
|
|
Probe 'actual-final-includes-playground' (@($suites|Where-Object {$_.name-eq'PlaygroundInteraction'-and$_.script-eq'exercise-playground.ps1'-and$_.report-eq'android-playground-evidence.json'}).Count-eq1) $suites
|
|
$gate=@(Get-Content -LiteralPath $FinalSource|Where-Object {$_-match '^\s*Check "suite-\$\(\$suite.name\)-actual-exit-status-checks"'})[0]
|
|
if(!$gate){throw 'Missing actual Final child assertion'}
|
|
$record=@{sourceCommit='same-source';apkSha256='same-apk'};$suite=@{name='PlaygroundInteraction'}
|
|
foreach($kind in @('valid','child-exit','empty','failed-status','failed-check','pending','skipped','stale-source','changed-source-end','dirty','dirty-end','changed-apk')){
|
|
$actualExit=0;$child=[pscustomobject]@{status='passed';sourceCommit='same-source';sourceCommitEnd='same-source';sourceDirty=$false;sourceDirtyEnd=$false;apkSha256='same-apk';checks=@(@{id='evaluated';passed=$true});pending=@();skipped=@()}
|
|
switch($kind){'child-exit'{$actualExit=37}'empty'{$child.checks=@()}'failed-status'{$child.status='failed'}'failed-check'{$child.checks[0].passed=$false}'pending'{$child.pending=@('native-unobserved')}'skipped'{$child.skipped=@('native-unobserved')}'stale-source'{$child.sourceCommit='old-source'}'changed-source-end'{$child.sourceCommitEnd='changed'}'dirty'{$child.sourceDirty=$true}'dirty-end'{$child.sourceDirtyEnd=$true}'changed-apk'{$child.apkSha256='old-apk'}}
|
|
$accepted=$false;try{& ([scriptblock]::Create($gate));$accepted=$true}catch{}
|
|
Probe "actual-final-gate-$kind" ($accepted-eq($kind-eq'valid')) @{accepted=$accepted;actualExit=$actualExit;statement=$gate}
|
|
}
|
|
$controlRoot=$EvidenceDir;$sandbox=Join-Path $EvidenceDir 'actual-loop-sandbox';New-Item -ItemType Directory -Path $sandbox -Force|Out-Null
|
|
# Create() has no file-owned PSScriptRoot; substitute only the sandbox path binding.
|
|
$actualLoop=$loop.Extent.Text.Replace('$PSScriptRoot','$sandbox')
|
|
foreach($dispatchExit in @(0,37)){
|
|
$EvidenceDir=Join-Path $sandbox "child-$dispatchExit";New-Item -ItemType Directory -Path $EvidenceDir -Force|Out-Null
|
|
'{}'|Set-Content -LiteralPath (Join-Path $EvidenceDir 'androidbuild-evidence.json')
|
|
foreach($definition in $suites){
|
|
$stub='param([string]$ApkPath,[string]$EvidenceDir,[string]$Serial)' + "`n" + '$exitCode=' + $(if($definition.name-eq'PlaygroundInteraction'){$dispatchExit}else{0}) + "`n" + '$report=''' + $definition.report + "'`n" + @'
|
|
$checks=@(1..30|ForEach-Object {@{id="stage-$_-actual-success";passed=$true}})
|
|
@{status='passed';sourceCommit='same-source';sourceCommitEnd='same-source';sourceDirty=$false;sourceDirtyEnd=$false;apkSha256='same-apk';checks=$checks;commands=@();pending=@();skipped=@();actualArguments=@{apk=$ApkPath;serial=$Serial}}|ConvertTo-Json -Depth 8|Set-Content -LiteralPath (Join-Path $EvidenceDir $report)
|
|
exit $exitCode
|
|
'@
|
|
Set-Content -LiteralPath (Join-Path $sandbox $definition.script) -Value $stub
|
|
}
|
|
$ApkPath='sentinel-final.apk';$Serial='sentinel-device';$children=[Collections.Generic.List[object]]::new();$accepted=$false
|
|
$dispatchError=$null;try{& ([scriptblock]::Create($actualLoop));$accepted=$true}catch{$dispatchError=$_.Exception.Message}
|
|
$dispatched=@($children|Where-Object suite -eq 'PlaygroundInteraction')
|
|
Probe "actual-final-loop-dispatch-exit-$dispatchExit" ($accepted-eq($dispatchExit-eq0)-and$dispatched.Count-eq1-and$dispatched[0].exitCode-eq$dispatchExit-and$dispatched[0].report.actualArguments.apk-eq'sentinel-final.apk'-and$dispatched[0].report.actualArguments.serial-eq'sentinel-device') @{accepted=$accepted;actualChild=$dispatched;error=$dispatchError;scope='Actual Final loop with sandbox child scripts, never native game evidence'}
|
|
}
|
|
$EvidenceDir=$controlRoot
|
|
if(!(Test-Path -LiteralPath $PlaygroundSource)){Probe 'actual-playground-gesture-present' $false 'Missing suite'}else{
|
|
$gestureAst=[Management.Automation.Language.Parser]::ParseFile($PlaygroundSource,[ref]$null,[ref]$null)
|
|
$native=$gestureAst.Find({param($node)$node-is[Management.Automation.Language.FunctionDefinitionAst]-and$node.Name-eq'NativeTouch'},$true)
|
|
if(!$native){throw 'Missing actual NativeTouch function'}
|
|
. ([scriptblock]::Create($native.Extent.Text))
|
|
$commands=[Collections.Generic.List[object]]::new()
|
|
$helper=Join-Path $EvidenceDir 'touch-stub.ps1'
|
|
@'
|
|
param([int[]]$Contacts)
|
|
$Contacts|ConvertTo-Json|Set-Content -LiteralPath $env:PLAYGROUND_CONTROL_TOUCH_FILE
|
|
exit ([int]$env:PLAYGROUND_CONTROL_TOUCH_EXIT)
|
|
'@|Set-Content -LiteralPath $helper
|
|
foreach($exitCode in @(0,37)){
|
|
$env:PLAYGROUND_CONTROL_TOUCH_EXIT=[string]$exitCode;$env:PLAYGROUND_CONTROL_TOUCH_FILE=Join-Path $EvidenceDir "touch-observed-$exitCode.json";$rejected=$false;$errorText=$null
|
|
try{NativeTouch @(1200,500,71,1024,1300,500,72,1024)}catch{$rejected=$true;$errorText=$_.Exception.Message}
|
|
$observedContacts=Get-Content -LiteralPath $env:PLAYGROUND_CONTROL_TOUCH_FILE -Raw|ConvertFrom-Json
|
|
Probe "native-gesture-actual-child-exit-$exitCode" ($rejected-eq($exitCode-ne0)-and($observedContacts-join',')-eq'1200,500,71,1024,1300,500,72,1024'-and$commands[-1].exitCode-eq$exitCode) @{rejected=$rejected;contacts=$observedContacts;actualExit=$commands[-1].exitCode;error=$errorText}
|
|
}
|
|
foreach($name in @('Clone','SameBalls','SameMembership','InBounds','FixtureState','Slot','BlueFriends')){
|
|
$function=$gestureAst.Find({param($node)$node-is[Management.Automation.Language.FunctionDefinitionAst]-and$node.Name-eq$name},$true)
|
|
. ([scriptblock]::Create($function.Extent.Text))
|
|
}
|
|
function Same($First,$Second){return ($First|ConvertTo-Json -Depth 12 -Compress)-eq($Second|ConvertTo-Json -Depth 12 -Compress)}
|
|
$fixtures=@{stages=@(@{stage=1;min=@{x=-8;y=0;z=-6};max=@{x=8;y=6;z=6}},@{stage=3;initial=@{stageId=3}},@{stage=6;initial=@{stageId=6}})}
|
|
$original=@{schemaVersion=1;completedStageIds=@(1..30);seenTutorialIds=@('merge','split');puzzleStates=@();latestFreePlay=@{sourceStageId=30}}
|
|
$first=@{balls=@(@{id='whole-id-a';position=@{x=0;y=1;z=0}},@{id='whole-id-b';position=@{x=1;y=1;z=0}});groups=@(@{id='whole-group';ballIds=@('whole-id-a','whole-id-b')})}
|
|
foreach($kind in @('valid','changed-id','duplicate-id','changed-position','changed-membership','outside-boundary')){
|
|
$second=Clone $first
|
|
switch($kind){'changed-id'{$second.balls[0].id='w'}'duplicate-id'{$second.balls[0].id='whole-id-b'}'changed-position'{$second.balls[0].position.x=.1}'changed-membership'{$second.groups[0].ballIds=@('whole-id-a')}'outside-boundary'{$second.balls[0].position.x=7.6}}
|
|
$accepted=(SameBalls $first $second)-and(SameMembership $first $second)-and(InBounds $second 1)
|
|
Probe "actual-native-board-oracle-$kind" ($accepted-eq($kind-eq'valid')) @{accepted=$accepted}
|
|
}
|
|
$injected=FixtureState $first 'free' 1
|
|
Probe 'fixture-retains-original-completion-tutorial-schema' ((Same $injected.completedStageIds $original.completedStageIds)-and(Same $injected.seenTutorialIds $original.seenTutorialIds)-and$injected.schemaVersion-eq1-and($injected.puzzleStates.stageId-join',')-eq'3,6') $injected
|
|
Add-Type -AssemblyName System.Drawing
|
|
foreach($kind in @('two-blue-friends','sky-only','green-foliage')){
|
|
$bitmap=[Drawing.Bitmap]::new(2400,1080);$graphics=[Drawing.Graphics]::FromImage($bitmap)
|
|
try{
|
|
$graphics.Clear([Drawing.Color]::FromArgb(147,211,218))
|
|
$color=if($kind-eq'two-blue-friends'){[Drawing.Color]::FromArgb(30,130,190)}else{[Drawing.Color]::FromArgb(100,150,110)}
|
|
if($kind-ne'sky-only'){$brush=[Drawing.SolidBrush]::new($color);try{$graphics.FillEllipse($brush,1000,470,60,60);$graphics.FillEllipse($brush,1450,470,60,60)}finally{$brush.Dispose()}}
|
|
$path=Join-Path $EvidenceDir "$kind.png";$bitmap.Save($path,[Drawing.Imaging.ImageFormat]::Png)
|
|
}finally{$graphics.Dispose();$bitmap.Dispose()}
|
|
$friends=@(BlueFriends $path);$expectedCount=if($kind-eq'two-blue-friends'){2}else{0}
|
|
Probe "actual-live-steering-vision-$kind" ($friends.Count-eq$expectedCount) @{friends=$friends;expectedCount=$expectedCount;scope='Synthetic semantic control, not native screenshot verdict'}
|
|
}
|
|
}
|
|
}catch{Probe 'control-execution' $false $_.Exception.Message}
|
|
$passed=$checks.Count-gt0-and@($checks|Where-Object passed -ne $true).Count-eq0
|
|
@{status=if($passed){'passed'}else{'failed'};scope='Tool semantic controls only; no native game verdict';sourceCommit=(& git rev-parse HEAD);sourceDirty=[bool](& git status --porcelain);startedSource=$FinalSource;finishedUtc=[DateTime]::UtcNow.ToString('o');checks=$checks.ToArray()}|ConvertTo-Json -Depth 8|Set-Content -LiteralPath (Join-Path $EvidenceDir 'playground-controls.json') -Encoding utf8
|
|
if(!$passed){exit 1};exit 0
|