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:
1 parent
f4729baca7
commit
45bbd66815
18 files changed
+2974
No files matched your search
@@ -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)
|
||||
Reference in new issue
Block a user