param([Parameter(Mandatory)][string]$ApkPath, [string]$Milestone='FirstSlice', [Parameter(Mandatory)][string]$EvidenceDir, [string]$Serial='emulator-5554') $ErrorActionPreference = 'Stop' $sdk = if ($env:ANDROID_SDK_ROOT) { $env:ANDROID_SDK_ROOT } else { Join-Path $env:LOCALAPPDATA 'Android/Sdk' } $adb = Join-Path $sdk 'platform-tools/adb.exe' $commands = [Collections.Generic.List[object]]::new() $checks = [Collections.Generic.List[object]]::new() function Invoke-Adb([string[]]$Arguments) { $output = & $adb -s $Serial @Arguments 2>&1 $code = $LASTEXITCODE $commands.Add([ordered]@{ command=('"' + $adb + '" -s ' + $Serial + ' ' + ($Arguments -join ' ')); exitCode=$code; output=($output -join "`n") }) if ($code -ne 0) { throw "ADB 실패: $($Arguments -join ' '), 종료 코드 $code" } return ($output -join "`n").Trim() } function Assert-Check([string]$Name,[bool]$Condition,[string]$Actual) { $checks.Add([ordered]@{ criterion=$Name; passed=$Condition; actual=$Actual }) if (!$Condition) { throw "검사 실패: $Name ($Actual)" } } $result = [ordered]@{ milestone=$Milestone; serial=$Serial; sourceCommit=(& git rev-parse HEAD); sourceDirty=[bool](& git status --porcelain); status='failed'; commands=@(); checks=@() } try { Assert-Check 'clean-source' (!$result.sourceDirty) $result.sourceCommit $buildRecord = Get-Content -LiteralPath (Join-Path $EvidenceDir 'androidbuild-evidence.json') -Raw | ConvertFrom-Json $buildMetadata = Get-Content -LiteralPath (Join-Path $EvidenceDir 'apk-metadata.json') -Raw | ConvertFrom-Json $actualApkHash = (Get-FileHash -LiteralPath $ApkPath -Algorithm SHA256).Hash Assert-Check 'verified-build-provenance' ($buildRecord.status -eq 'passed' -and $buildRecord.exitCode -eq 0 -and $buildRecord.sourceDirty -eq $false -and $buildRecord.sourceDirtyEnd -eq $false -and $buildRecord.sourceCommit -eq $result.sourceCommit -and $buildRecord.sourceCommitEnd -eq $result.sourceCommit -and $buildRecord.apkSha256 -eq $actualApkHash -and $buildMetadata.sourceCommit -eq $buildRecord.sourceCommit -and $buildMetadata.apkSha256 -eq $actualApkHash -and $buildMetadata.sourceDirty -eq $false) $actualApkHash & (Join-Path $PSScriptRoot 'inspect-apk.ps1') -ApkPath $ApkPath -EvidenceDir $EvidenceDir $apk = Get-Content -LiteralPath (Join-Path $EvidenceDir 'apk-inspection.json') -Raw | ConvertFrom-Json $result.apkSha256 = $apk.apkSha256 $supported = (Invoke-Adb @('shell','getprop','ro.product.cpu.abilist')) -split ',' $native = $supported[0] $bridge = Invoke-Adb @('shell','getprop','ro.dalvik.vm.native.bridge') $intersection = @($apk.abis | Where-Object { $supported -contains $_ }) $result.device = [ordered]@{ supportedAbis=$supported; nativeArchitecture=$native; bridge=$bridge; apkSupportedIntersection=$intersection; model=(Invoke-Adb @('shell','getprop','ro.product.model')); api=(Invoke-Adb @('shell','getprop','ro.build.version.sdk')); features=(Invoke-Adb @('shell','pm','list','features')); graphics=(Invoke-Adb @('shell','dumpsys','SurfaceFlinger')) } Assert-Check 'supported-abi-intersection' ($intersection.Count -gt 0) ($intersection -join ',') if ($supported[0] -ne $intersection[0]) { Assert-Check 'arm-native-bridge' ($bridge -match '^lib.+\.so$') $bridge } Invoke-Adb @('install','-r',$ApkPath) | Out-Null Invoke-Adb @('shell','am','start','-W','-n',($apk.package + '/' + $apk.activity)) | Out-Null Start-Sleep -Seconds 3 $pidValue = Invoke-Adb @('shell','pidof',$apk.package) Assert-Check 'running-process' ($pidValue -match '^\d+') $pidValue Invoke-Adb @('logcat','-d','-s','Unity','AndroidRuntime') | Set-Content -LiteralPath (Join-Path $EvidenceDir 'android-runtime.log') -Encoding utf8 & (Join-Path $PSScriptRoot 'observe.ps1') -Step 'smoke-home' -Serial $Serial -EvidenceDir $EvidenceDir $manualPath = Join-Path $EvidenceDir 'android-observations.json' Assert-Check 'observations-present' (Test-Path -LiteralPath $manualPath) $manualPath $manual = Get-Content -LiteralPath $manualPath -Raw | ConvertFrom-Json Assert-Check 'observations-match-apk' ($manual.apkSha256 -eq $apk.apkSha256 -and $manual.sourceCommit -eq $result.sourceCommit -and $manual.serial -eq $Serial) $manual.apkSha256 $required = @('landscape-korean-assets','precollect-tap','direct-collection','camera-orbit','incomplete-restore','count-success-same-board','free-move','free-restore','offline-collection-count','offline-restore','guide-collect-playback','guide-count-playback','count01-playback') foreach ($id in $required) { $observations = @($manual.cases | Where-Object id -eq $id) Assert-Check "observation-$id" ($observations.Count -eq 1 -and $observations[0].passed -eq $true -and $observations[0].actual -and $observations[0].procedure -and $observations[0].expected) (($observations | ForEach-Object actual) -join ' ') foreach ($file in $observations[0].evidence) { $path = Join-Path $EvidenceDir $file Assert-Check "evidence-$id-$file" ((Test-Path -LiteralPath $path) -and (Get-Item -LiteralPath $path).Length -gt 0) $file } Assert-Check "has-evidence-$id" (@($observations[0].evidence).Count -gt 0) (@($observations[0].evidence) -join ',') } $voiceHashes = [ordered]@{ guide_collect='62E15C9E38C36F4094A6C7E9C5148510141290B5F41DB5AAD9DC1D8F1A158594'; guide_count='DEBE29B89F3E2856F2BF4C5E01707A5F0A0657E5768C03BE385EC88FA2328B40'; count_01='13A882CE1620140D447635E14F2D4758045D5E348960C17386DF1C720050C100' } $audioMatches = @() $audioMetadata = Get-Content -LiteralPath (Join-Path $EvidenceDir 'audio-offline.wav.json') -Raw | ConvertFrom-Json Assert-Check 'actual-pcm-provenance' ($audioMetadata.sourceCommit -eq $result.sourceCommit -and $audioMetadata.sourceDirty -eq $false -and $audioMetadata.packets -gt 0 -and $audioMetadata.pcmBytes -gt 0 -and [DateTime]$audioMetadata.startedUtc -ge [DateTime]$buildRecord.finishedUtc) $audioMetadata.sourceCommit foreach ($voiceId in $voiceHashes.Keys) { $reference = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) "Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/$voiceId.wav" Assert-Check "approved-wave-$voiceId" ((Get-FileHash -LiteralPath $reference -Algorithm SHA256).Hash -eq $voiceHashes[$voiceId]) $voiceHashes[$voiceId] $comparisonPath = Join-Path $EvidenceDir "audio-offline-$voiceId-match.json" $capturedPath = Join-Path $EvidenceDir 'audio-offline.wav' & pwsh -NoProfile -File (Join-Path $PSScriptRoot 'compare-audio.ps1') -ReferencePath $reference -CapturedPath $capturedPath -OutputPath $comparisonPath $comparisonExit = $LASTEXITCODE $comparison = Get-Content -LiteralPath $comparisonPath -Raw | ConvertFrom-Json Assert-Check "actual-pcm-$voiceId" ($comparisonExit -eq 0 -and $comparison.passed -eq $true -and $comparison.normalizedCorrelation -ge 0.95 -and $comparison.rmsPcm -gt 32 -and $comparison.referenceSha256 -eq $voiceHashes[$voiceId]) ($comparison | ConvertTo-Json -Compress) $audioMatches += $comparison } Assert-Check 'offline-voice-order' ($audioMatches[0].onsetSeconds + $audioMatches[0].referenceDurationSeconds -le $audioMatches[1].onsetSeconds -and $audioMatches[1].onsetSeconds + $audioMatches[1].referenceDurationSeconds -le $audioMatches[2].onsetSeconds) (($audioMatches | ForEach-Object onsetSeconds) -join ',') $result.audioComparisons=$audioMatches $result.manualObservations = $manual $state = Get-Content -LiteralPath (Join-Path $EvidenceDir 'android-state-checks.json') -Raw | ConvertFrom-Json Assert-Check 'state-checks-provenance' ($state.sourceCommit -eq $result.sourceCommit -and $state.sourceDirty -eq $false -and $state.apkSha256 -eq $actualApkHash -and $state.serial -eq $Serial) $state.sourceCommit Assert-Check 'state-checks-passed' ($state.status -eq 'passed' -and @($state.checks).Count -ge 8 -and @($state.checks | Where-Object passed -ne $true).Count -eq 0) "$(@($state.checks).Count)" foreach ($id in @('direct-collection-required','collected-with-empty-count','incomplete-json-restored','completion-atomic-state','success-preserves-board-camera','free-move-changes-position','free-json-restored','restore-no-historical-voice','offline-network-disabled','offline-collected-unfinished','offline-collection-count-actually-success','offline-free-move-changes-position','offline-json-restored')) { Assert-Check "required-state-$id" (@($state.checks | Where-Object { $_.id -eq $id -and $_.passed -eq $true }).Count -eq 1) $id } $result.stateChecks=$state $result.status='passed' } catch { $result.error=$_.Exception.Message; Write-Host $result.error -ForegroundColor Red } finally { $result.commands=$commands.ToArray(); $result.checks=$checks.ToArray(); $result.finishedUtc=[DateTime]::UtcNow.ToString('o') $result | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath (Join-Path $EvidenceDir 'android-device-evidence.json') -Encoding utf8 } if ($result.status -ne 'passed') { exit 1 } Write-Host "Android 실제 기기 검사와 $($required.Count)개 관찰 검사 통과. 근거: $EvidenceDir" exit 0