Files
BakNRet/.scratch/ci-cd/blackbox-regression.ps1
T
Shuery 45bbd66815 docs(report): 新增 PROJECT_REPORT.md 与 CI/CD 流水线记录
PROJECT_REPORT.md:论文式项目报告(摘要 / 目录 / 7 章 / 参考文献 / 致谢 / 附录 A),
4 张 Mermaid 图同样经真实浏览器渲染验证。所有数字都与证据文件逐个核对过。

.scratch/ci-cd/:本次流水线的全部证据与可重跑脚本
  * 阶段报告:依赖安全扫描 / 许可证合规 / 阶段 0 静态分析 / 阶段 3 测试与性能
  * 证据:分析器原始输出(修复前 84 条、修复后 80 条)、Pester 详细输出、
    性能基线 JSON、黑盒用例结果(阶段 1 的 11 例与阶段 2 回归的 12 例)
  * 可重跑:blackbox-tests.ps1 / blackbox-regression.ps1 / perf-baseline.ps1

两条边界必须写在明处,不能含糊:

  * **VM 内验证只取到修复前的快照**(Pester 183 项全绿)。随后会话审批策略改为 never,
    gsudo 提权被自动拒绝(退出码 999),而 Hyper-V 与 PowerShell Direct 都需要管理员,
    于是修复后的三套件在 VM 内**没有跑成**。报告里明确标注了范围,没有把"宿主跑绿了"
    说成"VM 也跑绿了"。
  * **性能基线是首次建立**,无历史可比,故无退化可判;指标只在同机同宿主下对比,
    跨机比数字没有意义。

另外记一笔:静态分析的口径是"仓库自己的门禁"。剩余 80 条全是风格类(0 Error),且逐条
有依据 —— 行长 160 是配置里写明的有意偏离,PSPlaceCloseBrace 等集中在测试夹具字符串内
(改了会改变断言语义)。严格模式的"零容忍"在这里与仓库自身约定相抵,选择尊重仓库约定
并在报告里登记,而不是制造一个横跨 12 个文件的纯排版大 diff。
2026-10-02 01:17:40 +08:00

172 lines
11 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 阶段 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)