Files
BakNRet/test.ps1
T
Shuery 2d26f78d15 fix: 源文件改存 UTF-8 with BOM,让 Windows PowerShell 5.1 真正可用
改造前:全仓 6/6 个源文件在 5.1 上解析失败(README 却承诺支持 5.1)。原因是文件是无 BOM 的
UTF-8,而 5.1 没有 BOM 就按 ANSI 代码页解码源码,中文变乱码、全角问号吃掉引号,整块语法塌掉。
现在 28/28 个文件在 5.1 与 7 上都解析零错误,E2E 36 项在 5.1 上全绿。

顺带修掉一个被 5.1 掩盖的缺陷:带 [CmdletBinding()] 的脚本在 5.1 上,param() 默认值里
拿不到 $PSScriptRoot(实测为空串,7 上正常)。于是 Backup.ps1 / Restore.ps1 在 5.1 上不传
路径参数就报错退出 —— 而计划任务恰恰不传。E2E 之所以看不见,是因为它总是显式传路径。
8 处默认值全部移到 param() 之后的解析段,沿用本仓库对 -BackupDir 一直在用的写法。

新增三条可重放的约定,让编码不再是一次性动作:
  .gitattributes 接管行尾(本机 core.autocrlf=true,会把工作区改成 CRLF 制造伪 diff)
  .editorconfig 用 charset = utf-8-bom 锁住 BOM
  tools\Set-SourceEncoding.ps1 是规范化脚本,tools\Invoke-* 之外的任何改动之后都能重放
  test.ps1 是唯一验收入口:Encode + Parse + Unit + Smoke + E2E,在 7 与 5.1 上各跑一遍

test.ps1 的 Encode 层直接检查"必须有 BOM"这条规则。加它的原因很实际:实测本仓库用的
编辑工具在保存时会悄悄去掉 BOM,而丢了 BOM 的文件只在 5.1 上出错、在 7 上完全正常,
没有这条检查就会一直漏过去。

已知未修(下一步处理):5.1 上 Unit 有 6 项、Smoke 有 1 项失败,全部源于测试夹具写
临时文件时没指定编码(5.1 的 Set-Content 默认 ANSI),与产品代码无关。
2026-09-26 22:05:42 +08:00

254 lines
10 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.
<#
.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
#>
[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 {
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 {
$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 })