<# .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-BakNRetCatalogPath -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-BakNRetBackupEntry -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 '真实清单只读冒烟')