fix(presentation): reuse counting views and gate native QA readiness

This commit is contained in:
NINI
2026-10-05 22:24:13 +09:00
parent 9aa9e3c709
commit 4081a1c1ed
4 changed files with 134 additions and 7 deletions
+77 -7
View File
@@ -7,7 +7,7 @@ $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='Content';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
$networkBefore=$null; $musicBefore=$null
function Adb([string[]]$Arguments) {
$output=& $adb -s $Serial @Arguments 2>&1; $code=$LASTEXITCODE
$commands.Add(@{command=('adb -s '+$Serial+' '+($Arguments -join ' '));exitCode=$code;output=($output -join "`n")})
@@ -42,9 +42,44 @@ 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
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++}
}}
$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-ge2-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;homeButtonSamples=$homeButton;readyUi=$ui;png=$png})
} finally {$image.Dispose()}
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
Step "$Name-home" @('shell','am','start','-W','-n',$activity) 6500
StartHome "$Name-home"
}
function Fixture([int]$Stage) {
Adb @('shell','am','force-stop',$package) | Out-Null
@@ -52,17 +87,32 @@ function Fixture([int]$Stage) {
$path=Join-Path $EvidenceDir "fixture-$Stage.json"; $fixture | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding utf8
Adb @('shell','mkdir','-p',"/sdcard/Android/data/$package/files") | Out-Null; Adb @('push',$path,$remote) | Out-Null
Adb @('shell','rm','-f',($remote+'.bak')) | Out-Null
Step "fixture-$Stage-home" @('shell','am','start','-W','-n',$activity) 6500
StartHome "fixture-$Stage-home"
Step "fixture-$Stage-selection" @('shell','input','tap','1200','616')
}
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}); Start-Sleep -Seconds 2
$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'
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.txt") -Encoding utf8
$current=[regex]::Match($route,'(?s)- STREAM_MUSIC:.*?Current: ([^\r\n]+)').Groups[1].Value
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(30000)) 'PCM capture finished'; $recorder.Refresh(); Check "audio-exit-$Name" ($recorder.ExitCode -eq 0) "$($recorder.ExitCode)"
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"
@@ -77,11 +127,17 @@ try {
Check 'build-provenance' ($build.status -eq 'passed' -and $build.exitCode -eq 0 -and !$build.sourceDirty -and !$build.sourceDirtyEnd -and $build.sourceCommit -eq $result.sourceCommit -and $build.sourceCommitEnd -eq $result.sourceCommit -and $build.apkSha256 -eq $result.apkSha256) $result.apkSha256
Adb @('wait-for-device') | Out-Null; Check 'boot-completed' ((Adb @('shell','getprop','sys.boot_completed')) -eq '1') 'Emulator boot completed'
$result.device=@{model=(Adb @('shell','getprop','ro.product.model'));api=(Adb @('shell','getprop','ro.build.version.sdk'));abi=(Adb @('shell','getprop','ro.product.cpu.abilist'));bridge=(Adb @('shell','getprop','ro.dalvik.vm.native.bridge'))}
$audioBefore=Adb @('shell','dumpsys','audio')
$audioBefore | Set-Content -LiteralPath (Join-Path $EvidenceDir 'music-before.txt') -Encoding utf8
$current=[regex]::Match($audioBefore,'(?s)- STREAM_MUSIC:.*?Current: ([^\r\n]+)').Groups[1].Value
$musicBefore=@{}
foreach ($match in [regex]::Matches($current,'([0-9a-f]+) \([^)]+\): (\d+)')) { $musicBefore[$match.Groups[1].Value]=[int]$match.Groups[2].Value }
Check 'music-original-routes' ($musicBefore.ContainsKey('2') -and $musicBefore.ContainsKey('8000') -and $musicBefore.ContainsKey('40000000')) $current
Adb @('install','-r',$ApkPath) | Out-Null; Adb @('logcat','-c') | Out-Null; Adb @('shell','pm','clear',$package) | Out-Null
$networkBefore=@{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'))}; $result.networkBefore=$networkBefore
Adb @('shell','cmd','connectivity','airplane-mode','enable') | Out-Null; Adb @('shell','svc','wifi','disable') | Out-Null; Adb @('shell','svc','data','disable') | Out-Null
Check 'offline-real-network' ((Adb @('shell','settings','get','global','airplane_mode_on')) -eq '1' -and (Adb @('shell','settings','get','global','wifi_on')) -eq '0' -and (Adb @('shell','settings','get','global','mobile_data')) -eq '0' -and (Adb @('shell','dumpsys','connectivity')) -match 'Active default network: none') 'airplane1 wifi0 data0 no active default network'
Step 'fresh-home' @('shell','am','start','-W','-n',$activity) 6500; Step 'fresh-30-selection' @('shell','input','tap','1200','616')
StartHome 'fresh-home'; Step 'fresh-30-selection' @('shell','input','tap','1200','616')
$two=StagePoint 2; Step 'fresh-locked-two' @('shell','input','tap',"$($two[0])","$($two[1])")
$locked=Save 'fresh-locked'; Check 'fresh-only-one' ($locked.completedStageIds.Count -eq 0 -and $locked.puzzleStates.Count -eq 0) 'Locked Stage2 tap opens no puzzle'
StartAudio 'audio-one-offline' 20
@@ -151,6 +207,20 @@ try {
$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 did not finish before restoration' }
}
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
@@ -160,7 +230,7 @@ finally {
Check 'network-restored' (Same $networkBefore $result.networkAfter) ($result.networkAfter | ConvertTo-Json -Compress)
} catch { $result.status='failed'; $result.restoreError=$_.Exception.Message }
}
foreach ($recorder in $recorders) { if (!$recorder.process.HasExited) { $recorder.process.WaitForExit(30000) | Out-Null } }
$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-content-evidence.json') -Encoding utf8
}
+13
View File
@@ -18,6 +18,19 @@ $oldAdb=$env:ADB
try {
$env:ADB=Join-Path $env:LOCALAPPDATA 'Android/Sdk/platform-tools/adb.exe'
$process=Start-Process $exe -ArgumentList ($arguments | ForEach-Object {'"'+$_+'"'}) -WindowStyle Hidden -RedirectStandardOutput ($OutputPath+'.stdout') -RedirectStandardError ($OutputPath+'.stderr') -PassThru
$record.recorderPid=$process.Id
$ready=$false
for ($attempt=0; $attempt -lt 100; $attempt++) {
$process.Refresh()
if ($process.HasExited) { break }
if ((Get-Content -LiteralPath ($OutputPath+'.stdout') -Raw -ErrorAction SilentlyContinue) -match 'Recording started to wav file:') {
$record.readyUtc=[DateTime]::UtcNow.ToString('o')
@{recorderPid=$process.Id;readyUtc=$record.readyUtc;method=$record.method} | ConvertTo-Json | Set-Content -LiteralPath ($OutputPath+'.ready.json') -Encoding utf8
$ready=$true; break
}
Start-Sleep -Milliseconds 100
}
if (!$ready) { if (!$process.HasExited) { $process.Kill(); $process.WaitForExit() }; throw 'Android PCM recorder did not report recording started' }
if (!$process.WaitForExit(($Seconds+20)*1000)) { $process.Kill();throw 'Android PCM 녹음 종료 시간 초과' }
$process.Refresh();$record.exitCode=$process.ExitCode
if ($process.ExitCode -ne 0) { throw "Android PCM recorder exit $($process.ExitCode)" }