Files
BakNRet/tests/Run-Tests.ps1
T
Shuery 045d51ac9c 引入软件名录:清单写软件名,归档名也用软件名
新功能
- 新增 SoftwareCatalog.psd1 —— "软件名 -> 目录"映射表,BackupList.txt 里
  直接写软件名即可,归档名也就是软件名(FooClolor.7z),
  不再是 FooClolor_from_C_+Programs.7z 这种由路径拼出来的名字。
- 三种写法可混用:软件名、字面路径(现有清单无需改写)、软件名 @pathname。
- 名录支持前缀补全(legendary -> legendary_2.0.4,只认 <名>_* / <名>-*)、
  Variants(同名目录在多处)、Includes(分文件维护)。
- tools/Rename-Archives.ps1:存量归档重命名,默认试运行,逐份大小校验并重建 manifest。
- 归档名重复直接报错,不再静默互相覆盖。

两套测试全绿:单元 42 项、端到端 23 项(新增名录命名/解析/迁移用例)。

过程中修掉的缺陷
- Resolve-BackupEntry 里 @pathname 与 Unresolved 分支顺序错误,
  @pathname 会被静默吃掉(改名后仍用软件名)。
- 源目录被删除时解析器丢掉 Sources,导致恢复端把软件名当路径、
  报 "Cannot bind argument to parameter 'Path' because it is an empty string"。
  恢复的语义恰恰是"源不存在就要还原回去",现在 Sources 照旧给出。
- 源存在性检查曾被漏掉,Get-Item 对不存在路径抛异常会中断整轮备份;
  且不能用 Join-Path 探测——目标盘符不存在时它会直接抛异常。
- 计划任务脚本外的 Caller 需要 -DryRun 才能验,已实跑确认。
2026-09-21 20:55:19 +08:00

