param( [Parameter(Mandatory)][ValidateSet('EditMode','PlayMode','AndroidBuild','AndroidSmoke')][string]$Phase, [ValidateSet('FirstSlice','Composition','Persistence','Content','Final')][string]$Milestone = 'Final' ) $ErrorActionPreference = 'Stop' $projectRoot = Split-Path $PSScriptRoot -Parent Set-Location -LiteralPath $projectRoot $evidenceDir = Join-Path $projectRoot "artifacts/verification/$Milestone" New-Item -ItemType Directory -Path $evidenceDir -Force | Out-Null $phaseName = $Phase.ToLowerInvariant() $logPath = Join-Path $evidenceDir "$phaseName.log" $record = [ordered]@{ phase = $Phase; milestone = $Milestone; startedUtc = [DateTime]::UtcNow.ToString('o') sourceCommit = (& git rev-parse HEAD); sourceDirty = [bool](& git status --porcelain) command = $null; exitCode = $null; status = 'not-run'; log = $logPath } try { $record.editor = (Get-Content 'ProjectSettings/ProjectVersion.txt' | Select-String '^m_EditorVersion:').ToString().Split(':',2)[1].Trim() $record.packages = (Get-Content 'Packages/manifest.json' -Raw | ConvertFrom-Json).dependencies $apkPath = Join-Path $projectRoot "artifacts/android/$Milestone/MagnetismHeroes.apk" if ($Phase -in @('EditMode','PlayMode')) { $xmlPath = Join-Path $evidenceDir "$phaseName.xml" if (Test-Path -LiteralPath $xmlPath) { Remove-Item -LiteralPath $xmlPath } $cliArgs = @('test', $projectRoot, '--mode', $Phase, '--output', $xmlPath, '--timeout', '900', '--', '-logFile', $logPath) $record.command = 'unity ' + (($cliArgs | ForEach-Object { '"' + $_ + '"' }) -join ' ') & unity @cliArgs $record.exitCode = $LASTEXITCODE if (!(Test-Path -LiteralPath $xmlPath)) { throw "Unity $Phase exited $($record.exitCode) without a test report. See $logPath" } [xml]$report = Get-Content -LiteralPath $xmlPath -Raw $cases = @($report.SelectNodes('//test-case')) $record.total = $cases.Count $record.passed = @($cases | Where-Object { $_.result -eq 'Passed' }).Count $record.failed = @($cases | Where-Object { $_.result -eq 'Failed' }).Count $record.notPassed = $record.total - $record.passed if ($record.exitCode -ne 0) { throw "Unity $Phase exited $($record.exitCode). See $logPath" } if ($record.total -eq 0 -or $record.notPassed -ne 0) { throw "Test report contains $($record.total) tests and $($record.notPassed) non-passing cases." } } elseif ($Phase -eq 'AndroidBuild') { New-Item -ItemType Directory -Path (Split-Path $apkPath -Parent) -Force | Out-Null if (Test-Path -LiteralPath $apkPath) { Remove-Item -LiteralPath $apkPath } $env:MAGNETISM_APK_PATH = $apkPath $cliArgs = @('run', $projectRoot, '--timeout', '1800', '--log-file', $logPath, '--no-tail', '--', '-buildTarget', 'Android', '-executeMethod', 'MagnetismHeroes.Editor.ProjectBootstrap.BuildAndroid') $record.command = 'unity ' + (($cliArgs | ForEach-Object { '"' + $_ + '"' }) -join ' ') & unity @cliArgs $record.exitCode = $LASTEXITCODE 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 -SourceCommit $record.sourceCommit -SourceDirty $record.sourceDirty -MetadataName 'apk-metadata.json' $record.apkMetadata = Join-Path $evidenceDir 'apk-metadata.json' # Unity rewrites these files with LF; restore only when Git proves their content is unchanged. $record.normalizedUnchangedSettings = @() $record.settingsContentChecks = @() if (!$record.sourceDirty) { foreach ($settingsPath in @('ProjectSettings/GraphicsSettings.asset','ProjectSettings/ProjectAuditorSettings.asset','ProjectSettings/ProjectSettings.asset','ProjectSettings/ShaderGraphSettings.asset','Assets/Settings/Mobile_RPAsset.asset','Assets/Settings/UniversalRenderPipelineGlobalSettings.asset')) { $workingBlob = & git hash-object --path=$settingsPath -- $settingsPath $workingExit = $LASTEXITCODE $sourceBlob = & git rev-parse "HEAD:$settingsPath" $sourceExit = $LASTEXITCODE $record.settingsContentChecks += @{ path=$settingsPath; workingBlob=$workingBlob; sourceBlob=$sourceBlob; hashObjectExit=$workingExit; revParseExit=$sourceExit } if ($workingExit -eq 0 -and $sourceExit -eq 0 -and $workingBlob -eq $sourceBlob -and [bool](& git status --porcelain -- $settingsPath)) { & git restore --source=HEAD --worktree -- $settingsPath if ($LASTEXITCODE -ne 0) { throw "Cannot restore unchanged generated setting: $settingsPath" } $record.normalizedUnchangedSettings += $settingsPath } } } $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') } } else { $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 *> $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' } catch { $record.status = 'failed' $record.error = $_.Exception.Message Write-Host $record.error -ForegroundColor Red } finally { $record.finishedUtc = [DateTime]::UtcNow.ToString('o') $record | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $evidenceDir "$phaseName-evidence.json") -Encoding utf8 } if ($record.status -ne 'passed') { exit 1 } Write-Host "$Phase / $Milestone passed. Evidence: $evidenceDir" exit 0