Files
BakNRet/tests/Run-RealSmoke.ps1
T
Shuery 2ad7987ffe refactor: Common.psm1 拆成 BakNRet/{Public,Private},一函数一文件 + 薄加载器
3152 行、66 个函数的单文件模块拆成:
  BakNRet\BakNRet.psd1   模块清单:FunctionsToExport 是显式白名单(62 个名字)
  BakNRet\BakNRet.psm1   加载器:点源顺序的唯一一处声明
  BakNRet\Public\*.ps1   62 个对外函数,一函数一文件,文件名 = 函数名
  BakNRet\Private\*.ps1  4 个内部函数 + State.ps1(模块级状态集中一处)

为什么是一函数一文件:这是社区里脚本模块的主流形态(调研实测:winutil 79 个、
Terminal-Icons 24 个、ModuleBuilder 23 个,全部如此)。收益是改动落在小文件里、diff 按职责
可读、模块级状态有唯一去处。

为什么这不算"打散":模块内 dot-source 的文件共享同一个模块作用域(实测确认),所以
"按顺序点源 67 个文件"与"点源一个大文件"在语义上等价;顺序只在加载器里出现一次,
tools\Build-BakNRetModule.ps1 从那里读出顺序就能拼回单文件 —— 本次产物 dist\BakNRet.psm1
3240 行、两个版本都解析零错。

新增一条断言把这条承诺钉住:加载器点源的文件集合必须与磁盘一致、导出名单必须与
Public\ 一一对应。漏一个文件或漏一个名字就是静默少一个函数 —— 而那种错在运行时只表现为
"找不到命令"。

引用更新:11 个文件里的 Common.psm1 改成 BakNRet\BakNRet.psd1(走清单导入,
FunctionsToExport 才真的说了算);Common.psm1 直接删除,不留转发垫片。

验收:test.ps1 9/9 全绿(7 与 5.1),98 个文件两版解析零错,276 个断言原样通过 ——
这次搬家没有改变任何可观察行为。
2026-09-27 09:34:10 +08:00

