Files
MagnetismHeroes/tools/verify.ps1
T

128 lines
9.3 KiB
PowerShell

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." }
}
# Edit/Play tests may strip this Editor-generated runtime list. Restore only that exact
# transformation after proving every other serialized byte (apart from CRLF) is unchanged.
if ($Phase -in @('EditMode','PlayMode') -and !$record.sourceDirty) {
$record.testSourceDirtyBeforeNormalization = [bool](& git status --porcelain)
$record.testGeneratedSettings = @()
foreach ($settingsPath in @('Assets/Settings/UniversalRenderPipelineGlobalSettings.asset','Assets/Settings/Mobile_RPAsset.asset','ProjectSettings/GraphicsSettings.asset','ProjectSettings/ProjectSettings.asset','ProjectSettings/EditorBuildSettings.asset','ProjectSettings/ProjectAuditorSettings.asset','ProjectSettings/ShaderGraphSettings.asset')) {
$workingBlob = & git hash-object --path=$settingsPath -- $settingsPath
$sourceBlob = & git rev-parse "HEAD:$settingsPath"
$same = $workingBlob -eq $sourceBlob
$runtimeListStripped = $false
if (!$same -and $settingsPath -eq 'Assets/Settings/UniversalRenderPipelineGlobalSettings.asset') {
$sourceText = ((& git show "HEAD:$settingsPath") -join "`n") + "`n"
$workingText = (Get-Content -LiteralPath $settingsPath -Raw).Replace("`r`n","`n")
$pattern = '(?m)^ m_RuntimeSettings:\n m_List:\n(?: - rid: \d+\n)+'
$expected = [regex]::Replace($sourceText,$pattern," m_RuntimeSettings:`n m_List: []`n")
$runtimeListStripped = $expected -ne $sourceText -and $expected -ceq $workingText
if ($runtimeListStripped) { (& git diff -- $settingsPath) | Set-Content -LiteralPath (Join-Path $evidenceDir "$phaseName-generated-runtime-list.diff") -Encoding utf8 }
}
if ($same -or $runtimeListStripped) {
if ([bool](& git status --porcelain -- $settingsPath)) {
& git restore --source=HEAD --worktree -- $settingsPath
if ($LASTEXITCODE -ne 0) { throw "Cannot normalize proven generated setting: $settingsPath" }
}
}
$record.testGeneratedSettings += @{path=$settingsPath;workingBlob=$workingBlob;sourceBlob=$sourceBlob;identicalContent=$same;exactRuntimeListStripping=$runtimeListStripped}
}
}
$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 verification or is dirty.' }
$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