# 阶段 2 回归:把阶段 1 的 11 条黑盒用例按**修正后的前置条件**重跑一遍。 # 修正点: # * BB-11 原用 -DryRun,而孤儿审计只在真实运行里报告 -> 改成真实运行(原用例的夹具错误,不是产品缺陷) # * BB-06 的期望不变(显式清单不存在必须非 0),验证修复是否生效 param([string]$OutJson = "$PSScriptRoot\blackbox_regression.json") $ErrorActionPreference = 'Continue' $repo = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path $host7 = (Get-Command pwsh).Source $host51 = (Get-Command powershell).Source $zip = (Get-Command 7z -ErrorAction SilentlyContinue).Source if (-not $zip) { foreach ($p in 'C:\Programs\Scoop\shims\7z.exe') { if (Test-Path $p) { $zip = $p } } } $sandbox = Join-Path $env:TEMP ('baknret-reg-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) $src = Join-Path $sandbox 'srcdocs' New-Item -ItemType Directory -Path (Join-Path $src 'nested') -Force | Out-Null Set-Content -LiteralPath (Join-Path $src 'a.txt') -Value 'alpha' -Encoding utf8 Set-Content -LiteralPath (Join-Path $src 'nested\b.txt') -Value 'beta' -Encoding utf8 Set-Content -LiteralPath (Join-Path $src 'nested\skipme.log') -Value 'noise' -Encoding utf8 $listPath = Join-Path $sandbox 'BackupList.txt' Set-Content -LiteralPath $listPath -Encoding utf8 -Value @( "$src :- 'nested\skipme.log'", '+ ' + (Join-Path $sandbox 'nonexistent') ) $backupDir = Join-Path $sandbox 'Backups' $cases = [System.Collections.Generic.List[object]]::new() function Add-Case { param([string]$Id, [string]$Title, [string]$Expected, [string]$Actual, [bool]$Pass, [string]$Note = '') $cases.Add([pscustomobject]@{ Id = $Id; Title = $Title; Expected = $Expected; Actual = $Actual Status = $(if ($Pass) { 'PASS' } else { 'FAIL' }); Note = $Note }) } function Invoke-Bak { param([string]$HostName, [string[]]$Arguments, [string]$Script = 'Backup-Data.ps1') $exe = if ($HostName -eq '7') { $host7 } else { $host51 } $all = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', (Join-Path $repo $Script)) + $Arguments $out = & $exe @all 2>&1 return [pscustomobject]@{ ExitCode = $LASTEXITCODE; Output = @($out) } } # ---------------------------------------------------------------- BB-01 真实备份 $r = Invoke-Bak -HostName 7 -Arguments @('-BackupListPath', $listPath, '-BackupDir', $backupDir) $archives = @(Get-ChildItem -LiteralPath $backupDir -Filter '*.7z' -ErrorAction SilentlyContinue) Add-Case -Id 'BB-01' -Title '真实备份:退出码 0 + 产出归档 + manifest' ` -Expected '退出码 0;≥1 个 .7z;manifest.json 存在' ` -Actual ("退出码={0};归档={1}" -f $r.ExitCode, $archives.Count) ` -Pass ($r.ExitCode -eq 0 -and $archives.Count -ge 1 -and (Test-Path (Join-Path $backupDir 'manifest.json'))) # ---------------------------------------------------------------- BB-02 排除生效 $listing = if ($archives.Count -gt 0 -and $zip) { @(& $zip l -ba $archives[0].FullName 2>&1) } else { @() } $hasSkip = @($listing | Where-Object { $_ -match 'skipme\.log' }).Count -gt 0 $hasKeep = @($listing | Where-Object { $_ -match 'a\.txt|b\.txt' }).Count -gt 0 Add-Case -Id 'BB-02' -Title '排除模式真的把内容挡在归档之外' ` -Expected '归档内无 skipme.log,有 a.txt/b.txt' ` -Actual ("skipme={0};keep={1}" -f $hasSkip, $hasKeep) -Pass ($hasKeep -and -not $hasSkip) # ---------------------------------------------------------------- BB-03 干跑不写盘 $h1 = (Get-FileHash -LiteralPath (Join-Path $backupDir 'manifest.json') -Algorithm SHA256).Hash $r = Invoke-Bak -HostName 7 -Arguments @('-DryRun', '-BackupListPath', $listPath, '-BackupDir', $backupDir) $h2 = (Get-FileHash -LiteralPath (Join-Path $backupDir 'manifest.json') -Algorithm SHA256).Hash Add-Case -Id 'BB-03' -Title '-DryRun 一个字节都不写' ` -Expected '退出码 0;manifest SHA256 前后一致' ` -Actual ("退出码={0};哈希一致={1}" -f $r.ExitCode, ($h1 -eq $h2)) -Pass ($r.ExitCode -eq 0 -and $h1 -eq $h2) # ---------------------------------------------------------------- BB-04 missing-source $actions = @((Get-Content (Join-Path $backupDir 'manifest.json') -Raw | ConvertFrom-Json).items.PSObject.Properties.Value.action) Add-Case -Id 'BB-04' -Title '源不存在记 missing-source,不算失败' ` -Expected '含 missing-source;退出码 0' ` -Actual ("actions={0};退出码={1}" -f ($actions -join ','), $r.ExitCode) ` -Pass ($actions -contains 'missing-source' -and $r.ExitCode -eq 0) # ---------------------------------------------------------------- BB-05 -Only $r = Invoke-Bak -HostName 7 -Arguments @('-DryRun', '-Only', 'srcdocs', '-BackupListPath', $listPath, '-BackupDir', $backupDir) $hitMissing = @($r.Output | Where-Object { $_ -match 'nonexistent' }).Count -gt 0 Add-Case -Id 'BB-05' -Title '-Only 只处理匹配的条目' ` -Expected '退出码 0;输出不提未选中的 nonexistent' ` -Actual ("退出码={0};提到 nonexistent={1}" -f $r.ExitCode, $hitMissing) -Pass ($r.ExitCode -eq 0 -and -not $hitMissing) # ---------------------------------------------------------------- BB-06 ★ 修复验证 $ghost = Join-Path $sandbox 'typo-in-path.txt' $r = Invoke-Bak -HostName 7 -Arguments @('-BackupListPath', $ghost, '-BackupDir', $backupDir) $created = Test-Path -LiteralPath $ghost Add-Case -Id 'BB-06' -Title '★显式指定的清单不存在 -> 必须报失败(本次修复点)' ` -Expected '退出码 ≠ 0;不创建模板;输出含"指定的清单不存在"' ` -Actual ("退出码={0};误建模板={1};提示={2}" -f $r.ExitCode, $created, (@($r.Output | Where-Object { $_ -match '指定的清单不存在' }).Count -gt 0)) ` -Pass ($r.ExitCode -ne 0 -and -not $created -and (@($r.Output | Where-Object { $_ -match '指定的清单不存在' }).Count -gt 0)) ` -Note '阶段 1 原为 FAIL(退出码 0 且建了模板),本次修复后应转为 PASS' # ---------------------------------------------------------------- BB-06b 对照:首次运行引导仍在 $realList = Join-Path $repo 'BackupList.txt' $parked = Join-Path $sandbox 'BackupList.parked' $guideDir = Join-Path $sandbox 'guide' New-Item -ItemType Directory -Path $guideDir -Force | Out-Null $r = $null try { Move-Item -LiteralPath $realList -Destination $parked -Force $r = Invoke-Bak -HostName 7 -Arguments @('-BackupDir', (Join-Path $guideDir 'Backups')) $regenerated = Test-Path -LiteralPath $realList } finally { if (Test-Path -LiteralPath $parked) { Move-Item -LiteralPath $parked -Destination $realList -Force } } Add-Case -Id 'BB-06b' -Title '对照:未显式指定时首次运行仍建模板并退出 0' ` -Expected '退出码 0;模板被创建;随后仓库原文件已还原' ` -Actual ("退出码={0};模板创建={1};仓库还原={2}" -f $r.ExitCode, $regenerated, (Test-Path -LiteralPath $realList)) ` -Pass ($r.ExitCode -eq 0 -and $regenerated -and (Test-Path -LiteralPath $realList)) # ---------------------------------------------------------------- BB-07 垫片转发 $r = Invoke-Bak -HostName 7 -Arguments @('-DryRun', '-BackupListPath', $listPath, '-BackupDir', $backupDir) -Script 'Backup.ps1' $said = @($r.Output | Where-Object { $_ -match '已改名为' }).Count -gt 0 Add-Case -Id 'BB-07' -Title '旧名字垫片转发且退出码原样传递' ` -Expected '退出码 0;有改名提示' ` -Actual ("退出码={0};提示={1}" -f $r.ExitCode, $said) -Pass ($r.ExitCode -eq 0 -and $said) # ---------------------------------------------------------------- BB-08 双宿主 $r7 = Invoke-Bak -HostName 7 -Arguments @('-DryRun', '-BackupListPath', $listPath, '-BackupDir', $backupDir) $r51 = Invoke-Bak -HostName '5.1' -Arguments @('-DryRun', '-BackupListPath', $listPath, '-BackupDir', $backupDir) Add-Case -Id 'BB-08' -Title '5.1 与 7.x 行为一致' ` -Expected '两个宿主退出码都是 0' ` -Actual ("7={0};5.1={1}" -f $r7.ExitCode, $r51.ExitCode) -Pass ($r7.ExitCode -eq 0 -and $r51.ExitCode -eq 0) # ---------------------------------------------------------------- BB-09 特殊字符 $sp = Join-Path $sandbox 'sp&chars#dir' New-Item -ItemType Directory -Path $sp -Force | Out-Null Set-Content -LiteralPath (Join-Path $sp 'x.txt') -Value 'special' -Encoding utf8 $list2 = Join-Path $sandbox 'BackupList2.txt' Set-Content -LiteralPath $list2 -Encoding utf8 -Value "`"$sp`"" $bd2 = Join-Path $sandbox 'Backups2' $r = Invoke-Bak -HostName 7 -Arguments @('-BackupListPath', $list2, '-BackupDir', $bd2) $n = @(Get-ChildItem -LiteralPath $bd2 -Filter '*.7z' -ErrorAction SilentlyContinue).Count Add-Case -Id 'BB-09' -Title '含 & 与 # 的路径(整行引号)' ` -Expected '退出码 0 且产出归档' ` -Actual ("退出码={0};归档={1}" -f $r.ExitCode, $n) -Pass ($r.ExitCode -eq 0 -and $n -ge 1) # ---------------------------------------------------------------- BB-10 无控制台不挂起 $pinfo = New-Object System.Diagnostics.ProcessStartInfo $pinfo.FileName = $host7 $pinfo.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$repo\Manage-Backup.ps1`"" $pinfo.RedirectStandardOutput = $true; $pinfo.RedirectStandardError = $true $pinfo.UseShellExecute = $false; $pinfo.CreateNoWindow = $true $proc = [System.Diagnostics.Process]::Start($pinfo) $finished = $proc.WaitForExit(60000) if (-not $finished) { try { $proc.Kill() } catch {} } Add-Case -Id 'BB-10' -Title '无控制台且无 -InputScript 时报错退出,绝不挂起' ` -Expected '60 秒内退出且退出码 2' ` -Actual ("已退出={0};退出码={1}" -f $finished, $proc.ExitCode) -Pass ($finished -and $proc.ExitCode -eq 2) # ---------------------------------------------------------------- BB-11 ★ 修正夹具:真实运行 $orphanDir = Join-Path $sandbox 'Backups3' New-Item -ItemType Directory -Path $orphanDir -Force | Out-Null if ($archives.Count -gt 0) { Copy-Item -LiteralPath $archives[0].FullName -Destination (Join-Path $orphanDir 'GhostSoftware.7z') } $r = Invoke-Bak -HostName 7 -Arguments @('-BackupListPath', $listPath, '-BackupDir', $orphanDir) $named = @($r.Output | Where-Object { $_ -match 'GhostSoftware' }).Count -gt 0 Add-Case -Id 'BB-11' -Title '★孤儿归档审计(改用真实运行)' ` -Expected '输出点名 GhostSoftware.7z' ` -Actual ("退出码={0};点名={1}" -f $r.ExitCode, $named) -Pass $named ` -Note '阶段 1 的 FAIL 系夹具错误:孤儿审计只在真实运行里报告,干跑不报' $report = [ordered]@{ testedAt = (Get-Date).ToString('s') total = $cases.Count passed = @($cases | Where-Object Status -eq 'PASS').Count failed = @($cases | Where-Object Status -eq 'FAIL').Count cases = $cases } $report | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $OutJson -Encoding utf8 $cases | Format-Table Id, Status, Title -AutoSize Write-Host ("回归合计 {0};通过 {1};失败 {2}" -f $report.total, $report.passed, $report.failed) Write-Host ("沙盒: " + $sandbox)