10 'PrepareCancellation',
14 [string]$Phase = 'Build',
16 [string]$SourceRoot = (Get-Location).Path,
24 [string]$FirmwareModifiedNote,
26 [bool]$UseMethod8 = $true,
28 [string[]]$ExtraFriiDumpArgs = @(),
33 Set-StrictMode -Version Latest
34 $ErrorActionPreference = 'Stop'
36 $ExpectedVersion = '0.5.3.16-pf1-candidate10'
37 $ExpectedCommit = 'b03b227673ee00aec3c29d34038b3c86ee2d8b9a'
38 $ExpectedSchemaSha256 = 'd1e97cb3e88e0f5a94ccf9b9edf921554fc15278662e69420b8c1e564c600411'
40 $SourceRoot = (Resolve-Path -LiteralPath $SourceRoot).Path
41 if ([string]::IsNullOrWhiteSpace($ResultsRoot)) {
42 $ResultsRoot = Join-Path $SourceRoot '_native-report-live-validation'
44 $ResultsRoot = [System.IO.Path]::GetFullPath($ResultsRoot)
46 $Exe = Join-Path $SourceRoot 'friidump.exe'
47 $BuildScript = Join-Path $SourceRoot 'build_msvc32.cmd'
48 $FixtureBuildScript = Join-Path $SourceRoot 'build_msvc32_native_report_tests.cmd'
49 $Validator = Join-Path $SourceRoot 'tests\validate_native_reports.py'
50 $Schema = Join-Path $SourceRoot 'tests\contracts\friidump-test-result.v1.schema.json'
51 $SourceManifest = Join-Path $SourceRoot 'SOURCE_SHA256SUMS.txt'
52 $MainResponseFile = Join-Path $SourceRoot 'msvc32_friidump.rsp'
53 $FixtureResponseFile = Join-Path $SourceRoot 'msvc32_native_report_tests.rsp'
55 function Assert-True {
57 [Parameter(Mandatory)]
59 [Parameter(Mandatory)]
63 if (-not $Condition) {
68 function Assert-LastExitCode {
70 [Parameter(Mandatory)]
74 if ($LASTEXITCODE -ne 0) {
75 throw "$Operation failed with exit code $LASTEXITCODE."
80 param([Parameter(Mandatory)][string]$Path)
82 return (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant()
85 function Test-SourceManifest {
86 if (-not (Test-Path -LiteralPath $SourceManifest -PathType Leaf)) {
87 Write-Host '[INFO] SOURCE_SHA256SUMS.txt is not present; package-manifest validation skipped.'
92 foreach ($line in Get-Content -LiteralPath $SourceManifest) {
93 if ([string]::IsNullOrWhiteSpace($line)) {
96 if ($line -notmatch '^([0-9a-fA-F]{64}) (.+)$') {
97 throw "Malformed source-manifest line: $line"
100 $expected = $Matches[1].ToLowerInvariant()
101 $relative = $Matches[2] -replace '/', '\'
102 $path = Join-Path $SourceRoot $relative
103 Assert-True (Test-Path -LiteralPath $path -PathType Leaf) "Manifest file is missing: $relative"
104 $actual = Get-Sha256 $path
105 Assert-True ($actual -eq $expected) "Manifest SHA-256 mismatch: $relative"
109 Write-Host "Source manifest: PASS ($checked files)"
112 function Test-MsvcSplitLinkContract {
114 [Parameter(Mandatory)][string]$ResponsePath,
115 [Parameter(Mandatory)][string]$BuildScriptPath,
116 [Parameter(Mandatory)][string]$ExpectedInvocation,
117 [Parameter(Mandatory)][string]$Label
120 Assert-True (Test-Path -LiteralPath $ResponsePath -PathType Leaf) "$Label response file is missing: $ResponsePath"
121 Assert-True (Test-Path -LiteralPath $BuildScriptPath -PathType Leaf) "$Label build script is missing: $BuildScriptPath"
124 Get-Content -LiteralPath $ResponsePath |
125 ForEach-Object { $_.Trim() } |
126 Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
129 Assert-True (-not ($lines -icontains '/link')) "$Label response file must not contain /link. The linker boundary must be on the cl.exe command line."
130 Assert-True (-not ($lines -icontains 'ole32.lib')) "$Label response file must not contain ole32.lib. The library must follow /link on the cl.exe command line."
132 $buildText = Get-Content -Raw -LiteralPath $BuildScriptPath
133 Assert-True ($buildText.Contains($ExpectedInvocation)) "$Label build script is missing the required split compile/link invocation: $ExpectedInvocation"
135 Write-Host "$Label MSVC command-line linker boundary: PASS"
138 function Test-PythonValidatorAvailable {
139 & py -3 -c 'import jsonschema' 2>$null
140 if ($LASTEXITCODE -ne 0) {
141 throw 'Python 3 with jsonschema is required. Install it with: py -3 -m pip install jsonschema'
145 function Invoke-ReportValidation {
146 param([Parameter(Mandatory)][string]$ReportRoot)
148 Assert-True (Test-Path -LiteralPath $Validator -PathType Leaf) "Validator is missing: $Validator"
149 Assert-True (Test-Path -LiteralPath $Schema -PathType Leaf) "Schema is missing: $Schema"
150 Assert-True ((Get-Sha256 $Schema) -eq $ExpectedSchemaSha256) 'Bundled schema SHA-256 mismatch.'
152 Test-PythonValidatorAvailable
157 '--fixtures', $ReportRoot,
162 if (-not [string]::IsNullOrWhiteSpace($PhpValidator)) {
163 Assert-True (Test-Path -LiteralPath $PhpValidator -PathType Leaf) "PHP validator is missing: $PhpValidator"
164 $arguments += @('--php-validator', $PhpValidator)
168 Assert-LastExitCode 'Strict native-report validation'
171 function Get-NewReport {
173 [Parameter(Mandatory)][string]$ReportDirectory,
174 [Parameter(Mandatory)][datetime]$Started
178 Get-ChildItem -LiteralPath $ReportDirectory -Recurse -Filter '*.friidump.json' -File |
179 Where-Object { $_.LastWriteTimeUtc -ge $Started.ToUniversalTime().AddSeconds(-2) } |
180 Sort-Object LastWriteTimeUtc
183 Assert-True ($reports.Count -eq 1) "Expected exactly one new report; found $($reports.Count)."
187 function Read-Report {
188 param([Parameter(Mandatory)][string]$Path)
190 return Get-Content -Raw -LiteralPath $Path | ConvertFrom-Json
193 function Test-CommonReportIdentity {
195 [Parameter(Mandatory)]$Report,
196 [Parameter(Mandatory)][string]$ReportPath
199 Assert-True ($Report.schema -eq 'friidump-test-result.v1') "${ReportPath}: schema mismatch."
200 Assert-True ($Report.generator.name -eq 'FriiDump') "${ReportPath}: generator name mismatch."
201 Assert-True ($Report.generator.version -eq $ExpectedVersion) "${ReportPath}: generator version mismatch."
202 Assert-True ($Report.generator.build_commit -eq $ExpectedCommit) "${ReportPath}: build commit mismatch."
203 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.run.run_id)) "${ReportPath}: run UUID missing."
204 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.drive.model)) "${ReportPath}: drive model missing."
205 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.drive.firmware_revision)) "${ReportPath}: firmware revision missing."
207 if ([string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) {
208 Assert-True ($Report.drive.firmware_modified -eq $false) "${ReportPath}: omitted --firmware-modified must assume stock firmware."
209 Assert-True ($null -eq $Report.drive.modification_note) "${ReportPath}: stock firmware must not have a modification note."
212 Assert-True ($Report.drive.firmware_modified -eq $true) "${ReportPath}: modified-firmware flag was not recorded."
213 Assert-True ([string]$Report.drive.modification_note -eq $FirmwareModifiedNote) "${ReportPath}: modified-firmware explanation mismatch."
217 function Test-DumpOutputIdentity {
219 [Parameter(Mandatory)]$Report,
220 [Parameter(Mandatory)][string]$ExpectedOutput
223 $resolvedOutput = [System.IO.Path]::GetFullPath($ExpectedOutput)
224 Assert-True (Test-Path -LiteralPath $resolvedOutput -PathType Leaf) "Dump output is missing: $resolvedOutput"
226 $actualBytes = (Get-Item -LiteralPath $resolvedOutput).Length
227 Assert-True ([int64]$Report.dump.byte_count -eq $actualBytes) 'Report byte count does not match the output file.'
228 Assert-True ([string]$Report.dump.output_path -eq $resolvedOutput) 'Report output path does not match the requested path.'
230 $artifacts = @($Report.artifacts | Where-Object { $_.type -eq 'dump_output' })
231 Assert-True ($artifacts.Count -eq 1) 'Exactly one dump_output artifact is required.'
232 Assert-True ([string]$artifacts[0].path -eq $resolvedOutput) 'dump_output artifact path mismatch.'
233 Assert-True ([int64]$artifacts[0].bytes -eq $actualBytes) 'dump_output artifact byte count mismatch.'
235 if ($null -ne $Report.hashes.sha256) {
236 $actualSha = Get-Sha256 $resolvedOutput
237 Assert-True ([string]$Report.hashes.sha256 -eq $actualSha) 'Report SHA-256 does not match the output file.'
238 Assert-True ([string]$artifacts[0].sha256 -eq $actualSha) 'Artifact SHA-256 does not match the output file.'
242 function New-RunDirectory {
243 param([Parameter(Mandatory)][string]$Name)
245 $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')
246 $path = Join-Path $ResultsRoot "$stamp-$Name"
247 New-Item -ItemType Directory -Force -Path $path | Out-Null
248 New-Item -ItemType Directory -Force -Path (Join-Path $path 'reports') | Out-Null
252 function Invoke-FriiDumpRun {
254 [Parameter(Mandatory)][string[]]$Arguments,
255 [Parameter(Mandatory)][string]$RunDirectory,
256 [Parameter(Mandatory)][bool]$ExpectSuccess
259 $started = (Get-Date).ToUniversalTime()
261 Write-Host "Working directory: $RunDirectory"
262 Write-Host "Command: $Exe $($Arguments -join ' ')"
263 Write-Host 'Console mode: direct live FriiDump output; FriiDump native .log is the evidence log.'
265 # Do not route an interactive FriiDump run through a PowerShell
266 # pipeline. Tee-Object buffers carriage-return progress updates, turns
267 # native stderr into PowerShell error records, and also contaminates a
268 # function's return stream with every line emitted by the child.
270 # Start FriiDump with inherited console handles instead. This preserves
271 # real-time progress output and leaves this function with exactly one
273 function ConvertTo-WindowsCommandLineArgument {
274 param([AllowEmptyString()][string]$Value)
276 if ($Value.Length -gt 0 -and $Value -notmatch '[\s"]') {
280 $builder = New-Object System.Text.StringBuilder
281 [void]$builder.Append('"')
284 foreach ($character in $Value.ToCharArray()) {
285 if ($character -eq '\') {
290 if ($character -eq '"') {
291 [void]$builder.Append(('\' * (($backslashes * 2) + 1)))
292 [void]$builder.Append('"')
297 if ($backslashes -gt 0) {
298 [void]$builder.Append(('\' * $backslashes))
302 [void]$builder.Append($character)
305 if ($backslashes -gt 0) {
306 [void]$builder.Append(('\' * ($backslashes * 2)))
309 [void]$builder.Append('"')
310 return $builder.ToString()
316 ConvertTo-WindowsCommandLineArgument ([string]$_)
320 $startInfo = New-Object System.Diagnostics.ProcessStartInfo
321 $startInfo.FileName = $Exe
322 $startInfo.Arguments = $argumentLine
323 $startInfo.WorkingDirectory = $RunDirectory
324 $startInfo.UseShellExecute = $false
325 $startInfo.CreateNoWindow = $false
326 $startInfo.RedirectStandardOutput = $false
327 $startInfo.RedirectStandardError = $false
329 $process = New-Object System.Diagnostics.Process
330 $process.StartInfo = $startInfo
333 [void]$process.Start()
334 $process.WaitForExit()
335 $exitCode = $process.ExitCode
341 Set-Content -LiteralPath (Join-Path $RunDirectory 'exit-code.txt') -Value $exitCode -Encoding ASCII
343 if ($ExpectSuccess) {
344 Assert-True ($exitCode -eq 0) "FriiDump was expected to succeed but exited with $exitCode."
347 return [pscustomobject]@{
349 ExitCode = [int]$exitCode
354 function Add-ModifiedFirmwareArgument {
355 param([string[]]$Arguments)
357 if (-not [string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) {
358 return $Arguments + @('--firmware-modified', $FirmwareModifiedNote)
363 New-Item -ItemType Directory -Force -Path $ResultsRoot | Out-Null
367 Assert-True (Test-Path -LiteralPath $BuildScript -PathType Leaf) "Build script is missing: $BuildScript"
368 Assert-True (Test-Path -LiteralPath $FixtureBuildScript -PathType Leaf) "Fixture build script is missing: $FixtureBuildScript"
371 $XboxRegionHeader = Join-Path $SourceRoot 'libfriidump\xbox_region.h'
372 $XboxLegacyUtilsHeader = Join-Path $SourceRoot 'libfriidump\xbox_ref\utils.h'
373 $XboxRegionHeaderText = Get-Content -Raw -LiteralPath $XboxRegionHeader
374 $XboxLegacyUtilsText = Get-Content -Raw -LiteralPath $XboxLegacyUtilsHeader
376 Assert-True (-not $XboxRegionHeaderText.Contains('<stdbool.h>')) `
377 'xbox_region.h must not introduce stdbool.h into the copied Xbox header graph.'
378 Assert-True ($XboxRegionHeaderText.Contains('int xbox_region_format(')) `
379 'xbox_region.h must expose the MSVC-safe int return type.'
380 Assert-True ($XboxLegacyUtilsText.Contains('typedef int bool;')) `
381 'The copied Xbox legacy bool declaration changed unexpectedly.'
382 Write-Host 'Xbox region/MSVC bool isolation: PASS'
384 $XboxCancelContract = Join-Path $SourceRoot 'tests\test_xbox_cancel_contract.py'
385 Assert-True (Test-Path -LiteralPath $XboxCancelContract -PathType Leaf) `
386 'Xbox copied-dumper cancellation contract test is missing.'
387 & py -3 $XboxCancelContract --source-root $SourceRoot
388 Assert-LastExitCode 'Xbox copied-dumper cancellation contract'
390 Test-MsvcSplitLinkContract `
391 -ResponsePath $MainResponseFile `
392 -BuildScriptPath $BuildScript `
393 -ExpectedInvocation 'cl @"%EFFECTIVE_RSP%" /link ole32.lib' `
394 -Label 'FriiDump executable'
396 Test-MsvcSplitLinkContract `
397 -ResponsePath $FixtureResponseFile `
398 -BuildScriptPath $FixtureBuildScript `
399 -ExpectedInvocation 'cl @"%~dp0msvc32_native_report_tests.rsp" /link ole32.lib' `
400 -Label 'Native-report fixtures'
402 $env:FRIIDUMP_BUILD_COMMIT = $ExpectedCommit
405 Assert-LastExitCode 'MSVC clean'
408 Assert-LastExitCode 'MSVC candidate build'
410 & $FixtureBuildScript
411 Assert-LastExitCode 'MSVC native-report fixture validation'
413 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'friidump.exe was not produced.'
415 # Windows PowerShell 5.1 converts native stderr records into
416 # NativeCommandError objects when $ErrorActionPreference is Stop.
417 # FriiDump intentionally writes part of its startup/help text to
418 # stderr, so capture both streams through ProcessStartInfo instead
419 # of invoking the executable directly through the PowerShell
421 $helpStartInfo = New-Object System.Diagnostics.ProcessStartInfo
422 $helpStartInfo.FileName = $Exe
423 $helpStartInfo.Arguments = '--help'
424 $helpStartInfo.UseShellExecute = $false
425 $helpStartInfo.RedirectStandardOutput = $true
426 $helpStartInfo.RedirectStandardError = $true
427 $helpStartInfo.CreateNoWindow = $true
429 $helpProcess = New-Object System.Diagnostics.Process
430 $helpProcess.StartInfo = $helpStartInfo
433 [void]$helpProcess.Start()
435 # Drain stdout and stderr concurrently. Reading either redirected
436 # stream synchronously before the other can deadlock when the
437 # second pipe fills while the process is still running.
438 $helpStdoutTask = $helpProcess.StandardOutput.ReadToEndAsync()
439 $helpStderrTask = $helpProcess.StandardError.ReadToEndAsync()
441 $helpProcess.WaitForExit()
443 $helpStdout = $helpStdoutTask.GetAwaiter().GetResult()
444 $helpStderr = $helpStderrTask.GetAwaiter().GetResult()
445 $helpExitCode = $helpProcess.ExitCode
448 $helpProcess.Dispose()
454 ) -join [Environment]::NewLine
456 Assert-True ($helpExitCode -eq 1) "Candidate help command returned unexpected exit code $helpExitCode; FriiDump historically returns 1 for --help."
457 Assert-True ($help.Contains("FriiDump $ExpectedVersion")) 'Candidate version is missing from help output.'
458 foreach ($option in @('--report-json', '--report-dir', '--firmware-modified')) {
459 Assert-True ($help.Contains($option)) "Candidate help is missing $option."
461 Assert-True ($help.Contains('Native .friidump.json reports are created by default')) 'Default native-report behavior is missing from help output.'
462 Assert-True ($help.Contains('beside the final log')) 'Default beside-log report placement is missing from help output.'
463 Assert-True ($help.Contains('(may be combined with --report-json)')) 'Combined --report-json/--report-dir behavior is missing from help output.'
464 Assert-True ($help.Contains('omission assumes stock')) 'Stock-firmware default is missing from help output.'
467 "Generated UTC: $((Get-Date).ToUniversalTime().ToString('o'))",
468 "Version: $ExpectedVersion",
469 "Commit: $ExpectedCommit",
471 "Executable SHA-256: $(Get-Sha256 $Exe)",
473 'MSVC native-report fixtures: PASS',
474 'Schema and semantic validation: PASS (13 reports)',
475 'Help/version/options/default-location smoke: PASS',
476 'Xbox copied-dumper cancellation, hashing, and summary contract: PASS'
478 $recordPath = Join-Path $ResultsRoot 'build-validation.txt'
479 Set-Content -LiteralPath $recordPath -Value $buildRecord -Encoding UTF8
482 Write-Host 'FRIIDUMP CANDIDATE WINDOWS BUILD: PASS'
483 Write-Host "Build record: $recordPath"
488 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.'
489 Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.'
491 Read-Host "Remove all media from $Drive, wait for the drive to settle, then press Enter"
492 $runDir = New-RunDirectory 'no-media-default-report'
493 $output = Join-Path $runDir 'no-media-placeholder.iso'
494 $args = @('-d', $Drive, '-i', $output)
495 $args = Add-ModifiedFirmwareArgument $args
496 $args += $ExtraFriiDumpArgs
498 $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $false
499 $reportFile = Get-NewReport -ReportDirectory $runDir -Started $run.Started
500 Assert-True ($reportFile.Name -eq 'no-media-placeholder.iso.friidump.json') "Default report filename mismatch: $($reportFile.Name)"
501 Assert-True ($reportFile.DirectoryName -eq $runDir) "Default no-media report was not placed beside the final log/output path."
502 Invoke-ReportValidation $runDir
503 $report = Read-Report $reportFile.FullName
504 Test-CommonReportIdentity $report $reportFile.FullName
506 Assert-True ($report.run.test_type -eq 'diagnostic_no_media') 'No-media test_type mismatch.'
507 Assert-True ($report.run.result -eq 'not_applicable') 'No-media run result mismatch.'
508 Assert-True ($report.dump.attempted -eq $false) 'No-media report must not claim a dump.'
509 Assert-True (-not (Test-Path -LiteralPath $output)) 'No-media test unexpectedly created an output image.'
512 Write-Host 'NO-MEDIA DEFAULT NATIVE REPORT: PASS'
513 Write-Host "Report: $($reportFile.FullName)"
518 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.'
519 Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.'
521 $runDir = New-RunDirectory 'gamecube-full-dump'
522 if ([string]::IsNullOrWhiteSpace($OutputName)) {
523 $OutputName = 'Sonic Mega Collection.iso'
525 $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName))
526 $args = @('-d', $Drive)
530 $args += @('-i', $output, '--report-dir', $reportDir)
531 $args = Add-ModifiedFirmwareArgument $args
532 $args += $ExtraFriiDumpArgs
534 $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true
535 $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started
536 Invoke-ReportValidation $reportDir
537 $report = Read-Report $reportFile.FullName
538 Test-CommonReportIdentity $report $reportFile.FullName
539 Test-DumpOutputIdentity $report $output
541 Assert-True ($report.media.platform -eq 'gamecube') 'Expected a GameCube report.'
542 Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'GameCube run outcome mismatch.'
543 Assert-True ($report.dump.result -eq 'pass') 'GameCube dump result mismatch.'
544 Assert-True (@($report.notes) -contains 'Measurement scope: full_optical_payload.') 'GameCube measurement scope mismatch.'
547 Write-Host 'GAMECUBE FULL-DUMP NATIVE REPORT: PASS'
548 Write-Host "Report: $($reportFile.FullName)"
553 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.'
554 Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.'
556 $runDir = New-RunDirectory 'xbox-redump-iso'
557 $reportDir = Join-Path $runDir 'reports'
558 if ([string]::IsNullOrWhiteSpace($OutputName)) {
559 $OutputName = 'Red Faction II [TQ00501A].iso'
561 $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName))
562 $args = @('-d', $Drive, '-T', '4', '-i', $output, '--report-dir', $reportDir)
563 $args = Add-ModifiedFirmwareArgument $args
564 $args += $ExtraFriiDumpArgs
566 $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true
567 $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started
568 Invoke-ReportValidation $reportDir
569 $report = Read-Report $reportFile.FullName
570 Test-CommonReportIdentity $report $reportFile.FullName
571 Test-DumpOutputIdentity $report $output
573 Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.'
574 Assert-True ($report.media.region -eq 'North America') 'Expected the XBE GameRegion to report North America.'
575 Assert-True (@($report.notes) -contains 'Xbox XBE GameRegion mask: 0x00000001.') 'Expected the raw XBE GameRegion mask note.'
576 Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'Xbox ISO run outcome mismatch.'
577 Assert-True (@($report.notes) -contains 'Measurement scope: assembled_output.') 'Xbox ISO measurement scope mismatch.'
580 Write-Host 'XBOX REDUMP-STYLE ISO NATIVE REPORT: PASS'
581 Write-Host "Report: $($reportFile.FullName)"
586 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.'
587 Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.'
589 $runDir = New-RunDirectory 'xbox-xiso'
590 $reportDir = Join-Path $runDir 'reports'
591 if ([string]::IsNullOrWhiteSpace($OutputName)) {
592 $OutputName = 'Red Faction II [TQ00501A].xiso'
594 $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName))
595 $args = @('-d', $Drive, '-T', '4', '-X', $output, '--report-dir', $reportDir)
596 $args = Add-ModifiedFirmwareArgument $args
597 $args += $ExtraFriiDumpArgs
599 $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true
600 $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started
601 Invoke-ReportValidation $reportDir
602 $report = Read-Report $reportFile.FullName
603 Test-CommonReportIdentity $report $reportFile.FullName
604 Test-DumpOutputIdentity $report $output
606 Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.'
607 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.media.region)) 'Expected an XBE-derived Xbox region.'
608 Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'Xbox XISO run outcome mismatch.'
609 Assert-True (@($report.notes) -contains 'Measurement scope: assembled_output.') 'Xbox XISO measurement scope mismatch.'
612 Write-Host 'XBOX XISO NATIVE REPORT: PASS'
613 Write-Host "Report: $($reportFile.FullName)"
618 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.'
619 Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.'
621 $runDir = New-RunDirectory 'xbox-controlled-cancellation'
622 $reportDir = Join-Path $runDir 'reports'
623 if ([string]::IsNullOrWhiteSpace($OutputName)) {
624 $OutputName = 'Red Faction II [TQ00501A].xiso'
626 $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName))
627 $args = @('-d', $Drive, '-T', '4', '-X', $output, '--report-dir', $reportDir)
628 $args = Add-ModifiedFirmwareArgument $args
629 $args += $ExtraFriiDumpArgs
632 Write-Host 'After Regions: North America appears and XISO progress begins, press Ctrl+C once.'
635 $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $false
636 $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started
637 Invoke-ReportValidation $reportDir
638 $report = Read-Report $reportFile.FullName
639 Test-CommonReportIdentity $report $reportFile.FullName
640 Test-DumpOutputIdentity $report $output
642 Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.'
643 Assert-True ($report.media.region -eq 'North America') 'Expected the XBE GameRegion to report North America.'
644 Assert-True (@($report.notes) -contains 'Xbox XBE GameRegion mask: 0x00000001.') 'Expected the raw XBE GameRegion mask note.'
645 Assert-True ($report.run.test_type -eq 'partial_dump' -and $report.run.result -eq 'partial') 'Xbox cancellation run outcome mismatch.'
646 Assert-True ($report.dump.result -eq 'partial') 'Xbox cancellation dump result mismatch.'
647 Assert-True ($report.dump.failure_stage -eq 'user_cancelled') 'Xbox cancellation failure stage mismatch.'
648 Assert-True ([int64]$report.dump.sector_count -gt 32) 'Xbox cancellation did not record meaningful partial progress.'
649 Assert-True ([int64]$report.dump.byte_count -eq ([int64]$report.dump.sector_count * 2048)) 'Xbox cancellation byte/sector accounting mismatch.'
650 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.crc32)) 'Partial Xbox CRC32 is missing.'
651 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.md5)) 'Partial Xbox MD5 is missing.'
652 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.sha1)) 'Partial Xbox SHA-1 is missing.'
653 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.sha256)) 'Partial Xbox SHA-256 is missing.'
655 $nativeLog = "$output.log"
656 Assert-True (Test-Path -LiteralPath $nativeLog -PathType Leaf) "Xbox native log is missing: $nativeLog"
657 $logText = Get-Content -Raw -LiteralPath $nativeLog
658 foreach ($expectedText in @(
659 'Game/Media ID.....: TQ00501A',
660 'Title.............: Red Faction',
661 'Region............: North America',
662 'Seed read.........: N/A (Xbox reference auth path)',
663 'Source sectors....: 3431264',
664 'Expected output...: 1913920 sectors',
665 '[OK] Controlled partial Xbox output hashes captured.'
667 Assert-True ($logText.Contains($expectedText)) "Xbox summary/log is missing: $expectedText"
671 Write-Host 'XBOX REGION, SUMMARY, GEOMETRY, CANCELLATION, AND HASHING: PASS'
672 Write-Host "Report: $($reportFile.FullName)"
676 'PrepareCancellation' {
677 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.'
678 Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.'
680 $runDir = New-RunDirectory 'controlled-cancellation'
681 if ([string]::IsNullOrWhiteSpace($OutputName)) {
682 $OutputName = 'controlled-cancellation.partial.iso'
684 $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName))
685 $wrapper = Join-Path $runDir 'run-controlled-cancellation.ps1'
687 $argLines = @("'-d'", "'$($Drive.Replace("'", "''"))'")
692 "'-i'", "'$($output.Replace("'", "''"))'"
694 if (-not [string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) {
695 $argLines += @("'--firmware-modified'", "'$($FirmwareModifiedNote.Replace("'", "''"))'")
697 foreach ($arg in $ExtraFriiDumpArgs) {
698 $argLines += "'$($arg.Replace("'", "''"))'"
702 `$ErrorActionPreference = 'Stop'
703 `$exe = '$($Exe.Replace("'", "''"))'
705 $($argLines -join ",`r`n ")
707 Write-Host 'Allow data to be written, then press Ctrl+C once.'
711 Set-Content -LiteralPath $wrapper -Value $wrapperText -Encoding UTF8
714 Write-Host 'CONTROLLED CANCELLATION TEST PREPARED'
715 Write-Host 'Run this wrapper in a separate PowerShell window:'
716 Write-Host "powershell -NoProfile -ExecutionPolicy Bypass -File `"$wrapper`""
718 Write-Host 'After the dump has written data, press Ctrl+C once. Then validate all generated reports with:'
719 Write-Host "powershell -NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -Phase ValidateReports -SourceRoot `"$SourceRoot`" -ResultsRoot `"$ResultsRoot`""
721 Write-Host 'Expected cancellation report: beside the final .log, partial_dump / partial, failure_stage=user_cancelled, scope=partial_progress.'
722 Write-Host 'The copied GDR-8050L Windows reference-dumper path is not yet an approved cancellation target.'
727 $reports = @(Get-ChildItem -LiteralPath $ResultsRoot -Recurse -Filter '*.friidump.json' -File)
728 Assert-True ($reports.Count -gt 0) "No native reports were found under $ResultsRoot."
729 Invoke-ReportValidation $ResultsRoot
732 foreach ($file in $reports) {
733 $report = Read-Report $file.FullName
734 Test-CommonReportIdentity $report $file.FullName
735 $runId = [string]$report.run.run_id
736 Assert-True (-not $runIds.ContainsKey($runId)) "Duplicate run UUID: $runId"
737 $runIds[$runId] = $true
739 if ($report.dump.attempted -and $null -ne $report.dump.output_path) {
740 Test-DumpOutputIdentity $report ([string]$report.dump.output_path)
745 $reports | Where-Object {
746 $document = Read-Report $_.FullName
747 $document.dump.failure_stage -eq 'user_cancelled'
750 foreach ($file in $cancelReports) {
751 $report = Read-Report $file.FullName
752 Assert-True ($report.run.test_type -eq 'partial_dump') "$($file.FullName): cancellation test_type mismatch."
753 Assert-True ($report.run.result -eq 'partial') "$($file.FullName): cancellation run result mismatch."
754 Assert-True ($report.dump.result -eq 'partial') "$($file.FullName): cancellation dump result mismatch."
755 Assert-True (@($report.notes) -contains 'Measurement scope: partial_progress.') "$($file.FullName): cancellation scope mismatch."
757 $outputPath = [System.IO.Path]::GetFullPath([string]$report.dump.output_path)
758 $finalLogPath = "$outputPath.log"
759 $reportParent = [System.IO.Path]::GetFullPath($file.DirectoryName).TrimEnd([char[]]"\/")
760 $logParent = [System.IO.Path]::GetFullPath((Split-Path -Parent $finalLogPath)).TrimEnd([char[]]"\/")
762 Assert-True ($reportParent -eq $logParent) "$($file.FullName): default report is not beside the final FriiDump log."
763 Assert-True (Test-Path -LiteralPath $finalLogPath -PathType Leaf) "$($file.FullName): final FriiDump log is missing: $finalLogPath"
765 $logText = Get-Content -Raw -LiteralPath $finalLogPath
766 Assert-True ($logText.Contains('Dump status.......: CANCELLED (PARTIAL)')) "${finalLogPath}: corrected cancellation status is missing."
767 Assert-True ($logText.Contains("Cancelled at......: sector $($report.dump.failure_sector)")) "${finalLogPath}: corrected cancellation-sector line is missing."
768 Assert-True ($logText.Contains("Completed sectors.: $($report.dump.sector_count)")) "${finalLogPath}: completed-sector line is missing or inconsistent."
769 Assert-True ($logText.Contains("Completed bytes...: $($report.dump.byte_count)")) "${finalLogPath}: completed-byte line is missing or inconsistent."
770 Assert-True (-not $logText.Contains('MiB ISO payload')) "${finalLogPath}: obsolete full-payload throughput wording remains."
772 $averageMatch = [regex]::Match(
774 'Observed average\.\.: ([0-9]+(?:\.[0-9]+)?) MiB/h over ([0-9]+(?:\.[0-9]+)?) MiB completed output'
776 Assert-True ($averageMatch.Success) "${finalLogPath}: corrected completed-output throughput line is missing."
778 $culture = [System.Globalization.CultureInfo]::InvariantCulture
779 $observedRate = [double]::Parse($averageMatch.Groups[1].Value, $culture)
780 $observedMiB = [double]::Parse($averageMatch.Groups[2].Value, $culture)
781 $expectedMiB = [double]$report.dump.byte_count / 1MB
782 $expectedRate = $expectedMiB / [double]$report.dump.duration_seconds * 3600.0
784 Assert-True ([Math]::Abs($observedMiB - $expectedMiB) -lt 0.01) "${finalLogPath}: completed MiB does not match the report byte count."
785 Assert-True ([Math]::Abs($observedRate - $expectedRate) -lt 0.05) "${finalLogPath}: observed average does not use completed bytes and report duration."
787 # Verify that the native report and human-readable summary agree
788 # with the authoritative Nintendo platform magic in sector 0.
789 # This catches HLDS drives that accept out-of-range dummy reads
790 # and therefore defeat the legacy capacity-only classifier.
791 $header = New-Object byte[] 32
792 $stream = [System.IO.File]::OpenRead($outputPath)
795 $headerBytes = $stream.Read($header, 0, $header.Length)
801 if ($headerBytes -ge 32) {
803 ([uint32]$header[0x18] -shl 24) -bor
804 ([uint32]$header[0x19] -shl 16) -bor
805 ([uint32]$header[0x1a] -shl 8) -bor
806 [uint32]$header[0x1b]
810 ([uint32]$header[0x1c] -shl 24) -bor
811 ([uint32]$header[0x1d] -shl 16) -bor
812 ([uint32]$header[0x1e] -shl 8) -bor
813 [uint32]$header[0x1f]
816 if ($wiiMagic -eq [uint32]0x5d1c9ea3) {
818 $report.media.platform -eq 'wii'
819 ) "${finalLogPath}: Wii header magic does not match the native-report platform."
821 elseif ($gameCubeMagic -eq [uint32]0xc2339f3d) {
823 $report.media.platform -eq 'gamecube'
824 ) "${finalLogPath}: GameCube header magic does not match the native-report platform."
827 $logText.Contains('Disc type..........: GameCube')
828 ) "${finalLogPath}: GameCube header magic does not match the human-readable disc type."
831 $logText.Contains('Expected sectors..: 712880')
832 ) "${finalLogPath}: GameCube header magic does not match the expected-sector geometry."
838 Write-Host "LIVE NATIVE REPORT VALIDATION: PASS ($($reports.Count) reports)"
839 Write-Host "Controlled cancellation reports: $($cancelReports.Count)"
844 Assert-True (Test-Path -LiteralPath $ResultsRoot -PathType Container) "Results directory is missing: $ResultsRoot"
845 $destination = "$ResultsRoot-results.zip"
846 if (Test-Path -LiteralPath $destination) {
847 Remove-Item -LiteralPath $destination -Force
849 Compress-Archive -Path (Join-Path $ResultsRoot '*') -DestinationPath $destination -CompressionLevel Optimal
850 Write-Host 'VALIDATION RESULT COLLECTION: PASS'
851 Write-Host "Archive: $destination"
852 Write-Host "Bytes: $((Get-Item -LiteralPath $destination).Length)"
853 Write-Host "SHA-256: $(Get-Sha256 $destination)"