改造前:全仓 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),与产品代码无关。
1145 lines
55 KiB
PowerShell
1145 lines
55 KiB
PowerShell
<#
|
||
.SYNOPSIS
|
||
BakNRet 测试套件(零依赖,Windows PowerShell 5.1 与 PowerShell 7.x 均可运行)。
|
||
|
||
.DESCRIPTION
|
||
为什么不用 Pester:本机只装了 Pester 3.4.0,`Should -Be` 这类 Pester 4+ 语法
|
||
直接语法错误,而为了跑测试去装 Pester 5 不值得。这里自带一个极简断言运行器,
|
||
对备份工具这种小脚本来说更可靠。
|
||
|
||
覆盖范围(与重构后的 BackupList / SoftwareCatalog 契约一致):
|
||
* BackupList.txt 行语法:方向标记、`::` 路径覆盖、`:-` / `:+`、`:encrypt`、
|
||
`@ Key='Value'` 覆盖、历史写法、行尾 `# 说明`、记号边界
|
||
* 软件名录新结构(`软件名 -> Slot -> @{ Path = ... }`):Slot 排序、前缀补全、
|
||
多候选报错、`%VAR%` / `$( ... )` 展开、缓存与 -NoCache
|
||
* 归档命名与逆向解析(软件名 / 字面路径 / @pathname)
|
||
* 归档项与归档内布局(New-BaknretArchiveItem / 暂存 junction / 硬链接)
|
||
* 7z 排除参数翻译(!前缀、!re: 正则展开、Slot 前缀分配、去重)
|
||
* Windows 命令行拼接
|
||
* manifest 读写往返与 layouts
|
||
* 配置默认值与嵌套合并
|
||
* 集成测试:真的调用 7z 打包 + 解压,验证排除结果落在文件系统上
|
||
|
||
.EXAMPLE
|
||
pwsh -File .\tests\Run-Tests.ps1
|
||
#>
|
||
|
||
[CmdletBinding()]
|
||
param(
|
||
[string]$ModulePath
|
||
)
|
||
|
||
$ErrorActionPreference = 'Stop'
|
||
|
||
# 默认值不能写在 param() 里:Windows PowerShell 5.1 在带 [CmdletBinding()] 的脚本上,
|
||
# 参数绑定阶段还没有给 $PSScriptRoot 赋值,默认值表达式会拿到空串(实测:带
|
||
# [CmdletBinding()] -> 空串,不带 -> 正常;PowerShell 7 两种都正常)。所以默认值
|
||
# 一律在这里补 —— 这也是本仓库对 -BackupDir / -ConfigPath 一直在用的写法。
|
||
if (-not $ModulePath) { $ModulePath = Join-Path (Split-Path -Parent $PSScriptRoot) 'Common.psm1' }
|
||
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 '只有方向标记没有目标时返回 $null(不抛异常)' {
|
||
Assert-Null (ConvertFrom-BackupListLine -Line '+')
|
||
Assert-Null (ConvertFrom-BackupListLine -Line '-')
|
||
}
|
||
|
||
Test-Case '只有修饰符没有目标时返回 $null' {
|
||
Assert-Null (ConvertFrom-BackupListLine -Line '::')
|
||
Assert-Null (ConvertFrom-BackupListLine -Line ':- x')
|
||
}
|
||
|
||
Test-Case '裸路径:方向 both、按字面路径处理、没有任何覆盖' {
|
||
$r = ConvertFrom-BackupListLine -Line 'C:\Programs\FooClolor'
|
||
Assert-Equal 'C:\Programs\FooClolor' $r.Path
|
||
Assert-Equal 'both' $r.Direction
|
||
Assert-False $r.IsName
|
||
Assert-Equal 0 $r.Overrides.Keys.Count
|
||
Assert-Equal 0 $r.ExcludePatterns.Count
|
||
Assert-Equal 0 $r.Flags.Count
|
||
Assert-Equal 0 $r.UnknownKeys.Count
|
||
}
|
||
|
||
Test-Case '行首 + 表示仅备份' {
|
||
$r = ConvertFrom-BackupListLine -Line '+ C:\Programs\FooClolor :- logs\'
|
||
Assert-Equal 'backup' $r.Direction
|
||
Assert-Equal 'C:\Programs\FooClolor' $r.Path
|
||
Assert-Equal 1 $r.ExcludePatterns.Count
|
||
}
|
||
|
||
Test-Case '行首 - 表示仅恢复' {
|
||
$r = ConvertFrom-BackupListLine -Line '- C:\Programs\FooClolor'
|
||
Assert-Equal 'restore' $r.Direction
|
||
Assert-Equal 'C:\Programs\FooClolor' $r.Path
|
||
}
|
||
|
||
Test-Case '没有方向标记时是 both(同时备份与恢复)' {
|
||
Assert-Equal 'both' (ConvertFrom-BackupListLine -Line 'C:\x').Direction
|
||
}
|
||
|
||
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]
|
||
Assert-True $r.Overrides.ContainsKey('Exclude')
|
||
}
|
||
|
||
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
|
||
Assert-Equal 'cache\*' $r.ExcludePatterns[0]
|
||
}
|
||
|
||
Test-Case '模式值本身可以带引号(真实清单里的 :- ''a,b'' 写法)' {
|
||
$r = ConvertFrom-BackupListLine -Line "C:\x :- 'a,b'"
|
||
Assert-Equal 2 $r.ExcludePatterns.Count
|
||
Assert-Equal 'a' $r.ExcludePatterns[0]
|
||
Assert-Equal 'b' $r.ExcludePatterns[1]
|
||
}
|
||
|
||
Test-Case '整行被一对引号包住时是一个记号:引号内的 :: / :- 不再是指令' {
|
||
$line = '"C:\Programs\ScoopApps\persist :- persist\a\DawnCache,persist\a\GPUCache"'
|
||
$r = ConvertFrom-BackupListLine -Line $line
|
||
Assert-Equal 'C:\Programs\ScoopApps\persist :- persist\a\DawnCache,persist\a\GPUCache' $r.Path
|
||
Assert-Equal 0 $r.ExcludePatterns.Count
|
||
Assert-Equal 0 $r.Overrides.Keys.Count
|
||
}
|
||
|
||
Test-Case ':: 现在表示覆盖 Path,而不是 :- 的历史别名' {
|
||
$r = ConvertFrom-BackupListLine -Line 'MyApp :: C:\overridden'
|
||
Assert-Equal 'MyApp' $r.Path
|
||
Assert-True $r.Overrides.ContainsKey('Path') '必须记下 Path 覆盖'
|
||
Assert-Equal 'C:\overridden' $r.Overrides['Path']
|
||
Assert-False $r.Overrides.ContainsKey('Exclude') ':: 不能再被当成排除'
|
||
Assert-Equal 0 $r.ExcludePatterns.Count
|
||
}
|
||
|
||
Test-Case ':- 与 @ Exclude= 走同一条路(都是排除)' {
|
||
$a = ConvertFrom-BackupListLine -Line 'X :- logs\'
|
||
$b = ConvertFrom-BackupListLine -Line "X @ Exclude='logs\'"
|
||
Assert-Equal 'logs\' $a.ExcludePatterns[0]
|
||
Assert-Equal 'logs\' $b.ExcludePatterns[0]
|
||
}
|
||
|
||
Test-Case ':+ 追加包含项:一个记号可以带多个逗号分隔的值' {
|
||
$r = ConvertFrom-BackupListLine -Line 'MyApp :+ ModA:D:\a,ModB:D:\b'
|
||
Assert-Equal 'MyApp' $r.Path
|
||
Assert-True $r.Overrides.ContainsKey('Include')
|
||
Assert-Equal 2 $r.Includes.Count
|
||
Assert-Equal 'ModA:D:\a' $r.Includes[0]
|
||
Assert-Equal 'ModB:D:\b' $r.Includes[1]
|
||
Assert-Equal 0 $r.ExcludePatterns.Count
|
||
}
|
||
|
||
Test-Case ':- 与 :+ 混用且顺序任意(每个记号带自己的值表)' {
|
||
$r = ConvertFrom-BackupListLine -Line 'MyApp :- logs\,!*Cache :+ Mod:D:\a'
|
||
Assert-Equal 'MyApp' $r.Path
|
||
Assert-Equal 1 $r.Includes.Count
|
||
Assert-Equal 'Mod:D:\a' $r.Includes[0]
|
||
Assert-Equal 2 $r.ExcludePatterns.Count
|
||
Assert-Equal 'logs\' $r.ExcludePatterns[0]
|
||
Assert-Equal '!*Cache' $r.ExcludePatterns[1]
|
||
}
|
||
|
||
Test-Case '同一行重复写 :- / :+ 时累积(不静默丢掉前一条规则)' {
|
||
$r = ConvertFrom-BackupListLine -Line 'MyApp :- logs\ :+ D:\a :- !*Cache'
|
||
Assert-Equal 2 $r.ExcludePatterns.Count
|
||
Assert-Equal 'logs\' $r.ExcludePatterns[0]
|
||
Assert-Equal '!*Cache' $r.ExcludePatterns[1]
|
||
Assert-Equal 1 $r.Includes.Count
|
||
Assert-Equal 'D:\a' $r.Includes[0]
|
||
}
|
||
|
||
Test-Case ':encrypt / :!encrypt 直接给出布尔覆盖' {
|
||
$on = ConvertFrom-BackupListLine -Line 'MyApp :encrypt'
|
||
Assert-True $on.Overrides.ContainsKey('Encrypt')
|
||
Assert-Equal $true $on.Overrides['Encrypt']
|
||
$off = ConvertFrom-BackupListLine -Line 'MyApp :!encrypt'
|
||
Assert-Equal $false $off.Overrides['Encrypt']
|
||
}
|
||
|
||
Test-Case '@ Key=''Value'' 覆盖:Exclude / Include / Encrypt' {
|
||
$r = ConvertFrom-BackupListLine -Line "App @ Exclude='a,b' @ Include='Mod:D:\m' @ Encrypt='`$false'"
|
||
Assert-Equal 2 $r.ExcludePatterns.Count
|
||
Assert-Equal 1 $r.Includes.Count
|
||
Assert-Equal 'Mod:D:\m' $r.Includes[0]
|
||
Assert-Equal $false $r.Overrides['Encrypt']
|
||
}
|
||
|
||
Test-Case '@ Encrypt=''yes'' 之类的真值也能被识别' {
|
||
foreach ($truthy in '$true', 'true', '1', 'yes', 'on') {
|
||
$r = ConvertFrom-BackupListLine -Line "App @ Encrypt='$truthy'"
|
||
Assert-Equal $true $r.Overrides['Encrypt'] "值 $truthy 应视为真"
|
||
}
|
||
foreach ($falsy in '$false', 'false', '0', 'no', 'off') {
|
||
$r = ConvertFrom-BackupListLine -Line "App @ Encrypt='$falsy'"
|
||
Assert-Equal $false $r.Overrides['Encrypt'] "值 $falsy 应视为假"
|
||
}
|
||
}
|
||
|
||
Test-Case '未知的 @ 字段被记入 UnknownKeys 而不是静默吞掉' {
|
||
$r = ConvertFrom-BackupListLine -Line "App @ UnknownKey='v'"
|
||
Assert-Equal 1 $r.UnknownKeys.Count
|
||
Assert-Equal 'UnknownKey' $r.UnknownKeys[0]
|
||
Assert-Equal 0 $r.Overrides.Keys.Count
|
||
}
|
||
|
||
Test-Case '历史写法 @encrypt / @!encrypt 仍然被识别成加密覆盖' {
|
||
$on = ConvertFrom-BackupListLine -Line '%UserProfile%\.ssh @encrypt'
|
||
Assert-Equal '%UserProfile%\.ssh' $on.Path
|
||
Assert-True $on.Overrides.ContainsKey('Encrypt')
|
||
Assert-Equal $true $on.Overrides['Encrypt']
|
||
Assert-Equal 0 $on.Flags.Count '不再作为无意义的 Flags 传下去'
|
||
|
||
$off = ConvertFrom-BackupListLine -Line '%UserProfile%\.ssh @!encrypt'
|
||
Assert-Equal $false $off.Overrides['Encrypt']
|
||
}
|
||
|
||
Test-Case '历史写法 @pathname / @root=<名> 被记入 Flags' {
|
||
Assert-Equal 'pathname' (ConvertFrom-BackupListLine -Line 'App @pathname').Flags[0]
|
||
Assert-Equal 'root=xyz' (ConvertFrom-BackupListLine -Line 'App @root=xyz').Flags[0]
|
||
}
|
||
|
||
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 $true $r.Overrides['Encrypt']
|
||
}
|
||
|
||
Test-Case '盘符里的单个冒号不被误当分隔符' {
|
||
$r = ConvertFrom-BackupListLine -Line 'C:\Programs\Foo'
|
||
Assert-Equal 'C:\Programs\Foo' $r.Path
|
||
Assert-Equal 0 $r.Includes.Count
|
||
Assert-Equal 0 $r.ExcludePatterns.Count
|
||
}
|
||
|
||
Test-Case '标记必须是独立记号:C:\a:-b 仍然是一个路径' {
|
||
$r = ConvertFrom-BackupListLine -Line 'C:\a:-b'
|
||
Assert-Equal 'C:\a:-b' $r.Path
|
||
Assert-Equal 0 $r.Overrides.Keys.Count
|
||
Assert-Null (Test-BaknretMarker -Token 'C:\a:-b')
|
||
Assert-Null (Test-BaknretMarker -Token ':+X')
|
||
}
|
||
|
||
Test-Case 'Test-BaknretMarker 识别全部标记(大小写敏感)' {
|
||
Assert-Equal 'path' (Test-BaknretMarker -Token '::')
|
||
Assert-Equal 'exclude' (Test-BaknretMarker -Token ':-')
|
||
Assert-Equal 'include' (Test-BaknretMarker -Token ':+')
|
||
Assert-Equal 'encrypt' (Test-BaknretMarker -Token ':encrypt')
|
||
Assert-Equal 'noencrypt' (Test-BaknretMarker -Token ':!encrypt')
|
||
Assert-Equal 'at' (Test-BaknretMarker -Token '@x')
|
||
Assert-Equal 'path' (Test-BaknretMarker -Token ' :: ')
|
||
}
|
||
|
||
Test-Case '记号里有空格时目标可以带空格(第一个修饰符之前全是目标)' {
|
||
$r = ConvertFrom-BackupListLine -Line 'My App With Spaces :- logs\'
|
||
Assert-Equal 'My App With Spaces' $r.Path
|
||
Assert-Equal 1 $r.ExcludePatterns.Count
|
||
}
|
||
|
||
Test-Case '行尾 # 说明会被摘出来' {
|
||
$r = ConvertFrom-BackupListLine -Line 'App :- a\b # 这是说明'
|
||
Assert-Equal 'App' $r.Path
|
||
Assert-Equal '这是说明' $r.Comment
|
||
Assert-Equal 'a\b' $r.ExcludePatterns[0]
|
||
}
|
||
|
||
Test-Case '路径里紧贴的 # 不会被当成注释' {
|
||
$r = ConvertFrom-BackupListLine -Line 'C:\p\a#b'
|
||
Assert-Equal 'C:\p\a#b' $r.Path
|
||
Assert-Null $r.Comment
|
||
}
|
||
|
||
Test-Case '没有说明时 Comment 为 $null' {
|
||
Assert-Null (ConvertFrom-BackupListLine -Line 'App').Comment
|
||
}
|
||
|
||
Test-Case 'Split-BaknretToken:引号内的空白不切分,引号保留' {
|
||
$tokens = @(Split-BaknretToken -Text 'a "b c" d :- ''e,f''')
|
||
Assert-Equal 5 $tokens.Count
|
||
Assert-Equal 'a' $tokens[0]
|
||
Assert-Equal '"b c"' $tokens[1]
|
||
Assert-Equal 'd' $tokens[2]
|
||
Assert-Equal ':-' $tokens[3]
|
||
Assert-Equal "'e,f'" $tokens[4]
|
||
}
|
||
|
||
Test-Case 'Remove-BaknretQuote:成对引号去掉,不成对原样返回' {
|
||
Assert-Equal 'a' (Remove-BaknretQuote -Text "'a'")
|
||
Assert-Equal 'a' (Remove-BaknretQuote -Text '"a"')
|
||
Assert-Equal '"a' (Remove-BaknretQuote -Text '"a')
|
||
Assert-Equal 'plain' (Remove-BaknretQuote -Text 'plain')
|
||
}
|
||
|
||
Test-Case 'Test-LiteralPath:带分隔符或 % 的按路径,其余按软件名' {
|
||
Assert-True (Test-LiteralPath -Path 'C:\a')
|
||
Assert-True (Test-LiteralPath -Path 'C:/a')
|
||
Assert-True (Test-LiteralPath -Path '%UserProfile%\.ssh')
|
||
Assert-False (Test-LiteralPath -Path 'MyApp')
|
||
Assert-True (Test-LiteralPath -Path '')
|
||
}
|
||
|
||
# ============================================================================
|
||
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 '盘符冒号被归一化成下划线' {
|
||
Assert-Equal 'a_from_C_' (Get-BackupBaseName -RawPath 'C:\a')
|
||
}
|
||
|
||
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"
|
||
}
|
||
Assert-Equal 'keep%and&and+plus' (Format-CatalogName -Name 'keep%and&and+plus')
|
||
}
|
||
|
||
Test-Case 'Get-BaknretArchiveTopName:取归档内相对路径的第一段' {
|
||
Assert-Equal 'Slot' (Get-BaknretArchiveTopName -ArchivePath 'Slot\a\b')
|
||
Assert-Equal 'Slot' (Get-BaknretArchiveTopName -ArchivePath '\Slot\')
|
||
Assert-Equal 'leaf' (Get-BaknretArchiveTopName -ArchivePath 'leaf')
|
||
Assert-Equal '' (Get-BaknretArchiveTopName -ArchivePath ' ')
|
||
}
|
||
|
||
# ============================================================================
|
||
Write-Host "`n== 归档项构造与暂存 ==" -ForegroundColor Cyan
|
||
# ============================================================================
|
||
|
||
$itemSandbox = Join-Path $env:TEMP ("baknret-item-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||
|
||
Test-Case 'New-BaknretArchiveItem:归档内路径被清理,TopName 是第一段' {
|
||
$item = New-BaknretArchiveItem -ArchivePath '\AlphaData\' -RealPath 'C:\x'
|
||
Assert-Equal 'AlphaData' $item.ArchivePath
|
||
Assert-Equal 'AlphaData' $item.TopName
|
||
Assert-Equal 'slot' $item.Kind
|
||
Assert-Equal 'catalog' $item.Origin
|
||
Assert-False $item.Exists
|
||
Assert-False $item.IsFile
|
||
Assert-Equal 0 $item.Exclude.Count
|
||
|
||
$nested = New-BaknretArchiveItem -ArchivePath 'Alpha\sub' -RealPath 'C:\y' -Kind 'include' -Origin 'include'
|
||
Assert-Equal 'Alpha' $nested.TopName
|
||
Assert-Equal 'include' $nested.Kind
|
||
}
|
||
|
||
Test-Case '归档项的属性集与契约一致(没有旧的 Sources / Dirs / Variants)' {
|
||
$item = New-BaknretArchiveItem -ArchivePath 'A' -RealPath 'C:\a'
|
||
foreach ($expected in 'ArchivePath', 'TopName', 'RealPath', 'Kind', 'Slot', 'Description', 'Origin', 'Exists', 'IsFile', 'Exclude') {
|
||
Assert-True ($item.PSObject.Properties.Name -contains $expected) "缺少属性 $expected"
|
||
}
|
||
foreach ($gone in 'Sources', 'Dirs', 'Variants', 'ResolvedPath', 'RootName') {
|
||
Assert-False ($item.PSObject.Properties.Name -contains $gone) "不该再有旧属性 $gone"
|
||
}
|
||
}
|
||
|
||
Test-Case 'New-BaknretArchiveStaging:目录走 junction、文件走硬链接,按归档内名字挂载' {
|
||
$root = Join-Path $itemSandbox 'stage-src'
|
||
New-Item -ItemType Directory -Path (Join-Path $root 'App') -Force | Out-Null
|
||
Set-Content -LiteralPath (Join-Path $root 'App\f.txt') -Value 'x'
|
||
Set-Content -LiteralPath (Join-Path $root 'settings.json') -Value '{}'
|
||
|
||
$items = @(
|
||
(New-BaknretArchiveItem -ArchivePath 'AlphaData' -RealPath (Join-Path $root 'App') -Exists $true),
|
||
(New-BaknretArchiveItem -ArchivePath 'BetaFile' -RealPath (Join-Path $root 'settings.json') -Exists $true -IsFile $true)
|
||
)
|
||
|
||
$staging = New-BaknretArchiveStaging -Items $items -Root (Join-Path $itemSandbox 'stage')
|
||
try {
|
||
$dirLink = Get-Item -LiteralPath (Join-Path $staging 'AlphaData') -Force
|
||
$fileLink = Get-Item -LiteralPath (Join-Path $staging 'BetaFile') -Force
|
||
Assert-Equal 'Junction' $dirLink.LinkType
|
||
Assert-Equal 'HardLink' $fileLink.LinkType
|
||
Assert-True (Test-Path -LiteralPath (Join-Path $staging 'AlphaData\f.txt')) '应能穿过 junction 看到内容'
|
||
} finally {
|
||
Remove-BaknretArchiveStaging -Root $staging
|
||
}
|
||
|
||
Assert-False (Test-Path -LiteralPath $staging) '暂存目录应被清理'
|
||
Assert-True (Test-Path -LiteralPath (Join-Path $root 'App\f.txt')) '清理暂存绝不能删到真实数据'
|
||
}
|
||
|
||
Test-Case 'New-BaknretJunction / Remove-BaknretJunction:只删连接点,不动目标' {
|
||
$target = Join-Path $itemSandbox 'j-target'
|
||
$link = Join-Path $itemSandbox 'j-link'
|
||
New-Item -ItemType Directory -Path $target -Force | Out-Null
|
||
Set-Content -LiteralPath (Join-Path $target 'keep.txt') -Value 'keep'
|
||
|
||
New-BaknretJunction -Path $link -Target $target | Out-Null
|
||
Assert-Equal 'Junction' (Get-Item -LiteralPath $link -Force).LinkType
|
||
Remove-BaknretJunction -Path $link
|
||
Assert-False (Test-Path -LiteralPath $link)
|
||
Assert-True (Test-Path -LiteralPath (Join-Path $target 'keep.txt')) '目标内容必须留着'
|
||
|
||
try {
|
||
New-BaknretJunction -Path $link -Target $target | Out-Null
|
||
Remove-BaknretJunction -Path $link
|
||
New-BaknretJunction -Path $link -Target $target | Out-Null
|
||
Assert-True $true
|
||
} finally {
|
||
Remove-BaknretJunction -Path $link
|
||
}
|
||
}
|
||
|
||
Test-Case 'New-BaknretJunction:目标已存在时明确抛错' {
|
||
$target = Join-Path $itemSandbox 'j2-target'
|
||
$link = Join-Path $itemSandbox 'j2-link'
|
||
New-Item -ItemType Directory -Path $target -Force | Out-Null
|
||
New-Item -ItemType Directory -Path $link -Force | Out-Null
|
||
|
||
$threw = $false
|
||
try { New-BaknretJunction -Path $link -Target $target | Out-Null } catch { $threw = $true }
|
||
Assert-True $threw '同名实体存在时必须抛错,不能静默替换'
|
||
}
|
||
|
||
# ============================================================================
|
||
Write-Host "`n== 7z 排除参数翻译 ==" -ForegroundColor Cyan
|
||
# ============================================================================
|
||
|
||
Test-Case '旧的 Get-ArchiveExcludeArgument 已经不存在(契约破坏点)' {
|
||
Assert-Null (Get-Command Get-ArchiveExcludeArgument -ErrorAction SilentlyContinue)
|
||
}
|
||
|
||
Test-Case '相对模式自动补上归档项名(锚定在归档根)' {
|
||
$item = New-BaknretArchiveItem -ArchivePath 'March7thAssistant' -RealPath 'C:\x'
|
||
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('logs\')
|
||
Assert-Equal 1 $exclude.Arguments.Count
|
||
Assert-Equal '-x!March7thAssistant\logs' $exclude.Arguments[0]
|
||
Assert-Null $exclude.Error
|
||
}
|
||
|
||
Test-Case '模式里再写一遍归档项名不会被当成前缀剥掉(新语义:总是拼在项名之后)' {
|
||
$item = New-BaknretArchiveItem -ArchivePath 'March7thAssistant' -RealPath 'C:\x'
|
||
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('March7thAssistant\logs\')
|
||
Assert-Equal '-x!March7thAssistant\March7thAssistant\logs' $exclude.Arguments[0]
|
||
}
|
||
|
||
Test-Case '! 前缀翻译成任意层级匹配' {
|
||
$item = New-BaknretArchiveItem -ArchivePath 'UserData' -RealPath 'C:\x'
|
||
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('!*Cache')
|
||
Assert-Equal '-xr!*Cache' $exclude.Arguments[0]
|
||
}
|
||
|
||
Test-Case '模式里的空格转成 ? (7z 的模式不支持空格)' {
|
||
$item = New-BaknretArchiveItem -ArchivePath 'UserData' -RealPath 'C:\x'
|
||
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('Default\Code Cache')
|
||
Assert-Equal '-x!UserData\Default\Code?Cache' $exclude.Arguments[0]
|
||
}
|
||
|
||
Test-Case '生成的参数里绝不出现引号(旧实现 -x!"路径" 让排除全部失效)' {
|
||
$item = New-BaknretArchiveItem -ArchivePath 'UserData' -RealPath 'C:\x'
|
||
$patterns = @('Default\Code Cache', '!*Cache', 'logs\', 'UserData\3rdparty\WebBrowser\UserProfile\Integrated')
|
||
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns $patterns
|
||
Assert-Equal 4 $exclude.Arguments.Count
|
||
foreach ($argument in $exclude.Arguments) {
|
||
Assert-False ($argument.Contains('"')) "参数里出现了引号: $argument"
|
||
}
|
||
}
|
||
|
||
Test-Case '空模式被忽略' {
|
||
$item = New-BaknretArchiveItem -ArchivePath 'x' -RealPath 'C:\x'
|
||
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('', ' ', 'real\')
|
||
Assert-Equal 1 $exclude.Arguments.Count
|
||
Assert-Equal '-x!x\real' $exclude.Arguments[0]
|
||
}
|
||
|
||
Test-Case '!re:<正则> 展开成源目录里命中的精确路径' {
|
||
$root = Join-Path $itemSandbox 'regex-src'
|
||
foreach ($dir in 'Cache1', 'Cache2', 'Keep', 'sub\Cache3') {
|
||
New-Item -ItemType Directory -Path (Join-Path $root $dir) -Force | Out-Null
|
||
}
|
||
$item = New-BaknretArchiveItem -ArchivePath 'Data' -RealPath $root
|
||
|
||
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('!re:^Cache\d+$')
|
||
Assert-Null $exclude.Error
|
||
$sorted = @($exclude.Arguments | Sort-Object)
|
||
Assert-Equal 3 $sorted.Count
|
||
Assert-Equal '-x!Data\Cache1' $sorted[0]
|
||
Assert-Equal '-x!Data\Cache2' $sorted[1]
|
||
Assert-Equal '-x!Data\sub\Cache3' $sorted[2]
|
||
}
|
||
|
||
Test-Case '非法的 !re: 正则给出明确的 Error,而不是静默漏排除' {
|
||
$root = Join-Path $itemSandbox 'regex-src'
|
||
$item = New-BaknretArchiveItem -ArchivePath 'Data' -RealPath $root
|
||
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('!re:*Cache')
|
||
Assert-True ($null -ne $exclude.Error)
|
||
Assert-Equal 0 $exclude.Arguments.Count
|
||
}
|
||
|
||
Test-Case '空的 !re: 被忽略' {
|
||
$root = Join-Path $itemSandbox 'regex-src'
|
||
$item = New-BaknretArchiveItem -ArchivePath 'Data' -RealPath $root
|
||
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('!re:')
|
||
Assert-Equal 0 $exclude.Arguments.Count
|
||
Assert-Null $exclude.Error
|
||
}
|
||
|
||
Test-Case '排除参数超过命令行安全长度时明确报错' {
|
||
$root = Join-Path $itemSandbox 'regex-src'
|
||
$item = New-BaknretArchiveItem -ArchivePath 'Data' -RealPath $root
|
||
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('aaaa\bbbb') -MaxCommandLineChars 5
|
||
Assert-True ($null -ne $exclude.Error)
|
||
}
|
||
|
||
Test-Case 'Split-BaknretPatternScope:`<项名>\` 前缀把模式分配给对应归档项' {
|
||
$items = @(
|
||
(New-BaknretArchiveItem -ArchivePath 'DefaultConfig' -RealPath 'C:\c'),
|
||
(New-BaknretArchiveItem -ArchivePath 'GlobalPersist' -RealPath 'C:\g'),
|
||
(New-BaknretArchiveItem -ArchivePath 'UserPersist' -RealPath 'C:\u')
|
||
)
|
||
$map = Split-BaknretPatternScope -Items $items -Patterns @('GlobalPersist\steam\steamapps', 'plain\one', '!*Cache')
|
||
|
||
Assert-Equal 'steam\steamapps' $map[1][0]
|
||
Assert-Equal 'plain\one' $map[0][0] '没点名任何项的模式对每一项各展开一份'
|
||
Assert-Equal 'plain\one' $map[2][0]
|
||
foreach ($index in 0, 1, 2) {
|
||
Assert-True ($map[$index] -contains '!*Cache') "! 开头的模式应广播到第 $index 项"
|
||
}
|
||
}
|
||
|
||
Test-Case 'Merge-BaknretExcludeArgument:去重且保序' {
|
||
$merged = Merge-BaknretExcludeArgument -ArgumentLists @(@('-x!a', '-xr!c'), @('-x!a', '-x!b'))
|
||
Assert-Equal 3 $merged.Count
|
||
Assert-Equal '-x!a' $merged[0]
|
||
Assert-Equal '-xr!c' $merged[1]
|
||
Assert-Equal '-x!b' $merged[2]
|
||
}
|
||
|
||
# ============================================================================
|
||
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 读写往返(含 layouts)' {
|
||
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['my-app'] = [ordered]@{
|
||
baseName = 'my-app'
|
||
action = 'backed-up'
|
||
roots = @('AlphaData', 'BetaFile')
|
||
layouts = @(
|
||
[ordered]@{ name = 'AlphaData'; kind = 'dir' },
|
||
[ordered]@{ name = 'BetaFile'; kind = 'file' }
|
||
)
|
||
}
|
||
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['my-app'].action
|
||
Assert-Equal 2 $again.items['my-app'].layouts.Count
|
||
Assert-Equal 'BetaFile' $again.items['my-app'].layouts[1].name
|
||
Assert-Equal 'file' $again.items['my-app'].layouts[1].kind
|
||
|
||
$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== 软件名录(新 Slot 结构) ==" -ForegroundColor Cyan
|
||
# ============================================================================
|
||
|
||
$catalogSandbox = Join-Path $env:TEMP ("baknret-cat-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||
New-Item -ItemType Directory -Path (Join-Path $catalogSandbox 'App') -Force | Out-Null
|
||
New-Item -ItemType Directory -Path (Join-Path $catalogSandbox 'Config') -Force | Out-Null
|
||
$appDir = Join-Path $catalogSandbox 'App'
|
||
$configDir = Join-Path $catalogSandbox 'Config'
|
||
|
||
Test-Case '名录解析:裸键、引号键、带 - 与 . 的名字' {
|
||
$catPath = Join-Path $catalogSandbox 'SoftwareCatalog.psd1'
|
||
$content = "@{`n FooClolor = @{ DefaultData = @{ Path = '$appDir' } }`n 'scoop-config' = @{ DefaultConfig = @{ Path = '$configDir' } }`n '.ssh' = @{ DefaultData = @{ Path = '$appDir' } }`n}`n"
|
||
[System.IO.File]::WriteAllText($catPath, $content, [System.Text.UTF8Encoding]::new($false))
|
||
|
||
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
|
||
Assert-Equal 3 $catalog.Count
|
||
Assert-True $catalog.ContainsKey('FooClolor')
|
||
Assert-True $catalog.ContainsKey('scoop-config') '带连字符的键必须能解析'
|
||
Assert-True $catalog.ContainsKey('.ssh') '带点的键必须能解析'
|
||
Assert-Equal 'Single' $catalog['FooClolor'].Kind
|
||
}
|
||
|
||
Test-Case '名录:Slot 按名字排序(哈希表顺序不可依赖)' {
|
||
$catPath = Join-Path $catalogSandbox 'sorted.psd1'
|
||
[System.IO.File]::WriteAllText($catPath, "@{`n FooClolor = @{ Zeta = @{ Path = '$appDir' }; Alpha = @{ Path = '$configDir' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
|
||
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
|
||
Assert-Equal 'Multi' $catalog['FooClolor'].Kind
|
||
Assert-Equal 2 $catalog['FooClolor'].Slots.Count
|
||
Assert-Equal 'Alpha' $catalog['FooClolor'].Slots[0].Name
|
||
Assert-Equal 'Zeta' $catalog['FooClolor'].Slots[1].Name
|
||
}
|
||
|
||
Test-Case '名录:Slot 对象字段与契约一致' {
|
||
$catPath = Join-Path $catalogSandbox 'fields.psd1'
|
||
$filePath = Join-Path $catalogSandbox 'App\settings.json'
|
||
Set-Content -LiteralPath $filePath -Value '{}'
|
||
[System.IO.File]::WriteAllText($catPath, "@{`n Rich = @{`n Beta = @{ Path = '$filePath'; Encrypt = `$true; Description = 'beta 说明' }`n Alpha = @{ Path = '$configDir'; Exclude = '!*Cache,sub\one'; Include = 'Modules:$appDir' }`n }`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
|
||
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
|
||
$entry = $catalog['Rich']
|
||
foreach ($expected in 'Name', 'Path', 'Description', 'Slots', 'Kind', 'Error', 'Missing', 'Raw') {
|
||
Assert-True ($entry.PSObject.Properties.Name -contains $expected) "名录条目缺少 $expected"
|
||
}
|
||
foreach ($gone in 'Dirs', 'Variants', 'ResolvedPath', 'RootName') {
|
||
Assert-False ($entry.PSObject.Properties.Name -contains $gone) "名录条目不该再有旧属性 $gone"
|
||
}
|
||
|
||
$beta = $entry.Slots[1]
|
||
foreach ($expected in 'Name', 'Declared', 'Resolved', 'Exists', 'IsFile', 'Suffixed', 'Description', 'Exclude', 'Include', 'Encrypt') {
|
||
Assert-True ($beta.PSObject.Properties.Name -contains $expected) "Slot 缺少 $expected"
|
||
}
|
||
Assert-Equal 'Beta' $beta.Name
|
||
Assert-Equal $filePath $beta.Resolved
|
||
Assert-True $beta.IsFile 'Path 指向文件时 IsFile 必须为真'
|
||
Assert-True $beta.Encrypt
|
||
Assert-Equal 'beta 说明' $beta.Description
|
||
|
||
$alpha = $entry.Slots[0]
|
||
Assert-False $alpha.IsFile
|
||
Assert-Equal 2 $alpha.Exclude.Count
|
||
Assert-Equal '!*Cache' $alpha.Exclude[0]
|
||
Assert-Equal 1 $alpha.Include.Count
|
||
Assert-Equal "Modules:$appDir" $alpha.Include[0]
|
||
}
|
||
|
||
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 = @{ DefaultConfig = @{ Path = '$root\legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
|
||
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
|
||
Assert-Equal 'Single' $catalog['legendary'].Kind
|
||
Assert-True $catalog['legendary'].Slots[0].Resolved -like '*legendary_2.0.4'
|
||
Assert-True $catalog['legendary'].Slots[0].Suffixed '补全出来的路径要标记 Suffixed'
|
||
}
|
||
|
||
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 = @{ DefaultConfig = @{ Path = '$root\Legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
|
||
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
|
||
Assert-Equal 'Unresolved' $catalog['Legendary'].Kind '必须以下一个字符是 _ 或 - 为界'
|
||
Assert-False $catalog['Legendary'].Slots[0].Exists
|
||
Assert-Equal 1 $catalog['Legendary'].Missing.Count
|
||
}
|
||
|
||
Test-Case 'Find-ChildDirectoryByName:_ / - 边界之外不补全' {
|
||
$root = Join-Path $catalogSandbox 'boundary'
|
||
New-Item -ItemType Directory -Path (Join-Path $root 'Legendary_1.0') -Force | Out-Null
|
||
New-Item -ItemType Directory -Path (Join-Path $root 'Legendary-2.0') -Force | Out-Null
|
||
New-Item -ItemType Directory -Path (Join-Path $root 'LegendarySomething') -Force | Out-Null
|
||
New-Item -ItemType Directory -Path (Join-Path $root 'Unrelated') -Force | Out-Null
|
||
|
||
$found = @(Find-ChildDirectoryByName -Parent $root -Name 'Legendary')
|
||
Assert-Equal 2 $found.Count
|
||
Assert-True (@($found | Where-Object { $_ -like '*Legendary_1.0' }).Count -eq 1)
|
||
Assert-True (@($found | Where-Object { $_ -like '*Legendary-2.0' }).Count -eq 1)
|
||
}
|
||
|
||
Test-Case '名录:一个 Slot 补全出多个目录时明确报错(否则会混成一棵树)' {
|
||
$root = Join-Path $catalogSandbox 'multi-cand'
|
||
New-Item -ItemType Directory -Path (Join-Path $root 'Legendary_1') -Force | Out-Null
|
||
New-Item -ItemType Directory -Path (Join-Path $root 'Legendary_2') -Force | Out-Null
|
||
$catPath = Join-Path $catalogSandbox 'multicand.psd1'
|
||
[System.IO.File]::WriteAllText($catPath, "@{`n Legendary = @{ DefaultConfig = @{ Path = '$root\Legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
|
||
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
|
||
$entry = $catalog['Legendary']
|
||
Assert-True ($null -ne $entry.Error)
|
||
Assert-True ($entry.Error -like '*拆成多个 Slot*') '错误里要给出可操作的指引'
|
||
}
|
||
|
||
Test-Case '名录:Slot 写法不对 / 缺少 Path 都记成 Error,而不是崩掉' {
|
||
$bad = Join-Path $catalogSandbox 'bad.psd1'
|
||
[System.IO.File]::WriteAllText($bad, "@{`n A = @{ S = 'C:\x' }`n B = @{ S = @{ Description = 'x' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
$catalog = Get-SoftwareCatalog -Path $bad -NoCache
|
||
Assert-Equal 'Invalid' $catalog['A'].Kind
|
||
Assert-True ($catalog['A'].Error -like '*Slot S*')
|
||
Assert-Equal 'Invalid' $catalog['B'].Kind
|
||
Assert-True ($catalog['B'].Error -like '*缺少 Path*')
|
||
}
|
||
|
||
Test-Case '名录:裸字符串 / 数组等旧结构会被 ERROR 跳过(不再当目录用)' {
|
||
$legacy = Join-Path $catalogSandbox 'legacy.psd1'
|
||
[System.IO.File]::WriteAllText($legacy, "@{`n A = 'C:\Programs\A'`n B = @('C:\1','C:\2')`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
$catalog = Get-SoftwareCatalog -Path $legacy -NoCache
|
||
Assert-Equal 0 $catalog.Count '旧结构必须被跳过,不能猜成目录'
|
||
|
||
$dirs = Join-Path $catalogSandbox 'dirs.psd1'
|
||
[System.IO.File]::WriteAllText($dirs, "@{`n C = @{ Dirs = @('C:\1','C:\2') }`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
$catalog = Get-SoftwareCatalog -Path $dirs -NoCache
|
||
Assert-Equal 1 $catalog.Count
|
||
Assert-Equal 'Invalid' $catalog['C'].Kind
|
||
Assert-True ($catalog['C'].Error -like '*Dirs*')
|
||
}
|
||
|
||
Test-Case '名录:%VAR% 与 $( ... ) 子表达式都会被展开' {
|
||
Assert-Equal (Join-Path $env:TEMP 'x') (Expand-CatalogPathText -Text '%TEMP%\x')
|
||
Assert-Equal (Join-Path $env:TEMP 'sub') (Expand-CatalogPathText -Text '$(Join-Path $env:TEMP "sub")')
|
||
Assert-Equal '$(Join-Path' (Expand-CatalogPathText -Text '$(Join-Path') '括号不配对时原样保留,不抛异常'
|
||
}
|
||
|
||
Test-Case '名录:Includes 分文件维护会被合并' {
|
||
$extraPath = Join-Path $catalogSandbox 'Extra.psd1'
|
||
$basePath = Join-Path $catalogSandbox 'Base.psd1'
|
||
[System.IO.File]::WriteAllText($extraPath, "@{`n 'extra-app' = @{ D = @{ Path = '$appDir' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
[System.IO.File]::WriteAllText($basePath, "@{`n Includes = @('Extra.psd1')`n 'base-app' = @{ D = @{ Path = '$configDir' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
|
||
$catalog = Get-SoftwareCatalog -Path $basePath -NoCache
|
||
Assert-True $catalog.ContainsKey('base-app')
|
||
Assert-True $catalog.ContainsKey('extra-app') 'Includes 引入的条目也应在'
|
||
}
|
||
|
||
Test-Case '名录:结果按文件指纹缓存,-NoCache 强制重读' {
|
||
$catPath = Join-Path $catalogSandbox 'cache.psd1'
|
||
[System.IO.File]::WriteAllText($catPath, "@{`n Cached = @{ D = @{ Path = '$appDir' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
|
||
$first = Get-SoftwareCatalog -Path $catPath
|
||
$second = Get-SoftwareCatalog -Path $catPath
|
||
Assert-True ([object]::ReferenceEquals($first, $second)) '同一文件同一指纹应命中缓存'
|
||
|
||
$fresh = Get-SoftwareCatalog -Path $catPath -NoCache
|
||
Assert-False ([object]::ReferenceEquals($first, $fresh)) '-NoCache 必须重新读'
|
||
|
||
[System.IO.File]::WriteAllText($catPath, "@{`n Cached = @{ D = @{ Path = '$configDir' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
$changed = Get-SoftwareCatalog -Path $catPath
|
||
Assert-Equal $configDir $changed['Cached'].Slots[0].Resolved '文件变了缓存必须失效'
|
||
}
|
||
|
||
# ============================================================================
|
||
Write-Host "`n== Resolve-BackupEntry(归档项解析) ==" -ForegroundColor Cyan
|
||
# ============================================================================
|
||
|
||
$resolveSandbox = Join-Path $env:TEMP ("baknret-resolve-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||
$dirA = Join-Path $resolveSandbox 'A'
|
||
$dirB = Join-Path $resolveSandbox 'B'
|
||
$dirExtra = Join-Path $resolveSandbox 'Extra'
|
||
foreach ($d in $dirA, $dirB, $dirExtra) { New-Item -ItemType Directory -Path $d -Force | Out-Null }
|
||
$resolveCatalog = Join-Path $resolveSandbox 'catalog.psd1'
|
||
[System.IO.File]::WriteAllText($resolveCatalog, @"
|
||
@{
|
||
FooClolor = @{ DefaultData = @{ Path = '$dirA'; Encrypt = `$true } }
|
||
MultiApp = @{ Alpha = @{ Path = '$dirA' }; Beta = @{ Path = '$dirB' } }
|
||
}
|
||
"@, [System.Text.UTF8Encoding]::new($false))
|
||
|
||
Test-Case '软件名条目:一个 Slot -> 一个归档项,路径是 Slot 名,归档名是软件名' {
|
||
$item = ConvertFrom-BackupListLine -Line 'FooClolor'
|
||
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
|
||
|
||
Assert-True $resolved.IsName
|
||
Assert-Equal 'name' $resolved.ArchiveFlavor
|
||
Assert-Equal 'FooClolor' $resolved.BaseName
|
||
Assert-Equal 'both' $resolved.Direction
|
||
Assert-Equal 1 $resolved.Items.Count
|
||
Assert-Equal 'DefaultData' $resolved.Items[0].ArchivePath
|
||
Assert-Equal 'DefaultData' $resolved.Items[0].TopName
|
||
Assert-Equal $dirA $resolved.Items[0].RealPath
|
||
Assert-Equal 'slot' $resolved.Items[0].Kind
|
||
Assert-Equal 'DefaultData' $resolved.Items[0].Slot
|
||
Assert-Equal 'catalog' $resolved.Items[0].Origin
|
||
Assert-True $resolved.Items[0].Exists
|
||
}
|
||
|
||
Test-Case 'Resolve-BackupEntry 的返回值里没有旧的 Sources / RootName 等属性' {
|
||
$item = ConvertFrom-BackupListLine -Line 'FooClolor'
|
||
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
|
||
foreach ($expected in 'IsName', 'CatalogEntry', 'BaseName', 'ArchiveFlavor', 'Direction', 'Items', 'Encrypt', 'ExcludePatterns', 'HasExcludeOverride', 'Includes', 'HasIncludeOverride', 'Source', 'Error', 'Blocking') {
|
||
Assert-True ($resolved.PSObject.Properties.Name -contains $expected) "缺少属性 $expected"
|
||
}
|
||
foreach ($gone in 'Sources', 'RootName', 'Variants', 'ResolvedPath') {
|
||
Assert-False ($resolved.PSObject.Properties.Name -contains $gone) "不该再有旧属性 $gone"
|
||
}
|
||
}
|
||
|
||
Test-Case '字面路径条目:仍用路径命名算法,归档内是历史布局 <末级名>' {
|
||
$item = ConvertFrom-BackupListLine -Line 'C:\Programs\FooClolor'
|
||
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
|
||
Assert-False $resolved.IsName
|
||
Assert-Equal 'path' $resolved.ArchiveFlavor
|
||
Assert-Equal 'FooClolor_from_C_+Programs' $resolved.BaseName
|
||
Assert-Equal 1 $resolved.Items.Count
|
||
Assert-Equal 'FooClolor' $resolved.Items[0].ArchivePath
|
||
Assert-Equal 'path' $resolved.Items[0].Kind
|
||
}
|
||
|
||
Test-Case '@pathname 用名录里的真实路径跑命名算法' {
|
||
$item = ConvertFrom-BackupListLine -Line 'FooClolor @pathname'
|
||
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
|
||
Assert-Equal (Get-BackupBaseName -RawPath $dirA) $resolved.BaseName
|
||
Assert-Equal 'DefaultData' $resolved.Items[0].ArchivePath '归档内名字仍由 Slot 决定'
|
||
}
|
||
|
||
Test-Case ':: / @ Path= 覆盖单 Slot 条目的真实路径' {
|
||
$item = ConvertFrom-BackupListLine -Line "FooClolor :: $dirB"
|
||
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
|
||
Assert-Equal 1 $resolved.Items.Count
|
||
Assert-Equal $dirB $resolved.Items[0].RealPath
|
||
Assert-Equal 'DefaultData' $resolved.Items[0].ArchivePath
|
||
Assert-Null $resolved.Blocking
|
||
|
||
$atItem = ConvertFrom-BackupListLine -Line "FooClolor @ Path='$dirB'"
|
||
$atResolved = Resolve-BackupEntry -Entry $atItem -CatalogPath $resolveCatalog
|
||
Assert-Equal $dirB $atResolved.Items[0].RealPath
|
||
}
|
||
|
||
Test-Case ':: 覆盖路径时,多 Slot 的条目会 Blocking(说不清是哪个 Slot)' {
|
||
$item = ConvertFrom-BackupListLine -Line "MultiApp :: $dirB"
|
||
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
|
||
Assert-True ($null -ne $resolved.Blocking)
|
||
Assert-True ($resolved.Blocking -like '*2 个 Slot*')
|
||
}
|
||
|
||
Test-Case '名录里没有该名字:BaseName 退回可读目录名,并给出 Error' {
|
||
$item = ConvertFrom-BackupListLine -Line 'no-such-thing'
|
||
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
|
||
Assert-Equal 'no-such-thing' $resolved.BaseName
|
||
Assert-True ($null -ne $resolved.Error) '应给出错误说明'
|
||
Assert-Equal 0 $resolved.Items.Count
|
||
}
|
||
|
||
Test-Case '名录里的路径不存在时仍给出归档项(恢复要靠它还原回原位)' {
|
||
$catPath = Join-Path $resolveSandbox 'gone.psd1'
|
||
[System.IO.File]::WriteAllText($catPath, "@{`n 'gone-app' = @{ DefaultData = @{ Path = '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-Equal 1 $resolved.Items.Count '恢复端据 Items 还原'
|
||
Assert-Equal 'C:\definitely\not\here' $resolved.Items[0].RealPath
|
||
Assert-False $resolved.Items[0].Exists
|
||
Assert-Equal 'Unresolved' $resolved.CatalogEntry.Kind
|
||
}
|
||
|
||
Test-Case ':+ 追加项:<归档内相对路径>:<宿主机绝对路径>' {
|
||
$item = ConvertFrom-BackupListLine -Line "FooClolor :+ Modules:$dirExtra"
|
||
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
|
||
Assert-Equal 2 $resolved.Items.Count
|
||
Assert-Equal 'DefaultData' $resolved.Items[0].ArchivePath
|
||
Assert-Equal 'Modules' $resolved.Items[1].ArchivePath
|
||
Assert-Equal $dirExtra $resolved.Items[1].RealPath
|
||
Assert-Equal 'include' $resolved.Items[1].Kind
|
||
Assert-Equal 'include' $resolved.Items[1].Origin
|
||
Assert-True $resolved.HasIncludeOverride
|
||
}
|
||
|
||
Test-Case '条目级 :+ 覆盖名录里的 Include(不是叠加)' {
|
||
$incDir = Join-Path $resolveSandbox 'Inc'
|
||
New-Item -ItemType Directory -Path $incDir -Force | Out-Null
|
||
$catPath = Join-Path $resolveSandbox 'withinc.psd1'
|
||
[System.IO.File]::WriteAllText($catPath, "@{`n IncApp = @{ DefaultData = @{ Path = '$dirA'; Include = 'CatalogInc:$incDir' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
|
||
$plain = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'IncApp') -CatalogPath $catPath
|
||
Assert-Equal 2 $plain.Items.Count
|
||
Assert-Equal 'CatalogInc' $plain.Items[1].ArchivePath
|
||
|
||
$overridden = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line "IncApp :+ EntryInc:$dirExtra") -CatalogPath $catPath
|
||
Assert-Equal 2 $overridden.Items.Count
|
||
Assert-Equal 'EntryInc' $overridden.Items[1].ArchivePath
|
||
Assert-Equal $dirExtra $overridden.Items[1].RealPath
|
||
}
|
||
|
||
Test-Case '归档内路径冲突:同名追加项 -> Blocking' {
|
||
$item = ConvertFrom-BackupListLine -Line "FooClolor :+ DefaultData:$dirExtra"
|
||
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
|
||
Assert-True ($null -ne $resolved.Blocking)
|
||
Assert-True ($resolved.Blocking -like '*冲突*')
|
||
}
|
||
|
||
Test-Case '归档内路径冲突:父子关系 -> Blocking' {
|
||
$item = ConvertFrom-BackupListLine -Line "FooClolor :+ DefaultData\sub:$dirExtra"
|
||
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
|
||
Assert-True ($null -ne $resolved.Blocking)
|
||
Assert-True ($resolved.Blocking -like '*父子*')
|
||
}
|
||
|
||
Test-Case '加密:名录 Slot 的 Encrypt 之和,条目级覆盖优先' {
|
||
$catalogEncrypt = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'FooClolor') -CatalogPath $resolveCatalog
|
||
Assert-True $catalogEncrypt.Encrypt
|
||
|
||
$off = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'FooClolor :!encrypt') -CatalogPath $resolveCatalog
|
||
Assert-False $off.Encrypt
|
||
|
||
$on = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'MultiApp :encrypt') -CatalogPath $resolveCatalog
|
||
Assert-True $on.Encrypt '名录没开、条目开了 -> 开'
|
||
Assert-False $on.HasExcludeOverride
|
||
}
|
||
|
||
Test-Case '方向标记随条目一路传到 Resolve-BackupEntry' {
|
||
Assert-Equal 'backup' (Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line '+ FooClolor') -CatalogPath $resolveCatalog).Direction
|
||
Assert-Equal 'restore' (Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line '- FooClolor') -CatalogPath $resolveCatalog).Direction
|
||
}
|
||
|
||
Test-Case '条目级 :- 覆盖名录里的 Exclude(不是叠加)' {
|
||
$catPath = Join-Path $resolveSandbox 'withexc.psd1'
|
||
[System.IO.File]::WriteAllText($catPath, "@{`n ExcApp = @{ DefaultData = @{ Path = '$dirA'; Exclude = '!*CatalogCache' } }`n}`n", [System.Text.UTF8Encoding]::new($false))
|
||
|
||
$plain = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'ExcApp') -CatalogPath $catPath
|
||
Assert-False $plain.HasExcludeOverride
|
||
Assert-Equal 0 $plain.ExcludePatterns.Count
|
||
Assert-Equal '!*CatalogCache' $plain.Items[0].Exclude[0]
|
||
|
||
$overridden = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'ExcApp :- !*Listed') -CatalogPath $catPath
|
||
Assert-True $overridden.HasExcludeOverride
|
||
Assert-Equal '!*Listed' $overridden.ExcludePatterns[0]
|
||
}
|
||
|
||
# ============================================================================
|
||
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')
|
||
$archiveItem = New-BaknretArchiveItem -ArchivePath $itemName -RealPath $source -Kind 'path' -Exists $true
|
||
$excludeArgs = @((Get-BaknretExcludeArgument -Item $archiveItem -Patterns $patterns).Arguments)
|
||
Assert-True ($excludeArgs.Count -ge 5) '排除参数应被翻译出来'
|
||
|
||
$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 '!re:<正则> 展开出的排除参数在真实归档上生效' {
|
||
$root = Join-Path $sandbox 'src-re'
|
||
$itemName = 'Data'
|
||
$source = Join-Path $root $itemName
|
||
foreach ($d in 'Cache1', 'Cache2', 'Keep', 'sub\Cache3') {
|
||
New-Item -ItemType Directory -Path (Join-Path $source $d) -Force | Out-Null
|
||
Set-Content -LiteralPath (Join-Path $source "$d\x.bin") 'x'
|
||
}
|
||
|
||
$archiveItem = New-BaknretArchiveItem -ArchivePath $itemName -RealPath $source -Kind 'slot' -Exists $true
|
||
$excludeArgs = @((Get-BaknretExcludeArgument -Item $archiveItem -Patterns @('!re:^Cache\d+$')).Arguments)
|
||
Assert-Equal 3 $excludeArgs.Count '两个顶层 Cache 目录 + 嵌套的 sub\Cache3'
|
||
|
||
$archive = Join-Path $sandbox 're.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-re'
|
||
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\x.bin")) '不命中正则的目录要留着'
|
||
Assert-False (Test-Path -LiteralPath (Join-Path $verify "$itemName\Cache1\x.bin")) 'Cache1 应被正则排除'
|
||
Assert-False (Test-Path -LiteralPath (Join-Path $verify "$itemName\Cache2\x.bin")) 'Cache2 应被正则排除'
|
||
Assert-False (Test-Path -LiteralPath (Join-Path $verify "$itemName\sub\Cache3\x.bin")) '嵌套的 Cache3 也应被排除'
|
||
}
|
||
|
||
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 'Get-ArchiveTopLevelNames 能列出归档内的顶层条目名' {
|
||
$root = Join-Path $sandbox 'topnames'
|
||
New-Item -ItemType Directory -Path (Join-Path $root 'Alpha') -Force | Out-Null
|
||
New-Item -ItemType Directory -Path (Join-Path $root 'Beta') -Force | Out-Null
|
||
Set-Content -LiteralPath (Join-Path $root 'Alpha\a.txt') 'a'
|
||
Set-Content -LiteralPath (Join-Path $root 'Beta\b.txt') 'b'
|
||
$archive = Join-Path $sandbox 'topnames.7z'
|
||
$code = Invoke-ExternalCommand -FilePath $sevenZip -ArgumentList @('a', '-t7z', '-mx=1', '-bso0', '-bsp0', $archive, 'Alpha', 'Beta') -WorkingDirectory $root
|
||
Assert-Equal 0 $code
|
||
|
||
$names = @(Get-ArchiveTopLevelNames -ArchivePath $archive -SevenZip $sevenZip)
|
||
$sorted = @($names | Sort-Object)
|
||
Assert-Equal 2 $sorted.Count
|
||
Assert-Equal 'Alpha' $sorted[0]
|
||
Assert-Equal 'Beta' $sorted[1]
|
||
}
|
||
|
||
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
|
||
Remove-Item -LiteralPath $itemSandbox -Recurse -Force -ErrorAction SilentlyContinue
|
||
Remove-Item -LiteralPath $catalogSandbox -Recurse -Force -ErrorAction SilentlyContinue
|
||
Remove-Item -LiteralPath $resolveSandbox -Recurse -Force -ErrorAction SilentlyContinue
|
||
|
||
$failed = Write-TestSummary -Title '单元测试'
|
||
if ($failed -gt 0) { exit 1 }
|
||
exit 0
|