修掉的:空 catch 5 处;名词白名单 7 处;default-value 开关、自带 -WhatIf、lab 的明文口令与 irm|iex 各挂抑制并写明理由。 MaxDepth:它是分析器拓出来的真 bug —— 参数声明了却从未使用,也就是配置里的 CatalogMaxDepth 是假的,前缀补全实际只查 1 层,而配置注释与 README 都承诺「向下找几层」。按确认过的原则处理:**先让文档不撒谎**,所以把整条链路去掉(配置默认值、三个函数的参数、70 处实参、配置注释),而不是留一个假旋钮。零行为变化。想真的支持多层补全时,那是一个独立决定。 剩下 3 条都是分析器的误判,而且我实测确认过其中一条:$sourcePath 被报「赋值后从未使用」,我照着改成 $null = 之后,Set-StrictMode -Version 3.0 下读未定义变量直接抛错,Security 套件的 BeforeAll 挂掉、4 条用例连带失败。恢复后才绿。 这一类误判有共同成因:静态分析看不到「在传给 Test-Case / It / Where-Object 的 scriptblock 里被使用」。所以我只对能证明是误判的挂抑制并写明理由,不为了数字好看去改代码。 验收:test.ps1 9/9 全绿(7 与 5.1)、100 个文件两版解析零错、Run-RealSmoke 4/4。
264 lines
11 KiB
PowerShell
264 lines
11 KiB
PowerShell
<#
|
||
.SYNOPSIS
|
||
跑 BakNRet 的全部验收层次,两个 PowerShell 版本都覆盖。
|
||
|
||
.DESCRIPTION
|
||
BakNRet 同时支持 Windows PowerShell 5.1 与 PowerShell 7.x,而这两个版本会踩到
|
||
不同的坑(源码编码、Get-Content 的默认编码、File.Move 的重载、原子替换的回退路径)。
|
||
所以「验收」不是一句命令,而是分层的:
|
||
|
||
Parse 全仓 .ps1 / .psm1 / .psd1 在 5.1 与 7 上解析零错误
|
||
Unit Pester 套件(需要 Pester 5+,优先用仓库内 .tools/ 的副本)
|
||
Smoke 零依赖套件(只要 PowerShell 与 7z)
|
||
E2E 端到端验收:备份 → 确认排除生效 → 删源 → 恢复 → 逐字节对拍
|
||
Drill 真实归档恢复演练(只读,但依赖本机真实归档与活源,所以不在 All 里)
|
||
|
||
-Suite All 等价于 Parse + Unit + Smoke + E2E。Drill 必须显式点名。
|
||
|
||
.PARAMETER Suite
|
||
要跑哪些层次,可多选。
|
||
|
||
.PARAMETER PSVersion
|
||
在哪些宿主上跑:7、5.1 或 Both(默认)。所有层次都会在点名的宿主上各跑一遍;
|
||
某个宿主缺失或跑不动时报明确的失败,而不是静默跳过。
|
||
|
||
.PARAMETER Quiet
|
||
只打印每层的结论,不把各套件的完整输出透出来。
|
||
|
||
.EXAMPLE
|
||
.\test.ps1
|
||
|
||
.EXAMPLE
|
||
.\test.ps1 -Suite Parse
|
||
|
||
.EXAMPLE
|
||
.\test.ps1 -Suite E2E -PSVersion 5.1
|
||
#>
|
||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '',
|
||
Justification = '误判:这些参数的值是在传给 Test-Case / It 的 scriptblock 里用的,静态分析看不到那层使用。')]
|
||
[CmdletBinding()]
|
||
param(
|
||
[ValidateSet('All', 'Parse', 'Unit', 'Smoke', 'E2E', 'Drill')]
|
||
[string[]]$Suite = @('All'),
|
||
|
||
[ValidateSet('7', '5.1', 'Both')]
|
||
[string]$PSVersion = 'Both',
|
||
|
||
[switch]$Quiet
|
||
)
|
||
|
||
$ErrorActionPreference = 'Stop'
|
||
$root = $PSScriptRoot
|
||
|
||
# ---------------------------------------------------------------- 参数展开
|
||
|
||
$suites = [System.Collections.Generic.List[string]]::new()
|
||
foreach ($name in $Suite) {
|
||
$expanded = if ($name -eq 'All') { @('Parse', 'Unit', 'Smoke', 'E2E') } else { @($name) }
|
||
foreach ($item in $expanded) {
|
||
if (-not $suites.Contains($item)) { $suites.Add($item) }
|
||
}
|
||
}
|
||
|
||
$hosts = if ($PSVersion -eq 'Both') { @('7', '5.1') } else { @($PSVersion) }
|
||
|
||
$exeOf = @{ '7' = 'pwsh'; '5.1' = 'powershell' }
|
||
foreach ($h in $hosts) {
|
||
if (-not (Get-Command $exeOf[$h] -ErrorAction SilentlyContinue)) {
|
||
Write-Error ("找不到 {0},无法在 {1} 上验收。装好它,或用 -PSVersion 指定另一个宿主。" -f $exeOf[$h], $h)
|
||
exit 2
|
||
}
|
||
}
|
||
|
||
$script:Results = [System.Collections.Generic.List[object]]::new()
|
||
|
||
function Add-Result {
|
||
param([string]$Layer, [string]$HostName, [bool]$Ok, [string]$Detail)
|
||
|
||
$script:Results.Add([pscustomobject]@{ Layer = $Layer; HostName = $HostName; Ok = $Ok; Detail = $Detail })
|
||
$mark = if ($Ok) { 'PASS' } else { 'FAIL' }
|
||
$color = if ($Ok) { 'Green' } else { 'Red' }
|
||
Write-Host (" [{0}] {1,-5} {2,-4} {3}" -f $mark, $Layer, $HostName, $Detail) -ForegroundColor $color
|
||
}
|
||
|
||
function Invoke-ScriptInHost {
|
||
<#
|
||
在指定宿主里用独立进程跑一个脚本,返回退出码。
|
||
独立进程是刻意的:Backup.ps1 / Restore.ps1 结尾都会 exit,同进程调用会把本脚本一起带走。
|
||
#>
|
||
param(
|
||
[string]$HostName,
|
||
[string]$ScriptPath,
|
||
[string[]]$Arguments = @()
|
||
)
|
||
|
||
$stdout = Join-Path $env:TEMP ("baknret-test-{0}.log" -f [guid]::NewGuid().ToString('N'))
|
||
$argumentList = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $ScriptPath) + $Arguments
|
||
|
||
try {
|
||
# 5.1 在 $ErrorActionPreference = 'Stop' 下会把原生命令写到 stderr 的内容升级成
|
||
# 终止性错误;而被调脚本往 stderr 写字是正常的(7z 就这么干),不该让验收崩掉。
|
||
$ErrorActionPreference = 'Continue'
|
||
if ($Quiet) {
|
||
& $exeOf[$HostName] @argumentList *> $null
|
||
}
|
||
else {
|
||
& $exeOf[$HostName] @argumentList 2>&1 | Tee-Object -FilePath $stdout | Out-Null
|
||
}
|
||
return $LASTEXITCODE
|
||
}
|
||
finally {
|
||
Remove-Item -LiteralPath $stdout -Force -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
|
||
# ---------------------------------------------------------------- 各层实现
|
||
|
||
$parseProbeSource = @'
|
||
$root = $args[0]
|
||
$exclude = '\\(\.git|\.tools|\.scratch|Backups|logs|dist)\\'
|
||
# 注意:这里不能用 -Include 筛扩展名。5.1 与 7 的语义不同:-LiteralPath 上的 -Include
|
||
# 在 5.1 里会被忽略,于是 .md / .txt / .editorconfig / unattend.xml 全被拿去 ParseFile,
|
||
# 报一堆与 PowerShell 无关的假错(实测 40 个「文件」里 9 个不是 PowerShell)。
|
||
$powerShellExtensions = @('.ps1', '.psm1', '.psd1')
|
||
$files = @(Get-ChildItem -LiteralPath $root -Recurse -File |
|
||
Where-Object { $powerShellExtensions -contains $_.Extension -and $_.FullName -notmatch $exclude })
|
||
$bad = 0
|
||
foreach ($file in $files) {
|
||
$errors = $null
|
||
[void][System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$null, [ref]$errors)
|
||
if ($errors -and $errors.Count) {
|
||
$bad++
|
||
Write-Output (" [!] {0} : {1}" -f $file.FullName.Substring($root.Length).TrimStart('\'), $errors[0].Message)
|
||
}
|
||
}
|
||
Write-Output ("SUMMARY={0}/{1}" -f ($files.Count - $bad), $files.Count)
|
||
'@
|
||
|
||
function Invoke-EncodingCheck {
|
||
<#
|
||
编码规则检查:与宿主无关,只跑一次。
|
||
规则见 tools\Set-SourceEncoding.ps1:受管文件必须有 BOM、行尾必须是 LF、不许有制表符。
|
||
「必须有 BOM」这条尤其重要:它一旦丢失,5.1 上会以各种莫名其妙的方式失败,
|
||
而在 7 上完全正常 —— 实测就是这么漏过去的。
|
||
#>
|
||
$excluded = '\\(\.git|\.tools|\.scratch|Backups|logs|dist|snapshots)\\'
|
||
$targets = @(Get-ChildItem -LiteralPath $root -Recurse -File |
|
||
Where-Object { $_.Extension -in '.ps1', '.psm1', '.psd1' -or $_.Name -eq 'BackupList.txt' } |
|
||
Where-Object { $_.FullName -notmatch $excluded })
|
||
|
||
$noBom = [System.Collections.Generic.List[string]]::new()
|
||
$crlf = [System.Collections.Generic.List[string]]::new()
|
||
$tabs = [System.Collections.Generic.List[string]]::new()
|
||
|
||
foreach ($file in $targets) {
|
||
$relative = $file.FullName.Substring($root.Length).TrimStart('\')
|
||
$bytes = [System.IO.File]::ReadAllBytes($file.FullName)
|
||
if (-not ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF)) {
|
||
$noBom.Add($relative)
|
||
}
|
||
$text = (New-Object System.Text.UTF8Encoding($false)).GetString($bytes)
|
||
if ($text -match "`r`n") { $crlf.Add($relative) }
|
||
if ($text -match "`t") { $tabs.Add($relative) }
|
||
}
|
||
|
||
foreach ($item in $noBom) { Write-Host (" 缺 BOM:{0}" -f $item) -ForegroundColor DarkGray }
|
||
foreach ($item in $crlf) { Write-Host (" 有 CRLF:{0}" -f $item) -ForegroundColor DarkGray }
|
||
foreach ($item in $tabs) { Write-Host (" 有制表符:{0}" -f $item) -ForegroundColor DarkGray }
|
||
|
||
$ok = ($noBom.Count + $crlf.Count + $tabs.Count) -eq 0
|
||
Add-Result -Layer 'Encode' -HostName 'any' -Ok $ok `
|
||
-Detail ("受管 {0} 个文件:缺 BOM {1}、CRLF {2}、制表符 {3}" -f $targets.Count, $noBom.Count, $crlf.Count, $tabs.Count)
|
||
}
|
||
|
||
function Invoke-ParseLayer {
|
||
param([string]$HostName)
|
||
|
||
if (-not $script:EncodingChecked) {
|
||
$script:EncodingChecked = $true
|
||
Invoke-EncodingCheck
|
||
}
|
||
|
||
$probe = Join-Path $env:TEMP 'baknret-parse-gate.ps1'
|
||
Set-Content -LiteralPath $probe -Value $parseProbeSource -Encoding ascii
|
||
|
||
try {
|
||
$ErrorActionPreference = 'Continue' # 同上:原生命令的 stderr 不是错误
|
||
$output = & $exeOf[$HostName] -NoProfile -ExecutionPolicy Bypass -File $probe $root 2>&1
|
||
$summary = @($output | Where-Object { $_ -match '^SUMMARY=' })
|
||
$failures = @($output | Where-Object { $_ -match '^\s+\[!\]' })
|
||
|
||
if (-not $summary) {
|
||
Add-Result -Layer 'Parse' -HostName $HostName -Ok $false -Detail '探针没有给出结论'
|
||
return
|
||
}
|
||
|
||
$numbers = $summary[-1] -replace '^SUMMARY=', ''
|
||
$ok = ($numbers -split '/')[0] -eq ($numbers -split '/')[1]
|
||
if ($failures.Count -gt 0) { $failures | ForEach-Object { Write-Host $_ -ForegroundColor DarkGray } }
|
||
Add-Result -Layer 'Parse' -HostName $HostName -Ok $ok -Detail ("解析通过 {0} 个文件" -f $numbers)
|
||
}
|
||
finally {
|
||
Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
|
||
function Invoke-SuiteLayer {
|
||
param(
|
||
[string]$Layer,
|
||
[string]$HostName,
|
||
[string]$RelativeScript,
|
||
[string[]]$Arguments = @()
|
||
)
|
||
|
||
$path = Join-Path $root $RelativeScript
|
||
if (-not (Test-Path -LiteralPath $path)) {
|
||
Add-Result -Layer $Layer -HostName $HostName -Ok $false -Detail ("找不到 {0}" -f $RelativeScript)
|
||
return
|
||
}
|
||
|
||
# 套件自己会在结尾 exit:0 全绿、1 有失败、2 缺依赖
|
||
$code = Invoke-ScriptInHost -HostName $HostName -ScriptPath $path -Arguments $Arguments
|
||
$detail = switch ($code) {
|
||
0 { $RelativeScript }
|
||
2 { ("{0} —— 缺少依赖(退出码 2)" -f $RelativeScript) }
|
||
default { ("{0} —— 退出码 {1}" -f $RelativeScript, $code) }
|
||
}
|
||
Add-Result -Layer $Layer -HostName $HostName -Ok ($code -eq 0) -Detail $detail
|
||
}
|
||
|
||
# ---------------------------------------------------------------- 跑
|
||
|
||
Write-Host ''
|
||
Write-Host ("BakNRet 验收:层次 [{0}],宿主 [{1}]" -f ($suites -join ', '), ($hosts -join ', ')) -ForegroundColor Cyan
|
||
Write-Host ''
|
||
|
||
foreach ($hostName in $hosts) {
|
||
foreach ($layer in $suites) {
|
||
switch ($layer) {
|
||
'Parse' { Invoke-ParseLayer -HostName $hostName }
|
||
'Unit' { Invoke-SuiteLayer -Layer 'Unit' -HostName $hostName -RelativeScript 'tests\Run-Pester.ps1' -Arguments @('-Verbosity', 'Minimal') }
|
||
'Smoke' { Invoke-SuiteLayer -Layer 'Smoke' -HostName $hostName -RelativeScript 'tests\Run-Tests.ps1' }
|
||
'E2E' { Invoke-SuiteLayer -Layer 'E2E' -HostName $hostName -RelativeScript 'tests\Run-E2E.ps1' }
|
||
'Drill' { Invoke-SuiteLayer -Layer 'Drill' -HostName $hostName -RelativeScript 'tests\Restore-Drill.ps1' }
|
||
}
|
||
}
|
||
}
|
||
|
||
# ---------------------------------------------------------------- 汇总
|
||
|
||
$failed = @($script:Results | Where-Object { -not $_.Ok })
|
||
|
||
Write-Host ''
|
||
Write-Host ('=' * 64)
|
||
if ($failed.Count -eq 0) {
|
||
Write-Host ("验收全部通过:{0} 项(宿主 {1})" -f $script:Results.Count, ($hosts -join ' + ')) -ForegroundColor Green
|
||
}
|
||
else {
|
||
Write-Host ("验收有失败:{0} 项里失败 {1} 项" -f $script:Results.Count, $failed.Count) -ForegroundColor Red
|
||
$failed | ForEach-Object { Write-Host (" - {0} @ {1}:{2}" -f $_.Layer, $_.HostName, $_.Detail) -ForegroundColor Red }
|
||
}
|
||
Write-Host ('=' * 64)
|
||
|
||
exit $(if ($failed.Count -eq 0) { 0 } else { 1 })
|