164 lines
7.7 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
只读冒烟:在**真实清单与真实归档**上验证解析,以及"一个字节都不写"的承诺。
.DESCRIPTION
与其它套件的分工:Pester、零依赖、E2E 都在临时目录里自造夹具,跑得快、可重复;
这个冒烟专门跑真实清单,用来挡住"夹具全绿、真实数据全废"这一类问题。
它存在的理由是一件真事:清单里有 24 条把方向标记贴在目标上(`+WindowsTerminal`),
而解析器当时只认独立记号(`+ WindowsTerminal`)。于是那些条目被当成"名叫
+WindowsTerminal 的软件名",名录里查不到就退回当目录名,目录又不存在,整条静默
记成 missing-source 跳过 —— 备份照常退出 0,所有夹具测试也都是绿的。
只有拿真实清单跑一遍才看得见。
检查三件事:
1) 真实清单里没有"标记没被剥掉"的条目;
2) 真实清单里没有软件名在名录里查不到(那正是上面那种静默退化);
3) 两个只读模式跑完,manifest.json 的 SHA256 一个字都没变。
全程只读:Backup 只用 -DryRun,Restore 只用 -VerifyOnly。
真实清单或归档不存在时**明确跳过**(退出码 0 并打印原因),因为机器状态不在被测范围内。
.EXAMPLE
.\tests\Run-RealSmoke.ps1
#>
[CmdletBinding()]
param(
[string]$ModulePath,
# 被抽查的条目:挑体积小、静态、不会因为程序运行而变化的
[string[]]$SampleEntries = @('WindowsTerminal', 'OpenSSH', 'PowerShell'),
# Restore 端用来验证"行首 + 会被识别成仅备份"的条目
[string]$RestoreSample = 'WindowsPowerShell'
)
$ErrorActionPreference = 'Stop'
$projectRoot = Split-Path -Parent $PSScriptRoot
if (-not $ModulePath) { $ModulePath = Join-Path $projectRoot 'BakNRet\BakNRet.psd1' }
Import-Module $ModulePath -Force
Import-Module (Join-Path $PSScriptRoot 'TestHelpers.psm1') -Force
$listPath = Join-Path $projectRoot 'BackupList.txt'
$configPath = Join-Path $projectRoot 'BackupConfig.psd1'
$manifestPath = Join-Path $projectRoot 'Backups\manifest.json'
$backupScript = Join-Path $projectRoot 'Backup.ps1'
$restoreScript = Join-Path $projectRoot 'Restore.ps1'
Reset-TestResult
if (-not (Test-Path -LiteralPath $listPath)) {
Write-Host "跳过:找不到真实清单 $listPath" -ForegroundColor Yellow
exit 0
}
if (-not (Test-Path -LiteralPath $manifestPath)) {
Write-Host "跳过:找不到真实 manifest $manifestPath(这台机器还没跑过备份)" -ForegroundColor Yellow
exit 0
}
$config = Get-BaknretConfig -Path $configPath
$catalogPath = Resolve-CatalogPath -Configured $config.SoftwareCatalog -Root $projectRoot
$listLines = @(Get-Content -Encoding UTF8 -LiteralPath $listPath)
# 用 cmd 批次文件把子进程输出落盘再读回。为什么不用 `& pwsh ... 2>&1` 直接捕获:
# 那会给子进程建管道,在受限环境 / 沙箱里会被直接拒绝(见 tests\BakNRet.Tests.ps1 里的同款注释)。
function Invoke-BaknretCaptured {
param([string]$Script, [string[]]$Arguments)
$hostExe = if ($PSVersionTable.PSEdition -eq 'Core') { 'pwsh' } else { 'powershell' }
$stamp = [guid]::NewGuid().ToString('N')
$outFile = Join-Path ([System.IO.Path]::GetTempPath()) "bnr-smoke-$stamp.txt"
$cmdFile = Join-Path ([System.IO.Path]::GetTempPath()) "bnr-smoke-$stamp.cmd"
$quoted = (@($Script) + $Arguments | ForEach-Object { '"{0}"' -f $_ }) -join ' '
$batch = "@echo off`r`n$hostExe $quoted > `"$outFile`" 2>&1`r`nexit /b %ERRORLEVEL%`r`n"
# .cmd 必须无 BOM:cmd.exe 会被 BOM 弄坏
[System.IO.File]::WriteAllText($cmdFile, $batch, [System.Text.UTF8Encoding]::new($false))
try {
# 原生命令 + Stop 在 5.1 上会把 stderr 升级成终止错误,这里只要退出码
$previous = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
& cmd.exe /c $cmdFile | Out-Null
$code = $LASTEXITCODE
} finally {
$ErrorActionPreference = $previous
}
$text = if (Test-Path -LiteralPath $outFile) {
Get-Content -Encoding UTF8 -LiteralPath $outFile -Raw
} else { '' }
return [pscustomobject]@{ ExitCode = $code; Output = $text }
} finally {
Remove-Item -LiteralPath $outFile, $cmdFile -Force -ErrorAction SilentlyContinue
}
}
function Get-ManifestHash {
if (Test-Path -LiteralPath $manifestPath) {
return (Get-FileHash -LiteralPath $manifestPath -Algorithm SHA256).Hash
}
return '<不存在>'
}
Write-Host "`n== 真实清单解析 ==" -ForegroundColor Cyan
Test-Case '真实清单里没有"方向标记没被剥掉"的条目' {
$leftOver = @()
foreach ($line in $listLines) {
$item = ConvertFrom-BackupListLine -Line $line
if ($item -and $item.Path -match '^[+-]') { $leftOver += $item.Path }
}
Assert-Equal 0 $leftOver.Count ("这些条目的方向标记没被剥掉:" + ($leftOver -join '、'))
}
Test-Case '真实清单里每个条目都能解析出方向,且软件名都能在名录里查到' {
$entries = 0
$backupOnly = 0
$restoreOnly = 0
$catalogMiss = @()
foreach ($line in $listLines) {
$item = ConvertFrom-BackupListLine -Line $line
if (-not $item) { continue }
$entries++
if ($item.Direction -eq 'backup') { $backupOnly++ }
if ($item.Direction -eq 'restore') { $restoreOnly++ }
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $catalogPath -MaxDepth $config.CatalogMaxDepth
if ($resolved.Error -and $resolved.Error -like '*名录里没有*') { $catalogMiss += $resolved.Error }
}
Assert-True ($entries -gt 0) '真实清单一条都没解析出来,检查范围不对'
Assert-Equal 0 $catalogMiss.Count ("这些条目静默退化成了目录名:" + ($catalogMiss -join ';'))
Write-Host (" 条目 {0} 个:仅备份 {1}、仅恢复 {2}、双向 {3}" -f $entries, $backupOnly, $restoreOnly, ($entries - $backupOnly - $restoreOnly)) -ForegroundColor DarkGray
}
Write-Host "`n== 只读模式不写一个字节 ==" -ForegroundColor Cyan
Test-Case 'Backup.ps1 -DryRun 退出 0、不再有"名录里没有"告警、manifest 不变' {
$before = Get-ManifestHash
$arguments = @('-BackupListPath', $listPath, '-DryRun', '-QuietTool', '-Only') + $SampleEntries
$run = Invoke-BaknretCaptured -Script $backupScript -Arguments $arguments
Assert-Equal 0 $run.ExitCode ("DryRun 退出码不是 0;输出尾部:" + ($run.Output -split "`n" | Select-Object -Last 4 | Out-String))
Assert-False ($run.Output -match "名录里没有 '") "DryRun 仍然把某些条目当成查不到的软件名"
Assert-Equal $before (Get-ManifestHash) 'DryRun 改动了 manifest.json'
}
Test-Case 'Restore.ps1 -VerifyOnly 退出 0、把行首 + 识别成"仅备份"、manifest 不变' {
$before = Get-ManifestHash
$run = Invoke-BaknretCaptured -Script $restoreScript -Arguments @(
'-BackupListPath', $listPath, '-VerifyOnly', '-Only', $RestoreSample, '-Verbose')
Assert-Equal 0 $run.ExitCode ("VerifyOnly 退出码不是 0;输出尾部:" + ($run.Output -split "`n" | Select-Object -Last 4 | Out-String))
Assert-False ($run.Output -match "名录里没有 '") 'VerifyOnly 仍然把某些条目当成查不到的软件名'
# 这句跳过原因是 -Level DEBUG 的,要 -Verbose 才会打印 —— 顺带也验了那条路径
Assert-True ($run.Output -match '跳过(行首 \+,仅备份)') 'VerifyOnly 没有把行首 + 识别成"仅备份"'
Assert-Equal $before (Get-ManifestHash) 'VerifyOnly 改动了 manifest.json'
}
exit (Write-TestSummary -Title '真实清单只读冒烟')