Bind Android smoke to build provenance and actual PCM assertions
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
param([Parameter(Mandatory)][string]$ReferencePath, [Parameter(Mandatory)][string]$CapturedPath, [Parameter(Mandatory)][string]$OutputPath)
|
||||
$ErrorActionPreference='Stop'
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.IO;
|
||||
public static class AndroidPcmComparison {
|
||||
static short[] Read(string path) {
|
||||
using(var r=new BinaryReader(File.OpenRead(path))) {
|
||||
if(new string(r.ReadChars(4))!="RIFF") throw new Exception("RIFF WAV required");
|
||||
r.ReadInt32(); if(new string(r.ReadChars(4))!="WAVE") throw new Exception("WAVE required");
|
||||
bool format=false; int rate=0;
|
||||
while(r.BaseStream.Position+8<=r.BaseStream.Length) {
|
||||
string id=new string(r.ReadChars(4)); int size=r.ReadInt32(); long end=r.BaseStream.Position+size;
|
||||
if(id=="fmt ") { int encoding=r.ReadInt16(),channels=r.ReadInt16(); rate=r.ReadInt32(); r.ReadInt32(); r.ReadInt16(); format=encoding==1 && channels==1 && r.ReadInt16()==16 && rate>0; }
|
||||
if(id=="data") {
|
||||
if(!format) throw new Exception("Mono PCM16 required");
|
||||
var data=new short[size/2]; for(int i=0;i<data.Length;i++) data[i]=r.ReadInt16();
|
||||
if(rate==24000) return data;
|
||||
var resampled=new short[(int)((long)data.Length*24000/rate)];
|
||||
for(int i=0;i<resampled.Length;i++) { double index=(double)i*rate/24000; int left=(int)index; double fraction=index-left; resampled[i]=(short)(data[left]*(1-fraction)+data[Math.Min(left+1,data.Length-1)]*fraction); }
|
||||
return resampled;
|
||||
}
|
||||
r.BaseStream.Position=end+(size%2);
|
||||
}
|
||||
}
|
||||
throw new Exception("WAV data missing");
|
||||
}
|
||||
public static double[] Compare(string original,string captured) {
|
||||
var source=Read(original); var output=Read(captured); int length=Math.Min(4096,source.Length);
|
||||
double bestEnergy=-1; int anchor=0;
|
||||
for(int offset=0;offset+length<=source.Length;offset+=512) {
|
||||
double energy=0; for(int j=0;j<length;j++) energy+=(double)source[offset+j]*source[offset+j];
|
||||
if(energy>bestEnergy) { bestEnergy=energy; anchor=offset; }
|
||||
}
|
||||
if(bestEnergy<=0 || output.Length<source.Length) throw new Exception("Non-silent complete reference/capture required");
|
||||
double sourceAnchorEnergy=0; for(int j=0;j<length;j+=8) sourceAnchorEnergy+=(double)source[anchor+j]*source[anchor+j];
|
||||
double best=-1; int bestLag=0;
|
||||
for(int lag=0;lag+source.Length<=output.Length;lag++) {
|
||||
double dot=0, energy=0;
|
||||
for(int j=0;j<length;j+=8) { double value=output[lag+anchor+j]; dot+=source[anchor+j]*value; energy+=value*value; }
|
||||
double correlation=energy>0 ? dot/Math.Sqrt(sourceAnchorEnergy*energy) : 0;
|
||||
if(correlation>best) { best=correlation; bestLag=lag; }
|
||||
}
|
||||
double fullDot=0,sourceEnergy=0,outputEnergy=0; int nonzero=0;
|
||||
for(int j=0;j<source.Length;j++) { double s=source[j],o=output[bestLag+j]; fullDot+=s*o; sourceEnergy+=s*s; outputEnergy+=o*o; if(Math.Abs(o)>32) nonzero++; }
|
||||
double fullCorrelation=fullDot/Math.Sqrt(sourceEnergy*outputEnergy);
|
||||
return new[]{fullCorrelation,bestLag/24000.0,source.Length/24000.0,output.Length/24000.0,Math.Sqrt(outputEnergy/source.Length),fullDot/sourceEnergy,(double)nonzero};
|
||||
}
|
||||
}
|
||||
'@
|
||||
$values=[AndroidPcmComparison]::Compare((Resolve-Path -LiteralPath $ReferencePath).Path,(Resolve-Path -LiteralPath $CapturedPath).Path)
|
||||
$passed=$values[0] -ge 0.95 -and $values[4] -gt 32 -and $values[6] -gt 100
|
||||
$record=[ordered]@{ reference=$ReferencePath; referenceSha256=(Get-FileHash -LiteralPath $ReferencePath -Algorithm SHA256).Hash;
|
||||
captured=$CapturedPath; capturedSha256=(Get-FileHash -LiteralPath $CapturedPath -Algorithm SHA256).Hash;
|
||||
normalizedCorrelation=$values[0]; onsetSeconds=$values[1]; referenceDurationSeconds=$values[2]; capturedDurationSeconds=$values[3]; rmsPcm=$values[4]; gain=$values[5]; nonSilentSamples=$values[6]; thresholdCorrelation=0.95; passed=$passed; auditoryReview=$false }
|
||||
$record | ConvertTo-Json | Set-Content -LiteralPath $OutputPath -Encoding utf8
|
||||
Write-Host "실제 PCM 대조: 상관 $($values[0]), 비무음 샘플 $($values[6]), 통과 $passed"
|
||||
if (!$passed) { exit 1 }
|
||||
@@ -0,0 +1,114 @@
|
||||
param([string]$EvidenceDir='artifacts/verification/FirstSlice', [string]$Serial='emulator-5554')
|
||||
$ErrorActionPreference='Stop'
|
||||
$EvidenceDir=(Resolve-Path -LiteralPath $EvidenceDir).Path
|
||||
$apkPath=(Resolve-Path -LiteralPath 'artifacts/android/FirstSlice/MagnetismHeroes.apk').Path
|
||||
$checks=[Collections.Generic.List[object]]::new()
|
||||
$recordings=[Collections.Generic.List[object]]::new()
|
||||
function Step([string]$Name,[string[]]$Arguments,[int]$Delay=800) {
|
||||
& (Join-Path $PSScriptRoot 'observe.ps1') -Step $Name -AdbArguments $Arguments -Serial $Serial -EvidenceDir $EvidenceDir -DelayMilliseconds $Delay
|
||||
}
|
||||
function Save-State([string]$Name) {
|
||||
Step $Name @('pull','/sdcard/Android/data/com.magnetismheroes.game/files/progress.json',(Join-Path $EvidenceDir "$Name.json"))
|
||||
return (Get-Content -LiteralPath (Join-Path $EvidenceDir "$Name.json") -Raw | ConvertFrom-Json)
|
||||
}
|
||||
function Assert-State([string]$Id,[bool]$Passed,[string]$Actual) {
|
||||
$checks.Add([ordered]@{ id=$Id; passed=$Passed; actual=$Actual })
|
||||
if (!$Passed) { throw "저장 검증 실패: $Id ($Actual)" }
|
||||
}
|
||||
function Start-Audio([string]$Name,[int]$Seconds) {
|
||||
$audioPath=Join-Path $EvidenceDir "$Name.wav"
|
||||
$args=@('-NoProfile','-File',(Join-Path $PSScriptRoot 'record-audio.ps1'),'-OutputPath',$audioPath,'-Seconds',"$Seconds")
|
||||
$process=Start-Process -FilePath 'pwsh' -ArgumentList ($args | ForEach-Object { '"'+$_+'"' }) -WindowStyle Hidden -RedirectStandardOutput (Join-Path $EvidenceDir "$Name.stdout") -RedirectStandardError (Join-Path $EvidenceDir "$Name.stderr") -PassThru
|
||||
$recordings.Add(@{ name=$Name; process=$process; command=('pwsh '+($args -join ' ')) })
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
function Restart([string]$Prefix) {
|
||||
Step "$Prefix-pause" @('shell','input','keyevent','3')
|
||||
Step "$Prefix-force-stop" @('shell','am','force-stop','com.magnetismheroes.game')
|
||||
Step "$Prefix-home" @('shell','am','start','-W','-n','com.magnetismheroes.game/com.unity3d.player.UnityPlayerGameActivity') 10000
|
||||
}
|
||||
$result=[ordered]@{ sourceCommit=(& git rev-parse HEAD); sourceDirty=[bool](& git status --porcelain); apkSha256=(Get-FileHash -LiteralPath $apkPath -Algorithm SHA256).Hash; serial=$Serial; status='failed'; recordings=@(); checks=@() }
|
||||
try {
|
||||
Assert-State 'clean-source' (!$result.sourceDirty) $result.sourceCommit
|
||||
Step 'final-00-airplane-off' @('shell','cmd','connectivity','airplane-mode','disable')
|
||||
Step 'final-00-wifi-on' @('shell','svc','wifi','enable')
|
||||
Step 'final-00-data-on' @('shell','svc','data','enable')
|
||||
# 검증 담당자가 이 실행에서 설치한 앱 전용 데이터만 초기화합니다.
|
||||
Step 'final-00-reset-own-test-app' @('shell','pm','clear','com.magnetismheroes.game')
|
||||
Step 'final-01-home' @('shell','am','start','-W','-n','com.magnetismheroes.game/com.unity3d.player.UnityPlayerGameActivity') 10000
|
||||
Step 'final-02-selection' @('shell','input','tap','1200','616')
|
||||
Start-Audio 'audio-online-guidecollect' 8
|
||||
Step 'final-03-stage' @('shell','input','tap','1200','560') 2500
|
||||
Step 'final-04-precollect-tap' @('shell','input','swipe','930','510','930','510','200')
|
||||
$uncollected=Save-State 'final-04-uncollected'
|
||||
Assert-State 'direct-collection-required' ($uncollected.puzzleStates[0].collectedBallIds.Count -eq 0 -and $uncollected.puzzleStates[0].countedBallIds.Count -eq 0 -and $uncollected.completedStageIds.Count -eq 0) ($uncollected | ConvertTo-Json -Depth 8 -Compress)
|
||||
Start-Audio 'audio-online-guidecount' 8
|
||||
Step 'final-05-direct-collection' @('shell','input','swipe','930','510','1200','510','800') 4000
|
||||
Step 'final-06-camera-orbit' @('shell','input','swipe','1720','400','1860','470','600')
|
||||
$before=Save-State 'final-06-incomplete-before'
|
||||
Assert-State 'collected-with-empty-count' ($before.puzzleStates[0].collectedBallIds.Count -eq 1 -and $before.puzzleStates[0].countedBallIds.Count -eq 0) ($before | ConvertTo-Json -Depth 8 -Compress)
|
||||
Restart 'final-07'
|
||||
Step 'final-07-selection' @('shell','input','tap','1200','616')
|
||||
Step 'final-07-restored-incomplete' @('shell','input','tap','1200','560')
|
||||
$after=Save-State 'final-07-incomplete-after'
|
||||
Assert-State 'incomplete-json-restored' (($before | ConvertTo-Json -Depth 8 -Compress) -eq ($after | ConvertTo-Json -Depth 8 -Compress)) '배치/카메라/그룹/수집/빈 계수 전체 JSON 동일'
|
||||
Start-Audio 'audio-online-count01' 8
|
||||
Step 'final-08-success-same-board' @('shell','input','swipe','1182','514','1182','514','200') 1500
|
||||
$success=Save-State 'final-08-success'
|
||||
Assert-State 'completion-atomic-state' ($success.completedStageIds.Count -eq 1 -and $success.completedStageIds[0] -eq 1 -and $success.puzzleStates.Count -eq 0 -and $success.latestFreePlay.sourceStageId -eq 1) ($success | ConvertTo-Json -Depth 8 -Compress)
|
||||
Assert-State 'success-preserves-board-camera' (($before.puzzleStates[0].balls | ConvertTo-Json -Depth 6 -Compress) -eq ($success.latestFreePlay.balls | ConvertTo-Json -Depth 6 -Compress) -and ($before.puzzleStates[0].camera | ConvertTo-Json -Depth 6 -Compress) -eq ($success.latestFreePlay.camera | ConvertTo-Json -Depth 6 -Compress)) '성공 전후 balls/camera 동일'
|
||||
Step 'final-09-free-move' @('shell','input','swipe','1182','514','1380','470','800')
|
||||
$freeBefore=Save-State 'final-09-free-before'
|
||||
Assert-State 'free-move-changes-position' (($freeBefore.latestFreePlay.balls | ConvertTo-Json -Depth 6 -Compress) -ne ($success.latestFreePlay.balls | ConvertTo-Json -Depth 6 -Compress)) '실제 자유 놀이 balls 위치 변경'
|
||||
Restart 'final-10'
|
||||
Start-Audio 'audio-free-restore' 8
|
||||
Step 'final-10-continue-free' @('shell','input','tap','1200','810')
|
||||
$freeAfter=Save-State 'final-10-free-after'
|
||||
Assert-State 'free-json-restored' (($freeBefore | ConvertTo-Json -Depth 8 -Compress) -eq ($freeAfter | ConvertTo-Json -Depth 8 -Compress)) '자유 놀이 전체 JSON 동일'
|
||||
$restoreRecorder=$recordings | Where-Object name -eq 'audio-free-restore' | Select-Object -First 1
|
||||
if (!$restoreRecorder.process.WaitForExit(30000)) { throw '복원 음성 검사 수집 미종료' }
|
||||
$silence=Get-Content -LiteralPath (Join-Path $EvidenceDir 'audio-free-restore.wav.json') -Raw | ConvertFrom-Json
|
||||
Assert-State 'restore-no-historical-voice' ($silence.packets -gt 0 -and $silence.pcmBytes -gt 0 -and $silence.nonSilentSamples -eq 0) ($silence | ConvertTo-Json -Compress)
|
||||
Step 'final-11-airplane-on' @('shell','cmd','connectivity','airplane-mode','enable')
|
||||
Step 'final-11-wifi-off' @('shell','svc','wifi','disable')
|
||||
Step 'final-11-data-off' @('shell','svc','data','disable')
|
||||
Step 'final-11-wifi-state' @('shell','settings','get','global','wifi_on')
|
||||
Step 'final-11-data-state' @('shell','settings','get','global','mobile_data')
|
||||
Step 'final-11-connectivity' @('shell','dumpsys','connectivity')
|
||||
Step 'final-11-airplane-state' @('shell','settings','get','global','airplane_mode_on')
|
||||
$wifiState=Get-Content -LiteralPath (Join-Path $EvidenceDir 'final-11-wifi-state-commands.json') -Raw | ConvertFrom-Json
|
||||
$dataState=Get-Content -LiteralPath (Join-Path $EvidenceDir 'final-11-data-state-commands.json') -Raw | ConvertFrom-Json
|
||||
$connectivity=Get-Content -LiteralPath (Join-Path $EvidenceDir 'final-11-connectivity-commands.json') -Raw | ConvertFrom-Json
|
||||
$airplaneState=Get-Content -LiteralPath (Join-Path $EvidenceDir 'final-11-airplane-state-commands.json') -Raw | ConvertFrom-Json
|
||||
Assert-State 'offline-network-disabled' ($wifiState.commands[0].output.Trim() -eq '0' -and $dataState.commands[0].output.Trim() -eq '0' -and $airplaneState.commands[0].output.Trim() -eq '1' -and $connectivity.commands[0].output -match 'Active default network: none') 'airplane=1, wifi=0, mobile_data=0, Active default network: none'
|
||||
Step 'final-12-home' @('shell','input','tap','310','108')
|
||||
Step 'final-12-selection' @('shell','input','tap','1200','616')
|
||||
Start-Audio 'audio-offline' 20
|
||||
Step 'final-12-offline-stage' @('shell','input','tap','1200','560') 3000
|
||||
Step 'final-12-offline-collect' @('shell','input','swipe','930','510','1200','510','800') 4500
|
||||
$offlineCollected=Save-State 'final-12-offline-collected'
|
||||
Assert-State 'offline-collected-unfinished' ($offlineCollected.puzzleStates.Count -eq 1 -and $offlineCollected.puzzleStates[0].collectedBallIds.Count -eq 1 -and $offlineCollected.puzzleStates[0].countedBallIds.Count -eq 0) '오프라인 실제 수집됨/계수 전 0'
|
||||
Step 'final-12-offline-count-success' @('shell','input','swipe','1182','510','1182','510','200') 1500
|
||||
$offlineSuccess=Save-State 'final-12-offline-success'
|
||||
Assert-State 'offline-collection-count-actually-success' ($offlineSuccess.completedStageIds.Count -eq 1 -and $offlineSuccess.completedStageIds[0] -eq 1 -and $offlineSuccess.puzzleStates.Count -eq 0 -and $offlineSuccess.latestFreePlay.sourceStageId -eq 1 -and ($offlineCollected.puzzleStates[0].balls | ConvertTo-Json -Depth 6 -Compress) -eq ($offlineSuccess.latestFreePlay.balls | ConvertTo-Json -Depth 6 -Compress)) '완료1/미완료없음/수집 전후 같은 IDs와 배치 자유 전이'
|
||||
Step 'final-13-offline-free-move' @('shell','input','swipe','1182','510','1350','450','800')
|
||||
$offlineBefore=Save-State 'final-13-offline-before'
|
||||
Assert-State 'offline-free-move-changes-position' (($offlineBefore.latestFreePlay.balls | ConvertTo-Json -Depth 6 -Compress) -ne ($offlineSuccess.latestFreePlay.balls | ConvertTo-Json -Depth 6 -Compress)) '오프라인 실제 자유 놀이 balls 위치 변경'
|
||||
Restart 'final-14'
|
||||
Step 'final-14-offline-continue' @('shell','input','tap','1200','810')
|
||||
$offlineAfter=Save-State 'final-14-offline-after'
|
||||
Assert-State 'offline-json-restored' (($offlineBefore | ConvertTo-Json -Depth 8 -Compress) -eq ($offlineAfter | ConvertTo-Json -Depth 8 -Compress)) '오프라인 자유 놀이 전체 JSON 동일'
|
||||
foreach ($recording in $recordings) {
|
||||
if (!$recording.process.WaitForExit(30000)) { throw "오디오 수집 미종료: $($recording.name)" }
|
||||
$recording.process.Refresh()
|
||||
$result.recordings += [ordered]@{ name=$recording.name; command=$recording.command; exitCode=$recording.process.ExitCode }
|
||||
Assert-State "audio-recorder-$($recording.name)" ($recording.process.ExitCode -eq 0) "$($recording.process.ExitCode)"
|
||||
}
|
||||
$result.status='passed'
|
||||
} catch { $result.error=$_.Exception.Message; Write-Host $result.error -ForegroundColor Red }
|
||||
finally {
|
||||
$result.checks=$checks.ToArray(); $result.finishedUtc=[DateTime]::UtcNow.ToString('o')
|
||||
$result | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath (Join-Path $EvidenceDir 'android-state-checks.json') -Encoding utf8
|
||||
}
|
||||
if ($result.status -ne 'passed') { exit 1 }
|
||||
Write-Host "실제 Android 저장/복원 검사 $($checks.Count)개 통과. PNG 의미 검수는 별도입니다."
|
||||
@@ -1,4 +1,5 @@
|
||||
param([Parameter(Mandatory)][string]$ApkPath, [Parameter(Mandatory)][string]$EvidenceDir)
|
||||
param([Parameter(Mandatory)][string]$ApkPath, [Parameter(Mandatory)][string]$EvidenceDir,
|
||||
[string]$SourceCommit=(& git rev-parse HEAD), [bool]$SourceDirty=[bool](& git status --porcelain), [string]$MetadataName='apk-inspection.json')
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$sdk = if ($env:ANDROID_SDK_ROOT) { $env:ANDROID_SDK_ROOT } else { Join-Path $env:LOCALAPPDATA 'Android/Sdk' }
|
||||
$aapt = Get-ChildItem -LiteralPath (Join-Path $sdk 'build-tools') -Filter aapt.exe -Recurse | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
@@ -23,5 +24,5 @@ if (!$activity) { throw 'APK 시작 Activity를 찾지 못했습니다.' }
|
||||
if (($badging -join "`n") -match "uses-permission: name='android.permission.INTERNET'") { throw '오프라인 앱에 INTERNET 권한이 있습니다.' }
|
||||
[ordered]@{ apk=$ApkPath; apkSha256=(Get-FileHash -LiteralPath $ApkPath -Algorithm SHA256).Hash; package=$package;
|
||||
abis=$abis; activity=$activity; aaptCommand=('"' + $aapt.FullName + '" dump badging "' + $ApkPath + '"'); aaptExit=$nativeExit;
|
||||
sourceCommit=(& git rev-parse HEAD); sourceDirty=[bool](& git status --porcelain) } |
|
||||
ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $EvidenceDir 'apk-metadata.json') -Encoding utf8
|
||||
sourceCommit=$SourceCommit; sourceDirty=$SourceDirty } |
|
||||
ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $EvidenceDir $MetadataName) -Encoding utf8
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
param([Parameter(Mandatory)][string]$OutputPath, [int]$Seconds=15, [int]$Port=8554)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
public static class AndroidPcmLevels {
|
||||
public static double[] Measure(byte[] bytes) {
|
||||
double sum=0, peak=0; int active=0;
|
||||
for(int i=0;i+1<bytes.Length;i+=2) { int sample=(short)(bytes[i] | (bytes[i+1]<<8)); sum+=(double)sample*sample; peak=Math.Max(peak,Math.Abs(sample)); if(Math.Abs(sample)>32) active++; }
|
||||
return new[]{peak, bytes.Length>0 ? Math.Sqrt(sum/(bytes.Length/2)) : 0, (double)active};
|
||||
}
|
||||
}
|
||||
'@
|
||||
# EmulatorController.streamAudio의 PCM을 직접 기록합니다. 제품 APK에는 포함되지 않습니다.
|
||||
# wire schema: Android SDK emulator/lib/emulator_controller.proto.
|
||||
[AppContext]::SetSwitch('System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport', $true)
|
||||
@@ -19,11 +29,13 @@ $request.Content.Headers.ContentType = [Net.Http.Headers.MediaTypeHeaderValue]::
|
||||
$cancel = [Threading.CancellationTokenSource]::new([TimeSpan]::FromSeconds($Seconds))
|
||||
$pcm = [IO.MemoryStream]::new()
|
||||
$packets = 0
|
||||
$startedUtc=[DateTime]::UtcNow.ToString('o')
|
||||
$firstPacketTimestamp=$null; $lastPacketTimestamp=$null
|
||||
function Read-Exactly($Stream,[int]$Length) {
|
||||
$bytes = [byte[]]::new($Length)
|
||||
$offset=0
|
||||
while ($offset -lt $Length) {
|
||||
$n=$Stream.ReadAsync($bytes.AsMemory($offset,$Length-$offset),$cancel.Token).AsTask().GetAwaiter().GetResult()
|
||||
$n=$Stream.ReadAsync($bytes,$offset,$Length-$offset,$cancel.Token).GetAwaiter().GetResult()
|
||||
if ($n -eq 0) { throw 'gRPC 오디오 스트림이 끝났습니다.' }
|
||||
$offset += $n
|
||||
}
|
||||
@@ -42,13 +54,13 @@ try {
|
||||
while (!$cancel.IsCancellationRequested) {
|
||||
$header=Read-Exactly $stream 5
|
||||
if ($header[0] -ne 0) { throw '압축 gRPC 프레임은 지원하지 않습니다.' }
|
||||
$length=($header[1] -shl 24) -bor ($header[2] -shl 16) -bor ($header[3] -shl 8) -bor $header[4]
|
||||
$length=([int]$header[1] -shl 24) -bor ([int]$header[2] -shl 16) -bor ([int]$header[3] -shl 8) -bor [int]$header[4]
|
||||
$message=Read-Exactly $stream $length
|
||||
$i=0
|
||||
while ($i -lt $message.Length) {
|
||||
$tag=Read-Varint $message ([ref]$i)
|
||||
switch ($tag -band 7) {
|
||||
0 { $null=Read-Varint $message ([ref]$i) }
|
||||
0 { $value=Read-Varint $message ([ref]$i); if (($tag -shr 3) -eq 2) { if ($null -eq $firstPacketTimestamp) { $firstPacketTimestamp=$value }; $lastPacketTimestamp=$value } }
|
||||
2 {
|
||||
$size=Read-Varint $message ([ref]$i)
|
||||
if (($tag -shr 3) -eq 3) { $pcm.Write($message,$i,$size); $packets++ }
|
||||
@@ -62,6 +74,7 @@ try {
|
||||
if (!$cancel.IsCancellationRequested) { throw }
|
||||
} finally {
|
||||
$data=$pcm.ToArray()
|
||||
$levels=[AndroidPcmLevels]::Measure($data)
|
||||
$file=[IO.File]::Create($OutputPath)
|
||||
$writer=[IO.BinaryWriter]::new($file)
|
||||
try {
|
||||
@@ -69,7 +82,7 @@ try {
|
||||
$writer.Write([int]16); $writer.Write([int16]1); $writer.Write([int16]1); $writer.Write([int]24000); $writer.Write([int]48000); $writer.Write([int16]2); $writer.Write([int16]16);
|
||||
$writer.Write([Text.Encoding]::ASCII.GetBytes('data')); $writer.Write([int]$data.Length); $writer.Write($data)
|
||||
} finally { $writer.Dispose(); $file.Dispose(); $client.Dispose(); $cancel.Dispose(); $pcm.Dispose() }
|
||||
[ordered]@{ method='EmulatorController.streamAudio'; endpoint="127.0.0.1:$Port"; packets=$packets; pcmBytes=$data.Length; sampleRate=24000; channels=1; bitsPerSample=16; auditoryReview=$false } |
|
||||
[ordered]@{ method='EmulatorController.streamAudio'; endpoint="127.0.0.1:$Port"; sourceCommit=(& git rev-parse HEAD); sourceDirty=[bool](& git status --porcelain); startedUtc=$startedUtc; finishedUtc=[DateTime]::UtcNow.ToString('o'); firstDevicePacketTimestampUs=$firstPacketTimestamp; lastDevicePacketTimestampUs=$lastPacketTimestamp; packets=$packets; pcmBytes=$data.Length; sampleRate=24000; channels=1; bitsPerSample=16; peakPcm=$levels[0]; rmsPcm=$levels[1]; nonSilentSamples=$levels[2]; auditoryReview=$false } |
|
||||
ConvertTo-Json | Set-Content -LiteralPath ($OutputPath + '.json') -Encoding utf8
|
||||
}
|
||||
if ($packets -eq 0) { throw '실제 오디오 패킷을 받지 못했습니다.' }
|
||||
|
||||
+32
-2
@@ -18,8 +18,12 @@ function Assert-Check([string]$Name,[bool]$Condition,[string]$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-metadata.json') -Raw | ConvertFrom-Json
|
||||
$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]
|
||||
@@ -44,14 +48,40 @@ try {
|
||||
$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) (($observations | ForEach-Object actual) -join ' ')
|
||||
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 = @()
|
||||
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 {
|
||||
|
||||
+8
-2
@@ -45,8 +45,13 @@ try {
|
||||
if ($LASTEXITCODE -ne 0 -or !(Test-Path -LiteralPath $apkPath)) { throw 'Android build failed or produced no APK.' }
|
||||
$record.apk = $apkPath
|
||||
$record.apkSha256 = (Get-FileHash -LiteralPath $apkPath -Algorithm SHA256).Hash
|
||||
& (Join-Path $PSScriptRoot 'android/inspect-apk.ps1') -ApkPath $apkPath -EvidenceDir $evidenceDir
|
||||
& (Join-Path $PSScriptRoot 'android/inspect-apk.ps1') -ApkPath $apkPath -EvidenceDir $evidenceDir -SourceCommit $record.sourceCommit -SourceDirty $record.sourceDirty -MetadataName 'apk-metadata.json'
|
||||
$record.apkMetadata = Join-Path $evidenceDir 'apk-metadata.json'
|
||||
$record.sourceCommitEnd = (& git rev-parse HEAD)
|
||||
& git update-index --refresh | Out-Null
|
||||
$record.sourceDirtyEnd = [bool](& git status --porcelain)
|
||||
$record.trackedDiffEnd = @(& git diff --name-only HEAD)
|
||||
if ($record.sourceDirty -or $record.sourceDirtyEnd -or $record.sourceCommitEnd -ne $record.sourceCommit) { throw 'Source changed during the Android build or is dirty.' }
|
||||
if ($Milestone -eq 'Final') {
|
||||
Copy-Item -LiteralPath $apkPath -Destination (Join-Path $projectRoot 'artifacts/android/MagnetismHeroes.apk')
|
||||
}
|
||||
@@ -54,8 +59,9 @@ try {
|
||||
$smokeScript = Join-Path $PSScriptRoot 'android/smoke.ps1'
|
||||
if (!(Test-Path -LiteralPath $smokeScript)) { throw 'Android smoke verification has not been implemented yet.' }
|
||||
$record.command = "pwsh -NoProfile -File tools/android/smoke.ps1 -ApkPath $apkPath -Milestone $Milestone -EvidenceDir $evidenceDir"
|
||||
& pwsh -NoProfile -File $smokeScript -ApkPath $apkPath -Milestone $Milestone -EvidenceDir $evidenceDir
|
||||
& pwsh -NoProfile -File $smokeScript -ApkPath $apkPath -Milestone $Milestone -EvidenceDir $evidenceDir *> $logPath
|
||||
$record.exitCode = $LASTEXITCODE
|
||||
Get-Content -LiteralPath $logPath | Select-Object -Last 5 | Write-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw "Android smoke verification exited $LASTEXITCODE." }
|
||||
}
|
||||
$record.status = 'passed'
|
||||
|
||||
Reference in New Issue
Block a user