Complete Final native boundaries and full-speech evidence gates
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
param([Parameter(Mandatory)][string]$CapturedPath,[Parameter(Mandatory)][string]$MatchDir,[Parameter(Mandatory)][string]$Prefix,[Parameter(Mandatory)][string]$OutputPath)
|
||||
$ErrorActionPreference='Stop'
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.IO;
|
||||
public static class CountPcmResidual {
|
||||
public static double Rms(string file,double start,double end) {
|
||||
byte[] bytes=File.ReadAllBytes(file);int rate=BitConverter.ToInt32(bytes,24);
|
||||
if(BitConverter.ToInt16(bytes,20)!=1||BitConverter.ToInt16(bytes,22)!=1||BitConverter.ToInt16(bytes,34)!=16||System.Text.Encoding.ASCII.GetString(bytes,36,4)!="data")throw new Exception("Canonical actual monoPCM16 required");
|
||||
int a=Math.Max(0,(int)Math.Ceiling(start*rate)),b=Math.Min((bytes.Length-44)/2,(int)Math.Floor(end*rate));
|
||||
if(b<=a)return 0;double sum=0;for(int i=a;i<b;i++){double v=BitConverter.ToInt16(bytes,44+i*2);sum+=v*v;}return Math.Sqrt(sum/(b-a));
|
||||
}
|
||||
}
|
||||
'@
|
||||
$matches=@(1..10|ForEach-Object{Get-Content -LiteralPath (Join-Path $MatchDir ('{0}-count_{1:D2}-match.json'-f$Prefix,$_)) -Raw|ConvertFrom-Json})
|
||||
$intervals=@(for($i=0;$i-lt10;$i++){$start=$matches[$i].onsetSeconds+$matches[$i].referenceDurationSeconds+.015;$end=if($i-lt9){$matches[$i+1].onsetSeconds-.015}else{$matches[$i].capturedDurationSeconds};$rms=[CountPcmResidual]::Rms((Resolve-Path $CapturedPath).Path,$start,$end);@{afterCount=$i+1;start=$start;end=$end;rmsPcm=$rms;passed=$rms-le32}})
|
||||
$passed=$intervals.Count-eq10-and@($intervals|Where-Object passed -ne $true).Count-eq0
|
||||
@{passed=$passed;thresholdRmsPcm=32;excludedLeadBeforeFirstNumber='May include interrupted collection guide; all numeric gaps/tail checked';intervals=$intervals;capturedSha256=(Get-FileHash $CapturedPath).Hash}|ConvertTo-Json -Depth 5|Set-Content -LiteralPath $OutputPath
|
||||
if(!$passed){exit 1}
|
||||
exit 0
|
||||
@@ -0,0 +1,130 @@
|
||||
param([Parameter(Mandatory)][string]$ApkPath,[Parameter(Mandatory)][string]$EvidenceDir,[string]$Serial='emulator-5554')
|
||||
. (Join-Path $PSScriptRoot 'final-common.ps1') -ApkPath $ApkPath -EvidenceDir $EvidenceDir -Serial $Serial
|
||||
$result.milestone='FinalBoundary'
|
||||
function Touch([int[]]$Contacts) {
|
||||
& (Join-Path $PSScriptRoot 'send-composition-touch.ps1') -Contacts $Contacts
|
||||
$commands.Add(@{command=('Native EmulatorController.sendTouch contacts='+($Contacts-join','));exitCode=0;utc=[DateTime]::UtcNow.ToString('o')})
|
||||
}
|
||||
function ReturnOne([string]$Name) {
|
||||
for($attempt=0;$attempt-lt12;$attempt++) {
|
||||
$before=Save "$Name-before-$attempt";$board=$before.latestFreePlay;$group=$board.groups[0]
|
||||
$point=BallPoint $board $group.ballIds[0]
|
||||
TapBall "$Name-select-$attempt" $board $group.ballIds[0]
|
||||
Step "$Name-return-$attempt" @('shell','input','tap','2064','864')
|
||||
$after=Save "$Name-after-$attempt"
|
||||
if($after.latestFreePlay.balls.Count-lt$board.balls.Count){return @($before,$after)}
|
||||
Step "$Name-orbit-$attempt" @('shell','input','swipe','2200','410','2320','410','350')
|
||||
}
|
||||
throw 'Whole-group return did not confirm through real native selection'
|
||||
}
|
||||
try {
|
||||
$result.scope='Separate native boundaries after real30 completion; deliberate corrupt-file tests labelled, no fabricated completion or board injection'
|
||||
Check 'clean-source' (!$result.sourceDirty) $result.sourceCommit
|
||||
$build=Get-Content (Join-Path $EvidenceDir 'androidbuild-evidence.json') -Raw|ConvertFrom-Json
|
||||
Check 'build-provenance' ($build.status-eq'passed'-and$build.sourceCommit-eq$result.sourceCommit-and$build.apkSha256-eq$result.apkSha256-and!$build.sourceDirty-and!$build.sourceDirtyEnd) $result.apkSha256
|
||||
Step 'boundary-free30-start' @() 0;$original=Save 'boundary-real30-state'
|
||||
Check 'boundary-starts-real-all30-completed' ($original.completedStageIds.Count-eq30-and$original.latestFreePlay.sourceStageId-eq30-and$original.latestFreePlay.balls.Count-eq10) 'Consumes actual progression state, no fixture'
|
||||
Step 'free10-refuse-eleventh' @('shell','input','tap','2064','756')
|
||||
Check 'free10-eleventh-refused-no-mutation' (Same $original (Save 'free10-after-eleventh')) '10 balls exact save unchanged'
|
||||
$returned=ReturnOne 'free30-whole5-return';$before=$returned[0];$after=$returned[1]
|
||||
Check 'return-exact-entire5-group' ($before.latestFreePlay.balls.Count-eq10-and$after.latestFreePlay.balls.Count-eq5-and$after.latestFreePlay.groups.Count-eq1-and$after.latestFreePlay.groups[0].ballIds.Count-eq5) 'One selected5 friend returned, other5 remains'
|
||||
for($spawn=0;$spawn-lt5;$spawn++){Step "free-refill-$spawn" @('shell','input','tap','2064','756')}
|
||||
$ten=Save 'free-refilled10';Check 'free-supply-refills-up-to10' ($ten.latestFreePlay.balls.Count-eq10-and@($ten.latestFreePlay.balls.id|Sort-Object -Unique).Count-eq10) 'Five native supply buttons add five unique singleton units'
|
||||
for($returnedCount=0;$returnedCount-lt12;$returnedCount++){$state=Save "free-empty-step-$returnedCount";if($state.latestFreePlay.balls.Count-eq0){break};ReturnOne "free-empty-return-$returnedCount"|Out-Null}
|
||||
$empty=Save 'free-empty';Check 'free-empty-allowed' ($empty.latestFreePlay.balls.Count-eq0-and$empty.latestFreePlay.groups.Count-eq0) 'All whole groups natively returned, empty state accepted'
|
||||
Step 'free-empty-refill-one' @('shell','input','tap','2064','756');$one=Save 'free-empty-refilled'
|
||||
Check 'free-empty-refills-one' ($one.latestFreePlay.balls.Count-eq1-and$one.latestFreePlay.groups.Count-eq1-and(Same $one.completedStageIds $original.completedStageIds)-and(Same $one.puzzleStates $original.puzzleStates)) 'Refill changes only latest Free board, all records/puzzles unchanged'
|
||||
# Genuine two-contact zoom, followed by out-of-window release cancellation of a friend drag.
|
||||
Touch @(30,400,999,100);Touch @(30,400,999,0);Start-Sleep -Milliseconds 100
|
||||
Touch @(1100,500,101,100,1400,500,102,100);Start-Sleep -Milliseconds 150
|
||||
Touch @(900,500,101,100,1600,500,102,100);Start-Sleep -Milliseconds 150
|
||||
Touch @(900,500,101,0,1600,500,102,0);Step 'free-native-pinch'
|
||||
$pinch=Save 'free-pinch-state'
|
||||
Check 'native-pinch-zoom-only' ((Same $one.latestFreePlay.balls $pinch.latestFreePlay.balls)-and(Same $one.latestFreePlay.groups $pinch.latestFreePlay.groups)-and$pinch.latestFreePlay.camera.distance-lt$one.latestFreePlay.camera.distance) 'Two physical contacts change zoom, no ball or group mutation'
|
||||
Step 'free-pinch-home' @('shell','input','tap','312','108');SelectStage 'boundary13' 13
|
||||
$thirteen=Save 'boundary13-initial';$board=@($thirteen.puzzleStates|Where-Object stageId -eq13)[0];$point=BallPoint $board $board.groups[0].ballIds[0]
|
||||
TapBall 'boundary13-choose' $board $board.groups[0].ballIds[0];Step 'boundary13-split-mode' @('shell','input','tap','312','832')
|
||||
$confirmed=Save 'boundary13-split-confirmed'
|
||||
Step 'boundary13-zero-selected-drag' @('shell','input','swipe',"$($point[0])","$($point[1])","$($point[0]-190)","$($point[1])",'500')
|
||||
Check 'split-zero-selection-unchanged' (Same $confirmed (Save 'boundary13-zero-after')) 'Invalid zero selection never changes confirmed state'
|
||||
foreach($id in $board.groups[0].ballIds){TapBall "boundary13-all-$id" $board $id}
|
||||
Step 'boundary13-all-selected-drag' @('shell','input','swipe',"$($point[0])","$($point[1])","$($point[0]-190)","$($point[1])",'500')
|
||||
Check 'split-all-selection-unchanged' (Same $confirmed (Save 'boundary13-all-after')) 'Invalid whole friend selection never changes confirmed state'
|
||||
Step 'boundary13-cancel-all' @('shell','input','tap','312','832');TapBall 'boundary13-select-again' $board $board.groups[0].ballIds[0];Step 'boundary13-split-again' @('shell','input','tap','312','832');TapBall 'boundary13-one-selected' $board $board.groups[0].ballIds[0]
|
||||
Step 'boundary13-offscreen-release' @('shell','input','swipe',"$($point[0])","$($point[1])",'2500',"$($point[1])",'500')
|
||||
Check 'split-offscreen-cancel-unchanged' (Same $confirmed (Save 'boundary13-offscreen-after')) 'Native out-of-window release cancels selection and restores last confirmed board'
|
||||
TapBall 'boundary13-disconnected-choose' $board $board.groups[0].ballIds[0];Step 'boundary13-disconnected-mode' @('shell','input','tap','312','832')
|
||||
foreach($id in @($board.groups[0].ballIds[0],$board.groups[0].ballIds[-1])){TapBall "boundary13-disconnected-$id" $board $id}
|
||||
Step 'boundary13-disconnected-split' @('shell','input','swipe',"$($point[0])","$($point[1])","$($point[0])","$($point[1]-160)",'500')
|
||||
$split=Save 'boundary13-disconnected-success'
|
||||
Check 'disconnected-two-exact3-plus2' ($split.latestFreePlay.sourceStageId-eq13-and(($split.latestFreePlay.groups|ForEach-Object {$_.ballIds.Count}|Sort-Object)-join',')-eq'2,3'-and(Same $split.completedStageIds $original.completedStageIds)) 'Native first and last of5 selected; splits into exact2 and3 without changing old completion'
|
||||
Step 'native13-free-rotated-numerals' @('shell','input','swipe','2200','410','2320','445','500');Save 'native13-free-rotated'|Out-Null
|
||||
# Deliberate external corruption is a separate resilience case, after all native progression records.
|
||||
Adb @('shell','input','keyevent','3')|Out-Null;Start-Sleep -Milliseconds 700;Adb @('shell','am','force-stop',$package)|Out-Null
|
||||
Adb @('pull',($remote+'.bak'),(Join-Path $EvidenceDir 'valid-backup-before-damage.json'))|Out-Null
|
||||
$backup=Get-Content (Join-Path $EvidenceDir 'valid-backup-before-damage.json') -Raw|ConvertFrom-Json
|
||||
$damage=Join-Path $EvidenceDir 'damaged-primary.txt';[IO.File]::WriteAllText($damage,'T8-final-deliberate-primary-damage')
|
||||
Adb @('push',$damage,$remote)|Out-Null
|
||||
Step 'valid-backup-recovery-home' @('shell','am','start','-W','-n',$activity) 6500
|
||||
Check 'corrupt-original-retained-on-load' ((Adb @('shell','cat',$remote))-eq'T8-final-deliberate-primary-damage') 'Whole corrupt original remains before next save'
|
||||
Step 'valid-backup-free-restore' @('shell','input','tap','1200','810')
|
||||
Check 'valid-backup-whole-state-restored' (Same $backup (Save 'backup-restored-state')) 'No partial load; entire valid backup selected'
|
||||
Check 'corrupt-original-forensics-preserved' ((Adb @('shell','cat',($remote+'.damaged-*')))-match'T8-final-deliberate-primary-damage') 'New save retains damaged original bytes in forensic file'
|
||||
Adb @('shell','am','force-stop',$package)|Out-Null
|
||||
[IO.File]::WriteAllText($damage,'T8-final-damaged-primary');Adb @('push',$damage,$remote)|Out-Null
|
||||
$bad=Join-Path $EvidenceDir 'damaged-backup.txt';[IO.File]::WriteAllText($bad,'T8-final-damaged-backup');Adb @('push',$bad,($remote+'.bak'))|Out-Null
|
||||
Step 'both-damaged-home' @('shell','am','start','-W','-n',$activity) 6500
|
||||
Check 'both-corrupt-originals-preserved' ((Adb @('shell','cat',$remote))-eq'T8-final-damaged-primary'-and(Adb @('shell','cat',($remote+'.bak')))-eq'T8-final-damaged-backup') 'Both failed originals remain before new save'
|
||||
Step 'both-damaged-new-selection' @('shell','input','tap','1200','616');$initial=Save 'both-damaged-initial'
|
||||
Check 'both-damaged-valid-initial' ($initial.completedStageIds.Count-eq0-and$initial.puzzleStates.Count-eq0-and$null-eq$initial.latestFreePlay) 'No valid backup starts normal initial state'
|
||||
$result.sourceCommitEnd=(& git rev-parse HEAD);$result.sourceDirtyEnd=[bool](& git status --porcelain);Check 'unchanged-source-end' (!$result.sourceDirtyEnd-and$result.sourceCommitEnd-eq$result.sourceCommit) $result.sourceCommitEnd
|
||||
$result.status='passed'
|
||||
}catch{$result.error=$_.Exception.Message;Write-Host $result.error -ForegroundColor Red}
|
||||
finally {
|
||||
foreach ($recorder in $recorders) {
|
||||
if (!$recorder.process.HasExited -and !$recorder.process.WaitForExit(($recorder.seconds+30)*1000)) {
|
||||
$result.status='failed'; $result.recorderError='Recorder exceeded completion bound; own native process stopped before restoration'
|
||||
$ready=Get-Content -LiteralPath (Join-Path $EvidenceDir "$($recorder.name).wav.ready.json") -Raw | ConvertFrom-Json
|
||||
$native=Get-Process -Id $ready.recorderPid -ErrorAction SilentlyContinue
|
||||
if ($native) { $native.Kill(); $native.WaitForExit() }
|
||||
$recorder.process.WaitForExit(10000) | Out-Null
|
||||
if (!$recorder.process.HasExited) { $recorder.process.Kill(); $recorder.process.WaitForExit() }
|
||||
}
|
||||
$readyPath=Join-Path $EvidenceDir "$($recorder.name).wav.ready.json"
|
||||
if (Test-Path -LiteralPath $readyPath) {
|
||||
$ready=Get-Content -LiteralPath $readyPath -Raw | ConvertFrom-Json
|
||||
$native=Get-Process -Id $ready.recorderPid -ErrorAction SilentlyContinue
|
||||
if ($native) { $native.WaitForExit() }
|
||||
$commands.Add(@{command='recorder termination boundary';recorderPid=$ready.recorderPid;wrapperPid=$recorder.process.Id;nativeExited=$true;wrapperExited=$recorder.process.HasExited;utc=[DateTime]::UtcNow.ToString('o')})
|
||||
}
|
||||
}
|
||||
if ($musicBefore) {
|
||||
try {
|
||||
Adb @('shell','cmd','audio','set-volume','3',"$($musicBefore['2'])") | Out-Null
|
||||
Adb @('shell','cmd','audio','set-device-volume','3',"$($musicBefore['8000'])",'32768') | Out-Null
|
||||
Adb @('shell','cmd','audio','set-device-volume','3',"$($musicBefore['40000000'])",'1073741824') | Out-Null
|
||||
$audioAfter=Adb @('shell','dumpsys','audio')
|
||||
$audioAfter | Set-Content -LiteralPath (Join-Path $EvidenceDir 'music-after.txt') -Encoding utf8
|
||||
$current=[regex]::Match($audioAfter,'(?s)- STREAM_MUSIC:.*?Current: ([^\r\n]+)').Groups[1].Value
|
||||
Check 'music-routes-restored' ($current -match "2 \(speaker\): $($musicBefore['2'])(,|$)" -and $current -match "8000 \(remote_submix\): $($musicBefore['8000'])(,|$)" -and $current -match "40000000 \(default\): $($musicBefore['40000000'])(,|$)") $current
|
||||
} catch { $result.status='failed'; $result.volumeRestoreError=$_.Exception.Message }
|
||||
}
|
||||
if ($networkBefore) {
|
||||
try {
|
||||
Adb @('shell','cmd','connectivity','airplane-mode',$(if ($networkBefore.airplane -eq '1') {'enable'} else {'disable'})) | Out-Null
|
||||
Adb @('shell','svc','wifi',$(if ($networkBefore.wifi -eq '1') {'enable'} else {'disable'})) | Out-Null
|
||||
Adb @('shell','svc','data',$(if ($networkBefore.data -eq '1') {'enable'} else {'disable'})) | Out-Null
|
||||
for ($attempt=0; $attempt -lt 30; $attempt++) {
|
||||
$result.networkAfter=@{airplane=(Adb @('shell','settings','get','global','airplane_mode_on'));wifi=(Adb @('shell','settings','get','global','wifi_on'));data=(Adb @('shell','settings','get','global','mobile_data'))}
|
||||
if (Same $networkBefore $result.networkAfter) { break }; Start-Sleep -Milliseconds 500
|
||||
}
|
||||
Check 'network-restored' (Same $networkBefore $result.networkAfter) ($result.networkAfter | ConvertTo-Json -Compress)
|
||||
} catch { $result.status='failed'; $result.restoreError=$_.Exception.Message }
|
||||
}
|
||||
|
||||
$result.checks=$checks.ToArray(); $result.commands=$commands.ToArray(); $result.finishedUtc=[DateTime]::UtcNow.ToString('o')
|
||||
$result | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $EvidenceDir 'android-final-boundary-evidence.json') -Encoding utf8
|
||||
}
|
||||
if ($result.status -ne 'passed') { exit 1 }
|
||||
Write-Host "Final native boundary checks passed: $($checks.Count) checks."
|
||||
exit 0
|
||||
@@ -1,138 +1,5 @@
|
||||
param([Parameter(Mandatory)][string]$ApkPath,[Parameter(Mandatory)][string]$EvidenceDir,[string]$Serial='emulator-5554')
|
||||
$ErrorActionPreference='Stop'
|
||||
$EvidenceDir=(Resolve-Path -LiteralPath $EvidenceDir).Path
|
||||
$adb=Join-Path $env:LOCALAPPDATA 'Android/Sdk/platform-tools/adb.exe'
|
||||
$package='com.magnetismheroes.game'; $activity="$package/com.unity3d.player.UnityPlayerGameActivity"
|
||||
$remote="/sdcard/Android/data/$package/files/progress.json"
|
||||
$checks=[Collections.Generic.List[object]]::new(); $commands=[Collections.Generic.List[object]]::new(); $recorders=[Collections.Generic.List[object]]::new()
|
||||
$result=[ordered]@{milestone='Final';sourceCommit=(& git rev-parse HEAD);sourceDirty=[bool](& git status --porcelain);serial=$Serial;apkSha256=(Get-FileHash -LiteralPath $ApkPath -Algorithm SHA256).Hash;status='failed'}
|
||||
$result.rendererProjection=@{fieldOfView=62;fixedWorldPivot=@(0,.65,0);rawCamera='Saved Board.camera JSON remains canonical';viewport='Landscape wm size max/min; remaining UI and physical touch helper assume sole 2400x1080 AVD'}
|
||||
$networkBefore=$null; $musicBefore=$null
|
||||
function Adb([string[]]$Arguments) {
|
||||
$started=[DateTime]::UtcNow.ToString('o'); $output=& $adb -s $Serial @Arguments 2>&1; $code=$LASTEXITCODE
|
||||
$commands.Add(@{startedUtc=$started;finishedUtc=[DateTime]::UtcNow.ToString('o');command=('adb -s '+$Serial+' '+($Arguments -join ' '));exitCode=$code;output=($output -join "`n")})
|
||||
if ($code -ne 0) { throw "ADB exit $code : $($Arguments -join ' ')" }; return ($output -join "`n").Trim()
|
||||
}
|
||||
function Check([string]$Id,[bool]$Passed,[string]$Actual) {
|
||||
$checks.Add(@{id=$Id;passed=$Passed;actual=$Actual}); if (!$Passed) { throw "Content check failed: $Id ($Actual)" }
|
||||
}
|
||||
function Step([string]$Name,[string[]]$Arguments=@(),[int]$Delay=500) {
|
||||
& (Join-Path $PSScriptRoot 'observe.ps1') -Step $Name -AdbArguments $Arguments -Serial $Serial -EvidenceDir $EvidenceDir -DelayMilliseconds $Delay
|
||||
}
|
||||
function Save([string]$Name) { Adb @('pull',$remote,(Join-Path $EvidenceDir "$Name.json")) | Out-Null; return Get-Content -LiteralPath (Join-Path $EvidenceDir "$Name.json") -Raw | ConvertFrom-Json }
|
||||
function Same($First,$Second) { return ($First | ConvertTo-Json -Depth 12 -Compress) -eq ($Second | ConvertTo-Json -Depth 12 -Compress) }
|
||||
function BallPoint($Board,[string]$Id) {
|
||||
$ball=@($Board.balls | Where-Object id -eq $Id)[0]; $camera=$Board.camera
|
||||
$rotation=[Numerics.Quaternion]::new($camera.rotation.x,$camera.rotation.y,$camera.rotation.z,$camera.rotation.w)
|
||||
$origin=[Numerics.Vector3]::new($camera.target.x,($camera.target.y+.65),$camera.target.z)+[Numerics.Vector3]::Transform([Numerics.Vector3]::new(0,0,-$camera.distance),$rotation)
|
||||
$local=[Numerics.Vector3]::Transform([Numerics.Vector3]::new($ball.position.x,$ball.position.y,$ball.position.z)-$origin,[Numerics.Quaternion]::Inverse($rotation))
|
||||
$size=Adb @('shell','wm','size'); if ($size -notmatch '(\d+)x(\d+)') { throw "Unknown device resolution: $size" }
|
||||
$width=[Math]::Max([int]$Matches[1],[int]$Matches[2]); $height=[Math]::Min([int]$Matches[1],[int]$Matches[2])
|
||||
$scale=($height/2)/([Math]::Tan(31*[Math]::PI/180)*$local.Z)
|
||||
return @([int][Math]::Round(($width/2)+$local.X*$scale),[int][Math]::Round(($height/2)-$local.Y*$scale))
|
||||
}
|
||||
function TapBall([string]$Name,$Board,[string]$Id) { $point=BallPoint $Board $Id; Step $Name @('shell','input','tap',"$($point[0])","$($point[1])") }
|
||||
function MergeStage([string]$Name,[int]$Id) {
|
||||
$save=Save "$Name-before"; $board=@($save.puzzleStates | Where-Object stageId -eq $Id)[0]
|
||||
$start=BallPoint $board $board.groups[0].ballIds[0]; $end=BallPoint $board $board.groups[1].ballIds[0]
|
||||
Step $Name @('shell','input','swipe',"$($start[0])","$($start[1])","$($end[0]-45)","$($end[1])",'650')
|
||||
}
|
||||
function StagePoint([int]$Id) { $index=$Id-1; return @([int][Math]::Round((.12+($index%6)*.152)*2400),[int][Math]::Round((1-(.76-[Math]::Floor($index/6)*.123))*1080)) }
|
||||
function SelectStage([string]$Name,[int]$Id) {
|
||||
Step "$Name-selection" @('shell','input','tap','1200','616')
|
||||
$point=StagePoint $Id; Step "$Name-stage" @('shell','input','tap',"$($point[0])","$($point[1])")
|
||||
}
|
||||
function StartHome([string]$Name) {
|
||||
Adb @('shell','input','keyevent','224') | Out-Null
|
||||
Adb @('shell','am','start','-W','-n',$activity) | Out-Null
|
||||
$observations=[Collections.Generic.List[object]]::new()
|
||||
$ready=$false; $pauseRequested=$false
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
for($attempt=0;$attempt-lt30;$attempt++) {
|
||||
$pidText=Adb @('shell','pidof',$package)
|
||||
$saveText=Adb @('shell',('if test -s '+$remote+'; then cat '+$remote+'; fi'))
|
||||
$validSave=$false
|
||||
try { $state=$saveText|ConvertFrom-Json -ErrorAction Stop; $validSave=$state.schemaVersion-eq1 -and $null-ne$state.completedStageIds -and $null-ne$state.puzzleStates } catch { }
|
||||
Step "$Name-ready-$attempt" @() 1000
|
||||
$png=Join-Path $EvidenceDir "$Name-ready-$attempt.png"
|
||||
$image=[Drawing.Bitmap]::new($png)
|
||||
$sky=0;$gold=0;$dark=0
|
||||
try {
|
||||
for($y=1;$y-lt20;$y++){for($x=1;$x-lt30;$x++) {
|
||||
$pixel=$image.GetPixel([int]($image.Width*$x/30),[int]($image.Height*$y/20))
|
||||
if($pixel.R-ge150-and$pixel.G-ge180-and$pixel.B-ge210){$sky++}
|
||||
if($pixel.R-ge200-and$pixel.G-ge130-and$pixel.B-le120){$gold++}
|
||||
if($y-le6-and$pixel.R-lt110-and$pixel.G-lt110-and$pixel.B-lt110){$dark++}
|
||||
}}
|
||||
$dark=0
|
||||
for ($y=[int]($image.Height*.17); $y -lt [int]($image.Height*.28); $y+=4) {
|
||||
for ($x=[int]($image.Width*.39); $x -lt [int]($image.Width*.61); $x+=4) {
|
||||
$pixel=$image.GetPixel($x,$y)
|
||||
if ($pixel.R -lt 110 -and $pixel.G -lt 110 -and $pixel.B -lt 110) { $dark++ }
|
||||
}
|
||||
}
|
||||
$homeButton=0; foreach($px in @(.36,.64)){foreach($py in @(.53,.61)){$p=$image.GetPixel([int]($image.Width*$px),[int]($image.Height*$py));if($p.R-ge200-and$p.G-ge130-and$p.B-le120){$homeButton++}}}
|
||||
$ui=$image.Width-gt$image.Height-and$sky-ge200-and$gold-ge10-and$dark-ge20-and$homeButton-eq4
|
||||
$observations.Add(@{attempt=$attempt;utc=[DateTime]::UtcNow.ToString('o');pid=$pidText;validAppSave=$validSave;landscape=($image.Width-gt$image.Height);skySamples=$sky;goldButtonSamples=$gold;titleDarkSamples=$dark;titleBoundsNormalized=@(.39,.17,.61,.28);titleStridePixels=4;homeButtonSamples=$homeButton;readyUi=$ui;png=$png})
|
||||
} finally {$image.Dispose()}
|
||||
if ($pidText -match '^\d+' -and $ui -and !$validSave -and !$pauseRequested) {
|
||||
$observations[$observations.Count-1] | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-first-home-without-save.json") -Encoding utf8
|
||||
Check "$Name-first-home-ui" $true 'Actual first Home title/button/PID observed before production pause creates save'
|
||||
Adb @('shell','input','keyevent','3') | Out-Null
|
||||
Start-Sleep -Milliseconds 500
|
||||
Adb @('shell','am','start','-W','-n',$activity) | Out-Null
|
||||
$pauseRequested=$true
|
||||
}
|
||||
if($pidText-match'^\d+'-and$validSave-and$ui){$ready=$true;$saveText|Set-Content (Join-Path $EvidenceDir "$Name-ready-save.json") -Encoding utf8;break}
|
||||
}
|
||||
$observations.ToArray()|ConvertTo-Json -Depth 6|Set-Content (Join-Path $EvidenceDir "$Name-readiness.json") -Encoding utf8
|
||||
Check "$Name-home-ready" $ready 'Actual app PID, landscape home colors/button/title and app save JSON; bounded30 observations, black/Unity splash excluded, PNG retained for direct review'
|
||||
Step $Name @() 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Restart([string]$Name) {
|
||||
Adb @('shell','input','keyevent','3') | Out-Null; Adb @('shell','am','force-stop',$package) | Out-Null
|
||||
StartHome "$Name-home"
|
||||
}
|
||||
function StartAudio([string]$Name,[int]$Seconds) {
|
||||
$readyPath=Join-Path $EvidenceDir "$Name.wav.ready.json"
|
||||
if (Test-Path -LiteralPath $readyPath) { Remove-Item -LiteralPath $readyPath }
|
||||
$args=@('-NoProfile','-File',(Join-Path $PSScriptRoot 'record-native-audio.ps1'),'-OutputPath',(Join-Path $EvidenceDir "$Name.wav"),'-Seconds',"$Seconds",'-Serial',$Serial)
|
||||
$process=Start-Process pwsh -ArgumentList ($args | ForEach-Object {'"'+$_+'"'}) -WindowStyle Hidden -RedirectStandardOutput (Join-Path $EvidenceDir "$Name.stdout") -RedirectStandardError (Join-Path $EvidenceDir "$Name.stderr") -PassThru
|
||||
$recorders.Add(@{name=$Name;process=$process;seconds=$Seconds;startedUtc=[DateTime]::UtcNow})
|
||||
$readyPath=Join-Path $EvidenceDir "$Name.wav.ready.json"
|
||||
$ready=$false
|
||||
for ($attempt=0; $attempt -lt 120; $attempt++) {
|
||||
if ($process.HasExited) { break }
|
||||
if (Test-Path -LiteralPath $readyPath) { $ready=$true; break }
|
||||
Start-Sleep -Milliseconds 100
|
||||
}
|
||||
Check "audio-ready-$Name" $ready 'Recorder reported actual Recording started before interaction'
|
||||
for ($attempt=0; $attempt -lt 20; $attempt++) {
|
||||
Adb @('shell','cmd','audio','set-volume','3','15') | Out-Null
|
||||
$route=Adb @('shell','dumpsys','audio')
|
||||
$route | Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-active-route-$attempt.txt") -Encoding utf8
|
||||
$current=[regex]::Match($route,'(?s)- STREAM_MUSIC:.*?Current: ([^\r\n]+)').Groups[1].Value
|
||||
if ($current -match '8000 \(remote_submix\): 15') { break }
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
$route | Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-active-route.txt") -Encoding utf8
|
||||
Check "audio-active-route-$Name" ($current -match '8000 \(remote_submix\): 15') $current
|
||||
}
|
||||
function CompareAudio([string]$Name,[string[]]$Ids) {
|
||||
$recorder=@($recorders | Where-Object name -eq $Name)[0].process
|
||||
Check "audio-finished-$Name" ($recorder.WaitForExit((@($recorders | Where-Object name -eq $Name)[0].seconds+30)*1000)) 'PCM capture finished'; $recorder.Refresh(); Check "audio-exit-$Name" ($recorder.ExitCode -eq 0) "$($recorder.ExitCode)"
|
||||
foreach ($id in $Ids) {
|
||||
$reference=Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) "Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/$id.wav"
|
||||
$comparison=Join-Path $EvidenceDir "$Name-$id-match.json"
|
||||
& pwsh -NoProfile -File (Join-Path $PSScriptRoot 'compare-audio.ps1') -ReferencePath $reference -CapturedPath (Join-Path $EvidenceDir "$Name.wav") -OutputPath $comparison -RequireFullSpeech
|
||||
$exit=$LASTEXITCODE; $match=Get-Content -LiteralPath $comparison -Raw | ConvertFrom-Json
|
||||
Check "pcm-$Name-$id" ($exit -eq 0 -and $match.passed -and $match.normalizedCorrelation -ge .95 -and $match.rmsPcm -gt 32) ($match | ConvertTo-Json -Compress)
|
||||
}
|
||||
}
|
||||
|
||||
. (Join-Path $PSScriptRoot 'final-common.ps1') -ApkPath $ApkPath -EvidenceDir $EvidenceDir -Serial $Serial
|
||||
# This suite never pushes a save or injects completed stages. Every checkpoint is read from the APK.
|
||||
function Board($State,[int]$Stage) { return @($State.puzzleStates | Where-Object stageId -eq $Stage)[0] }
|
||||
function Counts($Board) { return (@($Board.groups | ForEach-Object {$_.ballIds.Count} | Sort-Object) -join ',') }
|
||||
@@ -257,7 +124,9 @@ try {
|
||||
Check 'two-unfinished-plus-free' ($three.puzzleStates.Count-eq2-and$three.latestFreePlay.sourceStageId-eq1-and(Same $partialBoard (Board $three 2))-and$three.completedStageIds.Count-eq1) 'Replay1, actual partially counted2, and independent Free1 coexist without fixture'
|
||||
Restart 'three-board-restart';Step 'free1-restored' @('shell','input','tap','1200','810')
|
||||
Check 'two-puzzles-free-whole-restored' (Same $three (Save 'two-unfinished-and-free-after')) 'Pause force-stop startup Home then Free restore exact full save'
|
||||
Step 'free1-home' @('shell','input','tap','312','108');SelectStage 'partial2-restored' 2
|
||||
Step 'free1-home' @('shell','input','tap','312','108');SelectStage 'replay1-restored' 1
|
||||
Check 'replay1-independent-ui-restored' (Same (Board $three 1) (Board (Save 'replay1-restored-state') 1)) 'Second unfinished board independently entered through actual UI with exact altered positions'
|
||||
Step 'replay1-restored-home' @('shell','input','tap','312','108');SelectStage 'partial2-restored' 2
|
||||
Check 'partial2-exact-restored' (Same $partialBoard (Board (Save 'partial2-restored') 2)) 'OriginalIDs positions membership collected counted camera exact'
|
||||
}
|
||||
if($stage-eq20) {
|
||||
@@ -273,6 +142,9 @@ try {
|
||||
CompareAudio 'audio-ten-rapid' @(1..10|ForEach-Object{'count_{0:D2}'-f$_})
|
||||
$matches=@(1..10|ForEach-Object{Get-Content (Join-Path $EvidenceDir ('audio-ten-rapid-count_{0:D2}-match.json'-f$_)) -Raw|ConvertFrom-Json})
|
||||
for($i=1;$i-lt10;$i++){Check "rapid-fifo-$i" ($matches[$i-1].onsetSeconds+$matches[$i-1].referenceDurationSeconds-le$matches[$i].onsetSeconds+.015) 'Entire numeric clip completes before next onset'}
|
||||
Check 'rapid-last-complete-before-record-end' ($matches[-1].onsetSeconds+$matches[-1].referenceDurationSeconds-le$matches[-1].capturedDurationSeconds) 'Entire tenth numeric clip retained after same-board Free transition'
|
||||
& pwsh -NoProfile -File (Join-Path $PSScriptRoot 'check-count-audio-residual.ps1') -CapturedPath (Join-Path $EvidenceDir 'audio-ten-rapid.wav') -MatchDir $EvidenceDir -Prefix 'audio-ten-rapid' -OutputPath (Join-Path $EvidenceDir 'rapid-no-extra-speech.json')
|
||||
Check 'rapid-no-extra-or-duplicate-speech' ($LASTEXITCODE-eq0) 'All gaps and trailing PCM outside ten complete matched words are below PCM32 RMS'
|
||||
$result.speechMatches=$matches
|
||||
} else {CountRemaining $stage|Out-Null}
|
||||
} else {
|
||||
|
||||
@@ -28,7 +28,7 @@ try {
|
||||
Check "apk-notice-exact-bytes-$name" ($sha-eq(Get-FileHash -LiteralPath (Join-Path $projectRoot ('Assets/StreamingAssets/Notices/'+$name))).Hash) $sha
|
||||
}
|
||||
}finally{$zip.Dispose()}
|
||||
foreach($suite in @(@{name='Fresh30';script='exercise-final-progression.ps1';report='android-final-progression-evidence.json'},@{name='ContentBoundary';script='exercise-content.ps1';report='android-content-evidence.json'})){
|
||||
foreach($suite in @(@{name='Fresh30';script='exercise-final-progression.ps1';report='android-final-progression-evidence.json'},@{name='FinalBoundary';script='exercise-final-boundaries.ps1';report='android-final-boundary-evidence.json'},@{name='ContentBoundary';script='exercise-content.ps1';report='android-content-evidence.json'})){
|
||||
$childDir=Join-Path $EvidenceDir $suite.name;New-Item -ItemType Directory -Path $childDir -Force|Out-Null
|
||||
Copy-Item -LiteralPath (Join-Path $EvidenceDir 'androidbuild-evidence.json') -Destination (Join-Path $childDir 'androidbuild-evidence.json')
|
||||
$log=Join-Path $childDir 'outer.log';$command="pwsh -NoProfile -File tools/android/$($suite.script) -ApkPath $ApkPath -EvidenceDir $childDir -Serial $Serial"
|
||||
@@ -41,6 +41,8 @@ try {
|
||||
Check 'fresh30-native-commands-no-fixture-write' (@($child.commands|Where-Object {$_.command-match'\bpush\b'}).Count-eq0) 'Native progression never pushes save data'
|
||||
}
|
||||
}
|
||||
& pwsh -NoProfile -File (Join-Path $PSScriptRoot 'test-native-audio.ps1') -ReferencePath (Join-Path $projectRoot 'Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_collect.wav') -CapturedPath (Join-Path $EvidenceDir 'ContentBoundary/audio-one-offline.wav') -WrongReferencePath (Join-Path $projectRoot 'Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/guide_split.wav') -EvidenceDir (Join-Path $EvidenceDir 'pcm-negative-controls')
|
||||
Check 'pcm-full-speech-negative-controls' ($LASTEXITCODE-eq0) 'Actual fullspeech positive and clipped front/back/middle/duplicate/noise/wrongclip negatives'
|
||||
$record.sourceCommitEnd=(& git rev-parse HEAD);$record.sourceDirtyEnd=[bool](& git status --porcelain)
|
||||
Check 'unchanged-source-end' (!$record.sourceDirtyEnd-and$record.sourceCommitEnd-eq$record.sourceCommit) $record.sourceCommitEnd
|
||||
$record.status='passed'
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
param([Parameter(Mandatory)][string]$ApkPath,[Parameter(Mandatory)][string]$EvidenceDir,[string]$Serial='emulator-5554')
|
||||
$ErrorActionPreference='Stop'
|
||||
$EvidenceDir=(Resolve-Path -LiteralPath $EvidenceDir).Path
|
||||
$projectRoot=Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$adb=Join-Path $env:LOCALAPPDATA 'Android/Sdk/platform-tools/adb.exe'
|
||||
$package='com.magnetismheroes.game'; $activity="$package/com.unity3d.player.UnityPlayerGameActivity"
|
||||
$remote="/sdcard/Android/data/$package/files/progress.json"
|
||||
$checks=[Collections.Generic.List[object]]::new(); $commands=[Collections.Generic.List[object]]::new(); $recorders=[Collections.Generic.List[object]]::new()
|
||||
$result=[ordered]@{milestone='Final';sourceCommit=(& git rev-parse HEAD);sourceDirty=[bool](& git status --porcelain);serial=$Serial;apkSha256=(Get-FileHash -LiteralPath $ApkPath -Algorithm SHA256).Hash;status='failed'}
|
||||
$result.rendererProjection=@{fieldOfView=62;fixedWorldPivot=@(0,.65,0);rawCamera='Saved Board.camera JSON remains canonical';viewport='Landscape wm size max/min; remaining UI and physical touch helper assume sole 2400x1080 AVD'}
|
||||
$networkBefore=$null; $musicBefore=$null
|
||||
function Adb([string[]]$Arguments) {
|
||||
$started=[DateTime]::UtcNow.ToString('o'); $output=& $adb -s $Serial @Arguments 2>&1; $code=$LASTEXITCODE
|
||||
$commands.Add(@{startedUtc=$started;finishedUtc=[DateTime]::UtcNow.ToString('o');command=('adb -s '+$Serial+' '+($Arguments -join ' '));exitCode=$code;output=($output -join "`n")})
|
||||
if ($code -ne 0) { throw "ADB exit $code : $($Arguments -join ' ')" }; return ($output -join "`n").Trim()
|
||||
}
|
||||
function Check([string]$Id,[bool]$Passed,[string]$Actual) {
|
||||
$checks.Add(@{id=$Id;passed=$Passed;actual=$Actual}); if (!$Passed) { throw "Content check failed: $Id ($Actual)" }
|
||||
}
|
||||
function Step([string]$Name,[string[]]$Arguments=@(),[int]$Delay=500) {
|
||||
& (Join-Path $PSScriptRoot 'observe.ps1') -Step $Name -AdbArguments $Arguments -Serial $Serial -EvidenceDir $EvidenceDir -DelayMilliseconds $Delay
|
||||
}
|
||||
function Save([string]$Name) { Adb @('pull',$remote,(Join-Path $EvidenceDir "$Name.json")) | Out-Null; return Get-Content -LiteralPath (Join-Path $EvidenceDir "$Name.json") -Raw | ConvertFrom-Json }
|
||||
function Same($First,$Second) { return ($First | ConvertTo-Json -Depth 12 -Compress) -eq ($Second | ConvertTo-Json -Depth 12 -Compress) }
|
||||
function BallPoint($Board,[string]$Id) {
|
||||
$ball=@($Board.balls | Where-Object id -eq $Id)[0]; $camera=$Board.camera
|
||||
$rotation=[Numerics.Quaternion]::new($camera.rotation.x,$camera.rotation.y,$camera.rotation.z,$camera.rotation.w)
|
||||
$origin=[Numerics.Vector3]::new($camera.target.x,($camera.target.y+.65),$camera.target.z)+[Numerics.Vector3]::Transform([Numerics.Vector3]::new(0,0,-$camera.distance),$rotation)
|
||||
$local=[Numerics.Vector3]::Transform([Numerics.Vector3]::new($ball.position.x,$ball.position.y,$ball.position.z)-$origin,[Numerics.Quaternion]::Inverse($rotation))
|
||||
$size=Adb @('shell','wm','size'); if ($size -notmatch '(\d+)x(\d+)') { throw "Unknown device resolution: $size" }
|
||||
$width=[Math]::Max([int]$Matches[1],[int]$Matches[2]); $height=[Math]::Min([int]$Matches[1],[int]$Matches[2])
|
||||
$scale=($height/2)/([Math]::Tan(31*[Math]::PI/180)*$local.Z)
|
||||
return @([int][Math]::Round(($width/2)+$local.X*$scale),[int][Math]::Round(($height/2)-$local.Y*$scale))
|
||||
}
|
||||
function TapBall([string]$Name,$Board,[string]$Id) { $point=BallPoint $Board $Id; Step $Name @('shell','input','tap',"$($point[0])","$($point[1])") }
|
||||
function StagePoint([int]$Id) { $index=$Id-1; return @([int][Math]::Round((.12+($index%6)*.152)*2400),[int][Math]::Round((1-(.76-[Math]::Floor($index/6)*.123))*1080)) }
|
||||
function SelectStage([string]$Name,[int]$Id) {
|
||||
Step "$Name-selection" @('shell','input','tap','1200','616')
|
||||
$point=StagePoint $Id; Step "$Name-stage" @('shell','input','tap',"$($point[0])","$($point[1])")
|
||||
}
|
||||
function StartHome([string]$Name) {
|
||||
Adb @('shell','input','keyevent','224') | Out-Null
|
||||
Adb @('shell','am','start','-W','-n',$activity) | Out-Null
|
||||
$observations=[Collections.Generic.List[object]]::new()
|
||||
$ready=$false; $pauseRequested=$false
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
for($attempt=0;$attempt-lt30;$attempt++) {
|
||||
$pidText=Adb @('shell','pidof',$package)
|
||||
$saveText=Adb @('shell',('if test -s '+$remote+'; then cat '+$remote+'; fi'))
|
||||
$validSave=$false
|
||||
try { $state=$saveText|ConvertFrom-Json -ErrorAction Stop; $validSave=$state.schemaVersion-eq1 -and $null-ne$state.completedStageIds -and $null-ne$state.puzzleStates } catch { }
|
||||
Step "$Name-ready-$attempt" @() 1000
|
||||
$png=Join-Path $EvidenceDir "$Name-ready-$attempt.png"
|
||||
$image=[Drawing.Bitmap]::new($png)
|
||||
$sky=0;$gold=0;$dark=0
|
||||
try {
|
||||
for($y=1;$y-lt20;$y++){for($x=1;$x-lt30;$x++) {
|
||||
$pixel=$image.GetPixel([int]($image.Width*$x/30),[int]($image.Height*$y/20))
|
||||
if($pixel.R-ge150-and$pixel.G-ge180-and$pixel.B-ge210){$sky++}
|
||||
if($pixel.R-ge200-and$pixel.G-ge130-and$pixel.B-le120){$gold++}
|
||||
if($y-le6-and$pixel.R-lt110-and$pixel.G-lt110-and$pixel.B-lt110){$dark++}
|
||||
}}
|
||||
$dark=0
|
||||
for ($y=[int]($image.Height*.17); $y -lt [int]($image.Height*.28); $y+=4) {
|
||||
for ($x=[int]($image.Width*.39); $x -lt [int]($image.Width*.61); $x+=4) {
|
||||
$pixel=$image.GetPixel($x,$y)
|
||||
if ($pixel.R -lt 110 -and $pixel.G -lt 110 -and $pixel.B -lt 110) { $dark++ }
|
||||
}
|
||||
}
|
||||
$homeButton=0; foreach($px in @(.36,.64)){foreach($py in @(.53,.61)){$p=$image.GetPixel([int]($image.Width*$px),[int]($image.Height*$py));if($p.R-ge200-and$p.G-ge130-and$p.B-le120){$homeButton++}}}
|
||||
$ui=$image.Width-gt$image.Height-and$sky-ge200-and$gold-ge10-and$dark-ge20-and$homeButton-eq4
|
||||
$observations.Add(@{attempt=$attempt;utc=[DateTime]::UtcNow.ToString('o');pid=$pidText;validAppSave=$validSave;landscape=($image.Width-gt$image.Height);skySamples=$sky;goldButtonSamples=$gold;titleDarkSamples=$dark;titleBoundsNormalized=@(.39,.17,.61,.28);titleStridePixels=4;homeButtonSamples=$homeButton;readyUi=$ui;png=$png})
|
||||
} finally {$image.Dispose()}
|
||||
if ($pidText -match '^\d+' -and $ui -and !$validSave -and !$pauseRequested) {
|
||||
$observations[$observations.Count-1] | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-first-home-without-save.json") -Encoding utf8
|
||||
Check "$Name-first-home-ui" $true 'Actual first Home title/button/PID observed before production pause creates save'
|
||||
Adb @('shell','input','keyevent','3') | Out-Null
|
||||
Start-Sleep -Milliseconds 500
|
||||
Adb @('shell','am','start','-W','-n',$activity) | Out-Null
|
||||
$pauseRequested=$true
|
||||
}
|
||||
if($pidText-match'^\d+'-and$validSave-and$ui){$ready=$true;$saveText|Set-Content (Join-Path $EvidenceDir "$Name-ready-save.json") -Encoding utf8;break}
|
||||
}
|
||||
$observations.ToArray()|ConvertTo-Json -Depth 6|Set-Content (Join-Path $EvidenceDir "$Name-readiness.json") -Encoding utf8
|
||||
Check "$Name-home-ready" $ready 'Actual app PID, landscape home colors/button/title and app save JSON; bounded30 observations, black/Unity splash excluded, PNG retained for direct review'
|
||||
Step $Name @() 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Restart([string]$Name) {
|
||||
Adb @('shell','input','keyevent','3') | Out-Null; Adb @('shell','am','force-stop',$package) | Out-Null
|
||||
StartHome "$Name-home"
|
||||
}
|
||||
function StartAudio([string]$Name,[int]$Seconds) {
|
||||
$readyPath=Join-Path $EvidenceDir "$Name.wav.ready.json"
|
||||
if (Test-Path -LiteralPath $readyPath) { Remove-Item -LiteralPath $readyPath }
|
||||
$args=@('-NoProfile','-File',(Join-Path $PSScriptRoot 'record-native-audio.ps1'),'-OutputPath',(Join-Path $EvidenceDir "$Name.wav"),'-Seconds',"$Seconds",'-Serial',$Serial)
|
||||
$process=Start-Process pwsh -ArgumentList ($args | ForEach-Object {'"'+$_+'"'}) -WindowStyle Hidden -RedirectStandardOutput (Join-Path $EvidenceDir "$Name.stdout") -RedirectStandardError (Join-Path $EvidenceDir "$Name.stderr") -PassThru
|
||||
$recorders.Add(@{name=$Name;process=$process;seconds=$Seconds;startedUtc=[DateTime]::UtcNow})
|
||||
$readyPath=Join-Path $EvidenceDir "$Name.wav.ready.json"
|
||||
$ready=$false
|
||||
for ($attempt=0; $attempt -lt 120; $attempt++) {
|
||||
if ($process.HasExited) { break }
|
||||
if (Test-Path -LiteralPath $readyPath) { $ready=$true; break }
|
||||
Start-Sleep -Milliseconds 100
|
||||
}
|
||||
Check "audio-ready-$Name" $ready 'Recorder reported actual Recording started before interaction'
|
||||
for ($attempt=0; $attempt -lt 20; $attempt++) {
|
||||
Adb @('shell','cmd','audio','set-volume','3','15') | Out-Null
|
||||
$route=Adb @('shell','dumpsys','audio')
|
||||
$route | Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-active-route-$attempt.txt") -Encoding utf8
|
||||
$current=[regex]::Match($route,'(?s)- STREAM_MUSIC:.*?Current: ([^\r\n]+)').Groups[1].Value
|
||||
if ($current -match '8000 \(remote_submix\): 15') { break }
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
$route | Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-active-route.txt") -Encoding utf8
|
||||
Check "audio-active-route-$Name" ($current -match '8000 \(remote_submix\): 15') $current
|
||||
}
|
||||
function CompareAudio([string]$Name,[string[]]$Ids) {
|
||||
$recorder=@($recorders | Where-Object name -eq $Name)[0].process
|
||||
Check "audio-finished-$Name" ($recorder.WaitForExit((@($recorders | Where-Object name -eq $Name)[0].seconds+30)*1000)) 'PCM capture finished'; $recorder.Refresh(); Check "audio-exit-$Name" ($recorder.ExitCode -eq 0) "$($recorder.ExitCode)"
|
||||
foreach ($id in $Ids) {
|
||||
$reference=Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) "Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/$id.wav"
|
||||
$comparison=Join-Path $EvidenceDir "$Name-$id-match.json"
|
||||
& pwsh -NoProfile -File (Join-Path $PSScriptRoot 'compare-audio.ps1') -ReferencePath $reference -CapturedPath (Join-Path $EvidenceDir "$Name.wav") -OutputPath $comparison -RequireFullSpeech
|
||||
$exit=$LASTEXITCODE; $match=Get-Content -LiteralPath $comparison -Raw | ConvertFrom-Json
|
||||
Check "pcm-$Name-$id" ($exit -eq 0 -and $match.passed -and $match.normalizedCorrelation -ge .95 -and $match.rmsPcm -gt 32) ($match | ConvertTo-Json -Compress)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
param([Parameter(Mandatory)][string]$EvidenceDir)
|
||||
$ErrorActionPreference='Stop'
|
||||
New-Item -ItemType Directory -Path $EvidenceDir -Force|Out-Null
|
||||
$source=Get-Content -LiteralPath (Join-Path $PSScriptRoot 'exercise-final-progression.ps1')
|
||||
$statements=@{}
|
||||
foreach($name in @('conserves-all-ids','actual-success','both-tutorials-persisted-once')){$statements[$name]=@($source|Where-Object {$_-match'^\s*Check '-and$_.Contains($name)})[0].Trim();if(!$statements[$name]){throw "Missing actual source assertion: $name"}}
|
||||
$results=[Collections.Generic.List[object]]::new()
|
||||
function Check([string]$Id,[bool]$Passed,[string]$Actual){if(!$Passed){throw "Actual source assertion rejected: $Id"}}
|
||||
function Counts($Board){return (@($Board.groups|ForEach-Object {$_.ballIds.Count}|Sort-Object)-join',')}
|
||||
function Board($State,[int]$Stage){return @($State.puzzleStates|Where-Object stageId -eq $Stage)[0]}
|
||||
foreach($case in @('valid-ids','wrong-ids','valid-target','wrong-target','missing-completion','valid-tutorials','missing-tutorial','duplicate-tutorial')){
|
||||
$stage=13;$expected='2,3'
|
||||
$original=[pscustomobject]@{balls=@([pscustomobject]@{id='a'},[pscustomobject]@{id='b'})}
|
||||
$free=[pscustomobject]@{sourceStageId=13;balls=@([pscustomobject]@{id='b'},[pscustomobject]@{id='a'});groups=@([pscustomobject]@{ballIds=@('a','b')},[pscustomobject]@{ballIds=@('c','d','e')})}
|
||||
$success=[pscustomobject]@{completedStageIds=@(1..13);puzzleStates=@();latestFreePlay=$free}
|
||||
$complete=[pscustomobject]@{seenTutorialIds=@('split','merge')}
|
||||
$kind=if($case.Contains('ids')){'conserves-all-ids'}elseif($case.Contains('tutorial')){'both-tutorials-persisted-once'}else{'actual-success'}
|
||||
switch($case){'wrong-ids'{$free.balls[1].id='different'}'wrong-target'{$free.groups[1].ballIds=@('c','d')}'missing-completion'{$success.completedStageIds=@(1..12)}'missing-tutorial'{$complete.seenTutorialIds=@('merge')}'duplicate-tutorial'{$complete.seenTutorialIds=@('merge','split','split')}}
|
||||
$expectedPass=$case.StartsWith('valid-');$actualPass=$false;$errorText=$null
|
||||
try{& ([scriptblock]::Create($statements[$kind]));$actualPass=$true}catch{$errorText=$_.Exception.Message}
|
||||
$results.Add(@{case=$case;sourceStatement=$statements[$kind];expectedPass=$expectedPass;actualPass=$actualPass;passed=($actualPass-eq$expectedPass);error=$errorText})
|
||||
}
|
||||
$passed=@($results|Where-Object passed -ne $true).Count-eq0
|
||||
@{passed=$passed;sourceCommit=(& git rev-parse HEAD);checks=$results.ToArray()}|ConvertTo-Json -Depth 5|Set-Content -LiteralPath (Join-Path $EvidenceDir 'assertions-result.json')
|
||||
if(!$passed){exit 1}
|
||||
exit 0
|
||||
Reference in New Issue
Block a user