444 lines
21 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 测试套件(零依赖,Windows PowerShell 5.1 与 PowerShell 7.x 均可运行)。
.DESCRIPTION
为什么不用 Pester:本机只装了 Pester 3.4.0,`Should -Be` 这类 Pester 4+ 语法
直接语法错误,而为了跑测试去装 Pester 5 不值得。这里自带一个极简断言运行器,
对备份工具这种小脚本来说更可靠。
覆盖范围:
* BackupList.txt 语法解析(包含历史上出错的两种写法)
* 归档命名与逆向解析
* 7z 排除参数翻译(含空格、! 前缀、重复前缀、禁用引号)
* Windows 命令行拼接
* manifest 读写往返
* 配置默认值与嵌套合并
* 集成测试:真的调用 7z 打包 + 解压,验证排除结果落在文件系统上
.EXAMPLE
pwsh -File .\tests\Run-Tests.ps1
#>
[CmdletBinding()]
param(
[string]$ModulePath = (Join-Path (Split-Path -Parent $PSScriptRoot) 'Common.psm1')
)
$ErrorActionPreference = 'Stop'
Import-Module $ModulePath -Force
Import-Module (Join-Path $PSScriptRoot 'TestHelpers.psm1') -Force
Reset-TestResult
# ============================================================================
Write-Host "`n== BackupList 解析 ==" -ForegroundColor Cyan
# ============================================================================
Test-Case '注释与空行返回 $null' {
Assert-Null (ConvertFrom-BackupListLine -Line '# 这是注释')
Assert-Null (ConvertFrom-BackupListLine -Line ' ')
Assert-Null (ConvertFrom-BackupListLine -Line '')
}
Test-Case '裸路径' {
$r = ConvertFrom-BackupListLine -Line 'C:\Programs\FooClolor'
Assert-Equal 'C:\Programs\FooClolor' $r.Path
Assert-Equal 0 $r.ExcludePatterns.Count
Assert-Equal 0 $r.Flags.Count
}
Test-Case '逗号分隔的排除表被拆成多个模式(旧实现只认分号,这里曾经整串当成一个)' {
$r = ConvertFrom-BackupListLine -Line 'C:\Programs\March7thAssistant :: a\b,c\d\'
Assert-Equal 'C:\Programs\March7thAssistant' $r.Path
Assert-Equal 2 $r.ExcludePatterns.Count
Assert-Equal 'a\b' $r.ExcludePatterns[0]
Assert-Equal 'c\d\' $r.ExcludePatterns[1]
}
Test-Case '分号分隔同样支持' {
$r = ConvertFrom-BackupListLine -Line 'C:\x :: a;b'
Assert-Equal 2 $r.ExcludePatterns.Count
}
Test-Case '引号只包路径时排除表正常解析' {
$r = ConvertFrom-BackupListLine -Line '"C:\Program Files\App" :: cache\*,logs\'
Assert-Equal 'C:\Program Files\App' $r.Path
Assert-Equal 2 $r.ExcludePatterns.Count
}
Test-Case '整行被一对引号包住时,:: 之后的排除表不被吞进路径(真实第 27 行)' {
$line = '"C:\Programs\ScoopApps\persist :: persist\a\DawnCache,persist\a\GPUCache"'
$r = ConvertFrom-BackupListLine -Line $line
Assert-Equal 'C:\Programs\ScoopApps\persist' $r.Path
Assert-Equal 2 $r.ExcludePatterns.Count
Assert-Equal 'persist\a\DawnCache' $r.ExcludePatterns[0]
}
Test-Case '@ 标记单独出现' {
$r = ConvertFrom-BackupListLine -Line '%UserProfile%\.ssh @encrypt'
Assert-Equal '%UserProfile%\.ssh' $r.Path
Assert-Equal 1 $r.Flags.Count
Assert-Equal 'encrypt' $r.Flags[0]
Assert-Equal 0 $r.ExcludePatterns.Count
}
Test-Case '@ 标记跟在排除表后面' {
$r = ConvertFrom-BackupListLine -Line 'C:\x :: a,b @encrypt'
Assert-Equal 'C:\x' $r.Path
Assert-Equal 2 $r.ExcludePatterns.Count
Assert-Equal 'encrypt' $r.Flags[0]
}
# ============================================================================
Write-Host "`n== 归档命名 ==" -ForegroundColor Cyan
# ============================================================================
Test-Case '基础命名规则' {
Assert-Equal 'legendary_from_%UserProfile%+.config' (Get-BackupBaseName -RawPath '%UserProfile%/.config/legendary')
Assert-Equal 'FooClolor_from_C_+Programs' (Get-BackupBaseName -RawPath 'C:\Programs\FooClolor')
Assert-Equal 'settings.json_from_C_+Programs' (Get-BackupBaseName -RawPath 'C:\Programs\settings.json')
}
Test-Case '/ 与 \ 以及重复分隔符结果一致' {
$a = Get-BackupBaseName -RawPath '%UserProfile%/.config/scoop'
$b = Get-BackupBaseName -RawPath '%UserProfile%\.config\scoop'
$c = Get-BackupBaseName -RawPath '%UserProfile%\\Documents\\WindowsPowerShell'
Assert-Equal $a $b
Assert-Equal 'WindowsPowerShell_from_%UserProfile%+Documents' $c
}
Test-Case '命名 <-> 路径往返' {
$cases = @(
'%UserProfile%/.config/legendary',
'C:\Programs\FooClolor',
'%LocalAppData%\Microsoft\Edge\User Data'
)
foreach ($case in $cases) {
$base = Get-BackupBaseName -RawPath $case
$back = Convert-BackupFileNameToPath -FileName "$base.7z"
Assert-Equal ($case -replace '[/\\]+', '\') $back "往返 $case"
}
}
Test-Case '归档名里不含非法文件名字符' {
$base = Get-BackupBaseName -RawPath 'C:\a\b?c*d|e'
$invalid = [System.IO.Path]::GetInvalidFileNameChars() | Where-Object { $_ -notin @('&', '%', '+') }
foreach ($ch in $invalid) {
Assert-False ($base.Contains([string]$ch)) "归档名里出现了非法字符 $ch"
}
}
# ============================================================================
Write-Host "`n== 7z 排除参数翻译 ==" -ForegroundColor Cyan
# ============================================================================
Test-Case '相对模式自动补上归档根目录名' {
$exclude = Get-ArchiveExcludeArgument -ItemName 'March7thAssistant' -Patterns @('logs\')
Assert-Equal '-x!March7thAssistant\logs' $exclude[0]
}
Test-Case '已经带根目录名时不重复前缀' {
$exclude = Get-ArchiveExcludeArgument -ItemName 'March7thAssistant' -Patterns @('March7thAssistant\logs\')
Assert-Equal '-x!March7thAssistant\logs' $exclude[0]
}
Test-Case '! 前缀翻译成递归组件匹配' {
$exclude = Get-ArchiveExcludeArgument -ItemName 'User Data' -Patterns @('!*Cache')
Assert-Equal '-xr!*Cache' $exclude[0]
}
Test-Case '模式里的空格转成 ? (7z 的模式不支持空格)' {
$exclude = Get-ArchiveExcludeArgument -ItemName 'User Data' -Patterns @('Default\Code Cache')
Assert-Equal '-x!User?Data\Default\Code?Cache' $exclude[0]
}
Test-Case '生成的参数里绝不出现引号(旧实现 -x!"路径" 让排除全部失效)' {
$patterns = @('Default\Code Cache', '!*Cache', 'logs\', 'March7thAssistant\3rdparty\WebBrowser\UserProfile\Integrated')
foreach ($pattern in $patterns) {
foreach ($entry in (Get-ArchiveExcludeArgument -ItemName 'User Data' -Patterns @($pattern))) {
Assert-False ($entry.Contains('"')) "参数里出现了引号: $entry"
}
}
}
Test-Case '空模式被忽略' {
$exclude = Get-ArchiveExcludeArgument -ItemName 'x' -Patterns @('', ' ', 'real\')
Assert-Equal 1 $exclude.Count
}
# ============================================================================
Write-Host "`n== 命令行拼接 ==" -ForegroundColor Cyan
# ============================================================================
Test-Case '无空格参数原样输出' {
Assert-Equal 'a -t7z' (ConvertTo-NativeArgumentString -ArgumentList @('a', '-t7z'))
}
Test-Case '含空格参数加引号' {
Assert-Equal '"C:\Program Files\x"' (ConvertTo-NativeArgumentString -ArgumentList @('C:\Program Files\x'))
}
Test-Case '引号内的结尾反斜杠翻倍(否则会被当成转义引号)' {
Assert-Equal '"C:\Program Files\\"' (ConvertTo-NativeArgumentString -ArgumentList @('C:\Program Files\'))
}
Test-Case '内部引号被转义' {
Assert-Equal '"say \"hi\""' (ConvertTo-NativeArgumentString -ArgumentList @('say "hi"'))
}
Test-Case '空参数输出一对空引号' {
Assert-Equal '""' (ConvertTo-NativeArgumentString -ArgumentList @(''))
}
# ============================================================================
Write-Host "`n== manifest 与配置 ==" -ForegroundColor Cyan
# ============================================================================
$sandbox = Join-Path $env:TEMP ("baknret-tests-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
Test-Case 'manifest 读写往返' {
New-Item -ItemType Directory -Path $sandbox -Force | Out-Null
$path = Join-Path $sandbox 'manifest.json'
$m = Read-BaknretManifest -Path $path
Assert-Equal 0 $m.items.Count
$m.compressor = [pscustomobject]@{ name = '7z'; version = '26.03' }
$m.items['FooClolor_from_C_+Programs'] = [ordered]@{ baseName = 'FooClolor_from_C_+Programs'; action = 'backed-up'; archiveBytes = 12345 }
Write-BaknretManifest -Path $path -Manifest $m | Out-Null
$again = Read-BaknretManifest -Path $path
Assert-Equal 1 $again.items.Count
Assert-Equal 'backed-up' $again.items['FooClolor_from_C_+Programs'].action
$bytes = [System.IO.File]::ReadAllBytes($path)
Assert-False (($bytes[0] -eq 0xEF) -and ($bytes[1] -eq 0xBB)) 'manifest 不应带 BOM'
}
Test-Case 'manifest 损坏时不抛异常' {
$path = Join-Path $sandbox 'broken.json'
[System.IO.File]::WriteAllText($path, '{ this is not json')
$m = Read-BaknretManifest -Path $path
Assert-Equal 0 $m.items.Count
}
Test-Case '配置缺失时返回默认值' {
$config = Get-BaknretConfig -Path (Join-Path $sandbox 'nope.psd1')
Assert-Equal 'Backups' $config.BackupDir
Assert-True ($config.MinFreeSpaceGB -gt 0)
}
Test-Case '配置嵌套段落合并且不丢默认键' {
$path = Join-Path $sandbox 'cfg.psd1'
Set-Content -LiteralPath $path -Value "@{ Encryption = @{ Enabled = `$true }; CompressionLevel = 5 }"
$config = Get-BaknretConfig -Path $path
Assert-Equal 5 $config.CompressionLevel
Assert-True $config.Encryption.Enabled
Assert-True $config.Encryption.EncryptHeaders '未覆盖的嵌套键应保留默认值'
}
Test-Case '口令:环境变量可读取,取不到返回 $null' {
$old = $env:BAKNRET_PASSWORD
try {
Remove-Item Env:BAKNRET_PASSWORD -ErrorAction SilentlyContinue
Assert-Null (Get-BaknretPassword -PasswordFile (Join-Path $sandbox 'nope'))
$env:BAKNRET_PASSWORD = 'from-env'
Assert-Equal 'from-env' (Get-BaknretPassword)
} finally {
Remove-Item Env:BAKNRET_PASSWORD -ErrorAction SilentlyContinue
if ($old) { $env:BAKNRET_PASSWORD = $old }
}
}
# ============================================================================
Write-Host "`n== 软件名录 ==" -ForegroundColor Cyan
# ============================================================================
$catalogSandbox = Join-Path $env:TEMP ("baknret-cat-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Path $catalogSandbox -Force | Out-Null
Test-Case '名录解析:裸键、引号键、带 - 与 . 的名字' {
$catPath = Join-Path $catalogSandbox 'SoftwareCatalog.psd1'
$content = "@{`n FooClolor = 'C:\Programs\FooClolor'`n 'scoop-config' = '%UserProfile%\.config\scoop'`n '.ssh' = '%UserProfile%\.ssh'`n}`n"
[System.IO.File]::WriteAllText($catPath, $content, [System.Text.UTF8Encoding]::new($false))
$catalog = Get-SoftwareCatalog -Path $catPath
Assert-Equal 3 $catalog.Count
Assert-True $catalog.ContainsKey('FooClolor')
Assert-True $catalog.ContainsKey('scoop-config') '带连字符的键必须能解析'
Assert-True $catalog.ContainsKey('.ssh') '带点的键必须能解析'
}
Test-Case '名录解析:目录带版本后缀时按前缀补全(legendary_2.0.4)' {
$root = Join-Path $catalogSandbox 'probe'
New-Item -ItemType Directory -Path (Join-Path $root 'legendary_2.0.4') -Force | Out-Null
$catPath = Join-Path $catalogSandbox 'suffix.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n legendary = '$root\legendary'`n}`n", [System.Text.UTF8Encoding]::new($false))
$catalog = Get-SoftwareCatalog -Path $catPath
Assert-Equal 'Single' $catalog['legendary'].Kind
Assert-True ($catalog['legendary'].ResolvedPath -like '*legendary_2.0.4') '应补全到实际目录'
}
Test-Case '名录解析:不会把 Legendary 误配成 LegendarySomething' {
$root = Join-Path $catalogSandbox 'strict'
New-Item -ItemType Directory -Path (Join-Path $root 'LegendarySomething') -Force | Out-Null
$catPath = Join-Path $catalogSandbox 'strict.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n Legendary = '$root\Legendary'`n}`n", [System.Text.UTF8Encoding]::new($false))
$catalog = Get-SoftwareCatalog -Path $catPath
Assert-Equal 'Unresolved' $catalog['Legendary'].Kind '必须以下一个字符是 _ 或 - 为界'
}
Test-Case '软件名条目:默认用软件名做归档名' {
$item = ConvertFrom-BackupListLine -Line 'FooClolor'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath (Join-Path $catalogSandbox 'SoftwareCatalog.psd1')
Assert-Equal 'FooClolor' $resolved.BaseName
Assert-Equal 'name' $resolved.ArchiveFlavor
Assert-True $resolved.IsName
}
Test-Case '字面路径条目:仍用路径命名算法(现有清单无需改写)' {
$item = ConvertFrom-BackupListLine -Line 'C:\Programs\FooClolor'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath (Join-Path $catalogSandbox 'SoftwareCatalog.psd1')
Assert-False $resolved.IsName
Assert-Equal 'path' $resolved.ArchiveFlavor
Assert-Equal 'FooClolor_from_C_+Programs' $resolved.BaseName
}
Test-Case '@pathname 用真实路径命名,而不是软件名' {
$item = ConvertFrom-BackupListLine -Line 'FooClolor @pathname'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath (Join-Path $catalogSandbox 'SoftwareCatalog.psd1')
Assert-Equal 'path' $resolved.ArchiveFlavor
Assert-Equal 'FooClolor_from_C_+Programs' $resolved.BaseName
}
Test-Case '名录里没有该名字:BaseName 退回可读目录名,并给出 Error' {
$item = ConvertFrom-BackupListLine -Line 'no-such-thing'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath (Join-Path $catalogSandbox 'SoftwareCatalog.psd1')
Assert-Equal 'no-such-thing' $resolved.BaseName
Assert-True ($null -ne $resolved.Error) '应给出错误说明'
Assert-Equal 0 $resolved.Sources.Count
}
Test-Case '名录里的路径不存在时仍给出 Sources(恢复要靠它还原回原位)' {
$catPath = Join-Path $catalogSandbox 'gone.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n 'gone-app' = 'C:\definitely\not\here'`n}`n", [System.Text.UTF8Encoding]::new($false))
$item = ConvertFrom-BackupListLine -Line 'gone-app'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $catPath
Assert-True ($null -ne $resolved.Error) '备份端据 Error 跳过'
Assert-Equal 1 $resolved.Sources.Count '恢复端据 Sources 还原'
Assert-Equal 'C:\definitely\not\here' $resolved.Sources[0].SourcePath
}
Test-Case 'Includes:分文件维护的名录会被合并' {
$basePath = Join-Path $catalogSandbox 'Base.psd1'
$extraPath = Join-Path $catalogSandbox 'Extra.psd1'
[System.IO.File]::WriteAllText($extraPath, "@{`n 'extra-app' = 'C:\Programs\Extra'`n}`n", [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($basePath, "@{`n Includes = @('Extra.psd1')`n 'base-app' = 'C:\Programs\Base'`n}`n", [System.Text.UTF8Encoding]::new($false))
$catalog = Get-SoftwareCatalog -Path $basePath
Assert-True $catalog.ContainsKey('base-app')
Assert-True $catalog.ContainsKey('extra-app') 'Includes 引入的条目也应在'
}
Test-Case '软件名会被规范化成合法文件名' {
Assert-Equal 'keep%and&and+plus' (Format-CatalogName -Name 'keep%and&and+plus')
Assert-True ((Format-CatalogName -Name 'a/b:c') -notmatch '[/:]') '非法字符应被替换'
}
Remove-Item -LiteralPath $catalogSandbox -Recurse -Force -ErrorAction SilentlyContinue
# ============================================================================
Write-Host "`n== 集成测试:真的跑 7z,验证排除结果落在文件系统上 ==" -ForegroundColor Cyan
# ============================================================================
$sevenZip = Get-Command 7z -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source
if (-not $sevenZip) {
Write-Host ' 跳过:未找到 7z' -ForegroundColor Yellow
} else {
Test-Case '排除规则与空格处理在真实归档上生效' {
$root = Join-Path $sandbox 'src'
$itemName = 'User Data'
$source = Join-Path $root $itemName
New-Item -ItemType Directory -Path $root -Force | Out-Null
foreach ($d in 'Default\Cache', 'Default\Code Cache', 'Default\Extensions', 'component_crx_cache', 'Default\IndexedDB') {
New-Item -ItemType Directory -Path (Join-Path $source $d) -Force | Out-Null
}
Set-Content -LiteralPath (Join-Path $source 'keep.txt') 'keep'
Set-Content -LiteralPath (Join-Path $source 'Default\Cache\c.bin') 'c'
Set-Content -LiteralPath (Join-Path $source 'Default\Code Cache\cc.bin') 'cc'
Set-Content -LiteralPath (Join-Path $source 'Default\Extensions\e.bin') 'e'
Set-Content -LiteralPath (Join-Path $source 'component_crx_cache\x.bin') 'x'
Set-Content -LiteralPath (Join-Path $source 'Default\IndexedDB\i.bin') 'i'
$patterns = @('!*Cache', 'component_crx_cache', 'Default\Code Cache', 'Default\Extensions', 'Default\IndexedDB')
$excludeArgs = Get-ArchiveExcludeArgument -ItemName $itemName -Patterns $patterns
$archive = Join-Path $sandbox 'excl.7z'
$argument = @('a', '-t7z', '-mx=1', '-bso0', '-bsp0') + $excludeArgs + @($archive, $itemName)
$code = Invoke-ExternalCommand -FilePath $sevenZip -ArgumentList $argument -WorkingDirectory $root
Assert-Equal 0 $code '7z 打包退出码'
$verify = Join-Path $sandbox 'verify'
New-Item -ItemType Directory -Path $verify -Force | Out-Null
$code = Invoke-ExternalCommand -FilePath $sevenZip -ArgumentList @('x', '-bso0', '-bsp0', '-y', "-o$verify", $archive)
Assert-Equal 0 $code '7z 解压退出码'
Assert-True (Test-Path -LiteralPath (Join-Path $verify "$itemName\keep.txt")) '保留的文件应当存在'
Assert-False (Test-Path -LiteralPath (Join-Path $verify "$itemName\Default\Cache\c.bin")) 'Default\Cache 应被 !*Cache 排除'
Assert-False (Test-Path -LiteralPath (Join-Path $verify "$itemName\Default\Code Cache\cc.bin")) 'Code Cache 应被排除(空格转 ? 生效)'
Assert-False (Test-Path -LiteralPath (Join-Path $verify "$itemName\Default\Extensions\e.bin")) 'Extensions 应被排除'
Assert-False (Test-Path -LiteralPath (Join-Path $verify "$itemName\component_crx_cache\x.bin")) 'component_crx_cache 应被排除'
Assert-False (Test-Path -LiteralPath (Join-Path $verify "$itemName\Default\IndexedDB\i.bin")) 'IndexedDB 应被排除'
}
Test-Case '对照组:不加排除时被排除的文件确实在归档里(证明上一条不是空归档)' {
$root = Join-Path $sandbox 'src'
$itemName = 'User Data'
$archive = Join-Path $sandbox 'full.7z'
$code = Invoke-ExternalCommand -FilePath $sevenZip -ArgumentList @('a', '-t7z', '-mx=1', '-bso0', '-bsp0', $archive, $itemName) -WorkingDirectory $root
Assert-Equal 0 $code
$verify = Join-Path $sandbox 'verify-full'
New-Item -ItemType Directory -Path $verify -Force | Out-Null
$code = Invoke-ExternalCommand -FilePath $sevenZip -ArgumentList @('x', '-bso0', '-bsp0', '-y', "-o$verify", $archive)
Assert-Equal 0 $code
Assert-True (Test-Path -LiteralPath (Join-Path $verify "$itemName\Default\Cache\c.bin")) '对照组应当包含被排除的那个文件'
}
Test-Case '7z t 对完好归档返回 0,对损坏归档返回非 0' {
$good = Join-Path $sandbox 'full.7z'
$code = Invoke-ExternalCommand -FilePath $sevenZip -ArgumentList @('t', '-bso0', '-bsp0', $good)
Assert-Equal 0 $code '完好归档应当校验通过'
$bad = Join-Path $sandbox 'corrupt.7z'
$bytes = [System.IO.File]::ReadAllBytes($good)
for ($i = [math]::Max(0, $bytes.Length - 40); $i -lt $bytes.Length; $i++) { $bytes[$i] = 0xFF }
[System.IO.File]::WriteAllBytes($bad, $bytes)
$code = Invoke-ExternalCommand -FilePath $sevenZip -ArgumentList @('t', '-bso0', '-bsp0', $bad)
Assert-True ($code -ne 0) '损坏的归档应当校验失败'
}
}
# ============================================================================
Write-Host "`n== 外部命令退出码 ==" -ForegroundColor Cyan
# ============================================================================
Test-Case 'Invoke-ExternalCommand 能拿到真实退出码(旧实现用 Start-Process 拿不到)' {
$code = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', 'exit 7')
Assert-Equal 7 $code
}
# ============================================================================
Remove-Item -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue
$failed = Write-TestSummary -Title '单元测试'
if ($failed -gt 0) { exit 1 }
exit 0