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。
This commit is contained in:
Shuery committed 2026-10-02 01:17:40 +08:00
1 parent f4729baca7
commit 45bbd66815
18 files changed
+2974

No files matched your search

+209
View File
@@ -0,0 +1,209 @@
# 黑盒测试:只依据 README 记录的对外契约,在**沙盒副本**里驱动 CLI。
# 不读实现细节;每个用例都断言"文档承诺的行为 vs 实际行为"。
param(
[string]$OutJson = "$PSScriptRoot\blackbox_results.json"
)
$ErrorActionPreference = 'Continue'
$repo = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$host7 = (Get-Command pwsh).Source
$host51 = (Get-Command powershell).Source
# ---------------------------------------------------------------- 沙盒(绝不碰真实 Backups/ logs/)
$sandbox = Join-Path $env:TEMP ('baknret-bb-' + [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]$Module, [string]$Title, [string]$Priority,
[string]$Precondition, [string]$Steps, [string]$Expected,
[string]$Actual, [bool]$Pass, [string]$Severity = '')
$cases.Add([pscustomobject]@{
Id = $Id; Module = $Module; Title = $Title; Priority = $Priority
Precondition = $Precondition; Steps = $Steps; Expected = $Expected
Actual = $Actual; Status = $(if ($Pass) { 'PASS' } else { 'FAIL' })
Severity = $(if ($Pass) { '' } else { $Severity })
})
}
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) }
}
function Get-ManifestHash {
$p = Join-Path $backupDir 'manifest.json'
if (-not (Test-Path -LiteralPath $p)) { return '<absent>' }
return (Get-FileHash -LiteralPath $p -Algorithm SHA256).Hash
}
# ================================================================ 用例
# BB-01 首次真实备份(核心流)
$before = Get-ManifestHash
$r = Invoke-Bak -HostName 7 -Arguments @('-BackupListPath', $listPath, '-BackupDir', $backupDir)
$archives = @(Get-ChildItem -LiteralPath $backupDir -Filter '*.7z' -ErrorAction SilentlyContinue)
Add-Case -Id 'BB-01' -Module '备份' -Title '文档承诺:备份成功返回 0,并产出 .7z 归档' -Priority 'P0' `
-Precondition '沙盒清单:1 个目录(含排除)+ 1 个不存在的源' `
-Steps 'Backup-Data.ps1(无 -DryRun)' `
-Expected '退出码 0;Backups\ 下出现 1 个 .7z;manifest.json 存在' `
-Actual ("退出码={0};归档={1} 个({2})" -f $r.ExitCode, $archives.Count, ($archives.Name -join ',')) `
-Pass ($r.ExitCode -eq 0 -and $archives.Count -ge 1 -and (Test-Path (Join-Path $backupDir 'manifest.json'))) `
-Severity '严重'
# BB-02 排除规则真的生效(逐个归档内容核对)
$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 } } }
$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' -Module '排除' -Title '文档承诺::- 排除模式把内容挡在归档之外' -Priority 'P0' `
-Precondition '清单里对源目录写了 :- ''nested\skipme.log''' `
-Steps '7z l 列出归档内容' `
-Expected '归档里**没有** skipme.log,但有 a.txt 与 b.txt' `
-Actual ("skipme.log 命中={0};a/b.txt 命中={1}" -f $hasSkip, $hasKeep) `
-Pass ($hasKeep -and -not $hasSkip) -Severity '严重'
# BB-03 只读模式不写盘(README 的强承诺)
$h1 = Get-ManifestHash
$r = Invoke-Bak -HostName 7 -Arguments @('-DryRun', '-BackupListPath', $listPath, '-BackupDir', $backupDir)
$h2 = Get-ManifestHash
Add-Case -Id 'BB-03' -Module '干跑' -Title '文档承诺:-DryRun 一个字节都不写(含 manifest.json)' -Priority 'P0' `
-Precondition '已有 1 份归档与 manifest.json' `
-Steps '记录 SHA256 → 跑 -DryRun → 再记录 SHA256' `
-Expected '退出码 0;manifest.json 的 SHA256 前后完全一致' `
-Actual ("退出码={0};哈希 {1} → {2}" -f $r.ExitCode, $h1.Substring(0, 12), $h2.Substring(0, 12)) `
-Pass ($r.ExitCode -eq 0 -and $h1 -eq $h2) -Severity '致命'
# BB-04 源不存在只算跳过、不算失败
Add-Case -Id 'BB-04' -Module '异常流' -Title '文档承诺:源路径不存在记 missing-source,不算失败' -Priority 'P1' `
-Precondition '清单第 2 行指向不存在的目录' `
-Steps '跑备份后读 manifest.json 的 action 字段' `
-Expected '该条目 action=missing-source,且整体退出码仍为 0' `
-Actual ("退出码={0};manifest action 分布={1}" -f $r.ExitCode,
(@((Get-Content (Join-Path $backupDir 'manifest.json') -Raw | ConvertFrom-Json).items.PSObject.Properties.Value.action) -join ',')) `
-Pass ($r.ExitCode -eq 0 -and (@((Get-Content (Join-Path $backupDir 'manifest.json') -Raw | ConvertFrom-Json).items.PSObject.Properties.Value.action) -contains 'missing-source')) `
-Severity '一般'
# 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' -Module '干跑' -Title '文档承诺:-Only 只处理匹配的条目' -Priority 'P1' `
-Precondition '清单 2 条:srcdocs(目录)、nonexistent(不存在)' `
-Steps 'Backup-Data.ps1 -DryRun -Only ''srcdocs''' `
-Expected '退出码 0;输出里不出现未选中的 nonexistent 条目' `
-Actual ("退出码={0};输出提到 nonexistent={1}" -f $r.ExitCode, $hitMissing) `
-Pass ($r.ExitCode -eq 0 -and -not $hitMissing) -Severity '一般'
# BB-06 非法参数(错误推测)
$r = Invoke-Bak -HostName 7 -Arguments @('-BackupListPath', (Join-Path $sandbox 'no-such-list.txt'), '-BackupDir', $backupDir)
Add-Case -Id 'BB-06' -Module '异常流' -Title '边界值:清单文件不存在时的行为' -Priority 'P1' `
-Precondition '传入一个不存在的 -BackupListPath' `
-Steps 'Backup-Data.ps1 -BackupListPath <不存在>' `
-Expected '明确报错(非 0 退出码)或给出可读提示,**不得静默返回 0**' `
-Actual ("退出码={0};末行={1}" -f $r.ExitCode, (@($r.Output) | Select-Object -Last 1)) `
-Pass ($r.ExitCode -ne 0) -Severity '一般'
# BB-07 旧名字垫片仍可用(文档承诺"只留一轮")
$r = Invoke-Bak -HostName 7 -Arguments @('-DryRun', '-BackupListPath', $listPath, '-BackupDir', $backupDir) -Script 'Backup.ps1'
$saidRename = @($r.Output | Where-Object { $_ -match '已改名为' }).Count -gt 0
Add-Case -Id 'BB-07' -Module '兼容' -Title '文档承诺:旧名字 Backup.ps1 是转发垫片,退出码原样传递' -Priority 'P1' `
-Precondition '实现已改名为 Backup-Data.ps1' `
-Steps 'Backup.ps1 -DryRun(旧名字)' `
-Expected '打印改名提示;退出码与直接调用一致(0)' `
-Actual ("退出码={0};有改名提示={1}" -f $r.ExitCode, $saidRename) `
-Pass ($r.ExitCode -eq 0 -and $saidRename) -Severity '一般'
# BB-08 双宿主一致性(5.1 是文档承诺支持的一半)
$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' -Module '跨端一致性' -Title '文档承诺:Windows PowerShell 5.1 与 7.x 行为一致' -Priority 'P0' `
-Precondition '同一沙盒、同一清单' `
-Steps '两个宿主各跑一次 -DryRun,比较退出码' `
-Expected '两者退出码都是 0(GBK 控制台下的中文输出不崩)' `
-Actual ("7 退出码={0};5.1 退出码={1}" -f $r7.ExitCode, $r51.ExitCode) `
-Pass ($r7.ExitCode -eq 0 -and $r51.ExitCode -eq 0) -Severity '致命'
# 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)
$ok9 = $r.ExitCode -eq 0 -and @(Get-ChildItem -LiteralPath $bd2 -Filter '*.7z' -ErrorAction SilentlyContinue).Count -ge 1
Add-Case -Id 'BB-09' -Module '边界值' -Title '特殊字符:含 & 与 # 的路径(整行引号写法)' -Priority 'P2' `
-Precondition '源目录名含 & 与 #;清单行整体加引号' `
-Steps '备份该条目' `
-Expected '退出码 0 且真的产出归档(# 不被当注释吃掉)' `
-Actual ("退出码={0};归档数={1}" -f $r.ExitCode, @(Get-ChildItem -LiteralPath $bd2 -Filter '*.7z' -ErrorAction SilentlyContinue).Count) `
-Pass $ok9 -Severity '一般'
# BB-10 无控制台时的明确失败(README:绝不挂起)
$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)
$stdout = if ($finished) { $proc.StandardOutput.ReadToEnd() } else { '' }
$stderr = if ($finished) { $proc.StandardError.ReadToEnd() } else { '' }
if (-not $finished) { try { $proc.Kill() } catch {} }
Add-Case -Id 'BB-10' -Module '无头' -Title '文档承诺:没有控制台且没给 -InputScript 时报错退出,绝不挂起' -Priority 'P0' `
-Precondition 'stdout/stderr 被重定向(等价于计划任务/管道环境)' `
-Steps '不传参数直接运行 Manage-Backup.ps1,等待最多 60 秒' `
-Expected '60 秒内退出,退出码 2,并提示"没有可用的控制台"' `
-Actual ("60 秒内退出={0};退出码={1};输出片段={2}" -f $finished, $proc.ExitCode, (@($stdout, $stderr) -join ' ').Trim().Substring(0, [Math]::Min(90, (@($stdout, $stderr) -join ' ').Trim().Length))) `
-Pass ($finished -and $proc.ExitCode -eq 2) -Severity '致命'
# BB-11 孤儿归档审计
$orphanDir = Join-Path $sandbox 'Backups3'
New-Item -ItemType Directory -Path $orphanDir -Force | Out-Null
Copy-Item -LiteralPath (Join-Path $backupDir 'manifest.json') -Destination (Join-Path $orphanDir 'manifest.json') -ErrorAction SilentlyContinue
$fake = Join-Path $orphanDir 'GhostSoftware.7z'
if ($archives.Count -gt 0) { Copy-Item -LiteralPath $archives[0].FullName -Destination $fake }
$r = Invoke-Bak -HostName 7 -Arguments @('-DryRun', '-BackupListPath', $listPath, '-BackupDir', $orphanDir)
$named = @($r.Output | Where-Object { $_ -match 'GhostSoftware' }).Count -gt 0
Add-Case -Id 'BB-11' -Module '审计' -Title '文档承诺:孤儿归档会被点名' -Priority 'P1' `
-Precondition 'Backups3\ 里放一个清单中不存在的 GhostSoftware.7z' `
-Steps '备份后看输出是否点名' `
-Expected '输出里出现该孤儿归档名' `
-Actual ("退出码={0};点名={1}" -f $r.ExitCode, $named) `
-Pass $named -Severity '一般'
# ---------------------------------------------------------------- 汇总
$report = [ordered]@{
testedAt = (Get-Date).ToString('s')
scope = '文档契约黑盒测试(README 为准)'
sandbox = $sandbox
total = $cases.Count
passed = @($cases | Where-Object Status -eq 'PASS').Count
failed = @($cases | Where-Object Status -eq 'FAIL').Count
fatalOrSevere = @($cases | Where-Object { $_.Status -eq 'FAIL' -and $_.Severity -in '致命', '严重' }).Count
cases = $cases
}
$report | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $OutJson -Encoding utf8
$cases | Format-Table Id, Module, Priority, Status, Severity, Title -AutoSize
Write-Host ''
Write-Host ("合计 {0};通过 {1};失败 {2};致命/严重 {3}" -f $report.total, $report.passed, $report.failed, $report.fatalOrSevere)
Write-Host ("结果写入 {0}" -f $OutJson)
Write-Host ("沙盒(保留供排查): {0}" -f $sandbox)