refactor: 公共面补 BakNRet 前缀,产品名大小写全仓统一

16 个没有前缀的公共函数补上 BakNRet(Write-Log → Write-BakNRetLog、Resolve-BackupEntry →
Resolve-BakNRetBackupEntry、Find-ChildDirectoryByName → Find-BakNRetChildDirectoryByName 等),
另外把全仓的 Baknret 统一成 BakNRet(47 个文件、940 处、65 个定义文件重命名)。

这不是审美问题:静态分析直接拓出一条实据 —— Write-Log 与本机某个已装模块导出的命令
**重名**(PSAvoidOverwritingBuiltInCmdlets),而重名的后果是导入两个模块时有一方的命令被
静默遮蔽。补前缀正是这条规则的解法,改名后它归零。

为什么敢做这个规模:PowerShell 的函数名解析大小写不敏感,所以 Baknret → BakNRet 在功能
上是零风险;真正要验证的是 16 个补前缀的调用点,而 276 个断言几乎覆盖了每个函数。另外
"名字与文件名一致"这条不变式有断言盯着(加载器点源的文件集合 vs 磁盘)。

踩到并记下的坑:Windows 文件系统大小写不敏感,所以**只改大小写**的重命名会被 Move-Item
当成同一个文件而静默跳过 —— 同一批里同时改了名字的那 16 个文件却成功了,于是"看起来能跑"。
最后用"先移到临时名、再移到目标名"的两步走解决,判断与替换全部改用显式大小写敏感的形式
(-creplace / -cmatch)。

顺带把名录指纹缓存从 MD5 换成 SHA256(PSAvoidUsingBrokenHashAlgorithms):它只是缓存键,
没有兼容负担。

验收:test.ps1 9/9 全绿(7 与 5.1)、100 个文件两版解析零错、276 个断言全过、
构建工具仍能合回单文件(3300 行)。
This commit is contained in:
Shuery committed 2026-09-27 09:56:36 +08:00
1 parent 187d2759fd
commit 42f02d0eca
84 files changed
+1043 -1043

No files matched your search

+40 -40
View File
@@ -37,7 +37,7 @@ BeforeAll {
# 见 BakNRet.Tests.ps1 里的说明:本机沙箱禁止 PowerShell 为捕获原生子进程输出建管道,
# 所以走"临时 .cmd + 文件重定向 + Invoke-ExternalCommand(继承 stdio)"这条路。
function Invoke-BaknretScript {
function Invoke-BakNRetScript {
param(
[Parameter(Mandatory = $true)][string]$Script,
[hashtable]$Parameters = @{}
@@ -138,7 +138,7 @@ Describe '软件名录:Slot 形状(新契约)' {
}
It '一个软件多个 Slot:Kind=Multi,Slot 按名排序且说明被保留' {
$catalog = Get-SoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog['pair'].Kind | Should -Be 'Multi'
@($catalog['pair'].Slots).Count | Should -Be 2
(@($catalog['pair'].Slots | ForEach-Object { $_.Name }) -join ',') | Should -Be 'A,B'
@@ -150,7 +150,7 @@ Describe '软件名录:Slot 形状(新契约)' {
It '每个 Slot 都是一个独立的归档项来源(Kind=slot / Origin=catalog)' {
$entry = ConvertFrom-BackupListLine -Line 'pair'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Items).Count | Should -Be 2
(@($resolved.Items | ForEach-Object { $_.ArchivePath }) -join ',') | Should -Be 'A,B'
(@($resolved.Items | ForEach-Object { $_.Kind }) -join ',') | Should -Be 'slot,slot'
@@ -161,22 +161,22 @@ Describe '软件名录:Slot 形状(新契约)' {
}
It 'Slot 级排除写在 Slot 自己身上(相对本 Slot 的归档根)' {
$catalog = Get-SoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
(@($catalog['slotex'].Slots[0].Exclude) -join '|') | Should -Be '!*Cache|logs\'
$entry = ConvertFrom-BackupListLine -Line 'slotex'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
(@($resolved.Items[0].Exclude) -join '|') | Should -Be '!*Cache|logs\'
$resolved.HasExcludeOverride | Should -BeFalse
}
It '数组里"当前不存在"的 Slot 仍然产出归档项(恢复要靠它还原回原位)' {
$catalog = Get-SoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog['partial'].Kind | Should -Be 'Partial'
@($catalog['partial'].Missing) | Should -Contain $script:MissingDir
$entry = ConvertFrom-BackupListLine -Line 'partial'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Items).Count | Should -Be 2
@($resolved.Items | ForEach-Object { $_.RealPath }) | Should -Contain $script:MissingDir
$resolved.Blocking | Should -BeNullOrEmpty # 源不存在不是致命错误
@@ -184,7 +184,7 @@ Describe '软件名录:Slot 形状(新契约)' {
It '文件 Slot:归档项是文件项(归档里就是名为 Slot 的文件)' {
$entry = ConvertFrom-BackupListLine -Line 'fileapp'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Items).Count | Should -Be 1
$resolved.Items[0].IsFile | Should -BeTrue
$resolved.Items[0].ArchivePath | Should -Be 'Cfg'
@@ -193,13 +193,13 @@ Describe '软件名录:Slot 形状(新契约)' {
}
It '旧的裸字符串 / 字符串数组写法被拒绝(ERROR + 跳过)' {
$catalog = Get-SoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog.ContainsKey('legacystr') | Should -BeFalse
$catalog.ContainsKey('legacyarr') | Should -BeFalse
}
It '旧的 @{ Dirs = @(...) } 写法不再展开,留下 Invalid 与原因' {
$catalog = Get-SoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3 -NoCache
$catalog.ContainsKey('legacydirs') | Should -BeTrue
$catalog['legacydirs'].Kind | Should -Be 'Invalid'
$catalog['legacydirs'].Error | Should -Not -BeNullOrEmpty
@@ -208,7 +208,7 @@ Describe '软件名录:Slot 形状(新契约)' {
It '多 Slot 的名录条目在清单里仍然按软件名命名归档' {
$entry = ConvertFrom-BackupListLine -Line 'pair'
(Get-ItemArchiveName -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3) | Should -Be 'pair'
(Get-BakNRetItemArchiveName -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3) | Should -Be 'pair'
}
}
@@ -227,7 +227,7 @@ Describe '清单修饰符:新契约格式' {
It ':: 覆盖 Path:单 Slot 条目直接生效' {
$entry = ConvertFrom-BackupListLine -Line "solo :: $script:DirB"
$entry.Overrides.ContainsKey('Path') | Should -BeTrue
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Items).Count | Should -Be 1
$resolved.Items[0].ArchivePath | Should -Be 'Only'
$resolved.Items[0].RealPath | Should -Be $script:DirB
@@ -236,7 +236,7 @@ Describe '清单修饰符:新契约格式' {
It ':: 覆盖遇到多 Slot 条目 -> Blocking(不知道给哪一个,绝不猜)' {
$entry = ConvertFrom-BackupListLine -Line "pair :: $script:DirB"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Items).Count | Should -Be 0
$resolved.Blocking | Should -Match '不能用一个'
}
@@ -246,7 +246,7 @@ Describe '清单修饰符:新契约格式' {
(@($entry.ExcludePatterns) -join '|') | Should -Be 'logs\|!*Cache'
(@($entry.Includes) -join '|') | Should -Be "Mods:$script:DirB"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved.HasExcludeOverride | Should -BeTrue
$resolved.HasIncludeOverride | Should -BeTrue
@($resolved.Items | ForEach-Object { $_.ArchivePath }) | Should -Contain 'Mods'
@@ -261,13 +261,13 @@ Describe '清单修饰符:新契约格式' {
}
It ':encrypt / :!encrypt 覆盖名录里的加密默认值' {
$base = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'fileapp') -CatalogPath $script:FormatCatalog -MaxDepth 3
$base = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'fileapp') -CatalogPath $script:FormatCatalog -MaxDepth 3
$base.Encrypt | Should -BeTrue # 名录里 Cfg Slot 标了 Encrypt
$off = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'fileapp :!encrypt') -CatalogPath $script:FormatCatalog -MaxDepth 3
$off = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'fileapp :!encrypt') -CatalogPath $script:FormatCatalog -MaxDepth 3
$off.Encrypt | Should -BeFalse
$on = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'pair :encrypt') -CatalogPath $script:FormatCatalog -MaxDepth 3
$on = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'pair :encrypt') -CatalogPath $script:FormatCatalog -MaxDepth 3
$on.Encrypt | Should -BeTrue
}
@@ -278,8 +278,8 @@ Describe '清单修饰符:新契约格式' {
# @pathname 对软件名条目也会改用真实路径命名
$entry = ConvertFrom-BackupListLine -Line 'pair @pathname'
$expected = Get-BackupBaseName -RawPath $script:DirA
(Get-ItemArchiveName -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3) | Should -Be $expected
$expected = Get-BakNRetBackupBaseName -RawPath $script:DirA
(Get-BakNRetItemArchiveName -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3) | Should -Be $expected
}
It '同一行里重复写同类记号会累积(不静默丢掉前一条规则)' {
@@ -350,14 +350,14 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ
$script:SlotList = Write-ListFile -Path (Join-Path $script:SlotRoot 'list.txt') -Content "appkit`n"
$script:SlotBackupDir = Join-Path $script:SlotRoot 'Backups'
$script:SlotBackupRun = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$script:SlotBackupRun = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:SlotList
BackupDir = $script:SlotBackupDir
ConfigPath = $script:SlotConfig
Force = $true
QuietTool = $true
}
$script:SlotManifest = Read-BaknretManifest -Path (Join-Path $script:SlotBackupDir 'manifest.json')
$script:SlotManifest = Read-BakNRetManifest -Path (Join-Path $script:SlotBackupDir 'manifest.json')
$script:SlotArchive = @(Get-ChildItem -LiteralPath $script:SlotBackupDir -File -Filter *.7z)[0]
}
@@ -367,7 +367,7 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ
}
It '归档顶层就是各个 Slot 名(目录 Slot + 文件 Slot + Include 项)' {
$top = @(Get-ArchiveTopLevelNames -ArchivePath $script:SlotArchive.FullName -SevenZip $script:SevenZip)
$top = @(Get-BakNRetArchiveTopLevelNames -ArchivePath $script:SlotArchive.FullName -SevenZip $script:SevenZip)
(@($top | Sort-Object) -join ',') | Should -Be 'Cfg,Data,Extra,Modules'
}
@@ -396,7 +396,7 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ
Remove-Item -LiteralPath (Join-Path $script:SlotRoot 'apps') -Recurse -Force
Remove-Item -LiteralPath $script:SlotInclude -Recurse -Force
$run = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:SlotList
BackupDir = $script:SlotBackupDir
ConfigPath = $script:SlotConfig
@@ -422,7 +422,7 @@ Describe '集成:Slot 布局的打包与恢复' -Skip:(-not ($script:HasSevenZ
}
It '恢复之后 manifest 记下 lastRestoreAt' {
$manifest = Read-BaknretManifest -Path (Join-Path $script:SlotBackupDir 'manifest.json')
$manifest = Read-BakNRetManifest -Path (Join-Path $script:SlotBackupDir 'manifest.json')
$manifest.items['appkit'].lastRestoreAt | Should -Not -BeNullOrEmpty
}
}
@@ -462,7 +462,7 @@ Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSeven
}
It '归档确实是旧布局:顶层是源目录名而不是 Slot 名' {
$top = @(Get-ArchiveTopLevelNames -ArchivePath $script:LegacyArchive -SevenZip $script:SevenZip)
$top = @(Get-BakNRetArchiveTopLevelNames -ArchivePath $script:LegacyArchive -SevenZip $script:SevenZip)
(@($top | Sort-Object) -join ',') | Should -Be $script:LegacyLeaf
}
@@ -471,7 +471,7 @@ Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSeven
# 不能靠 7z 的退出码猜(7z 对不存在的条目同样返回 0)。
Remove-Item -LiteralPath (Join-Path $script:LegacyDestParent $script:LegacyLeaf) -Recurse -Force -ErrorAction SilentlyContinue
$run = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:LegacyList
BackupDir = $script:LegacyBackupDir
ConfigPath = $script:LegacyConfig
@@ -490,7 +490,7 @@ Describe '集成:旧布局归档的回退恢复' -Skip:(-not ($script:HasSeven
$missingList = Write-ListFile -Path (Join-Path $script:LegacyRoot 'missing-list.txt') `
-Content "oldapp :: $script:LegacyRoot\dest2\Nothing Here`n"
$run = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $missingList
BackupDir = $script:LegacyBackupDir
ConfigPath = $script:LegacyConfig
@@ -522,7 +522,7 @@ Describe '集成:归档内路径冲突会被拒绝执行' -Skip:(-not ($script
$script:RejectConfig = Write-ListFile -Path (Join-Path $script:RejectRoot 'config.psd1') -Content "@{ SoftwareCatalog = '$script:RejectCatalog' }`n"
$script:RejectBackupDir = Join-Path $script:RejectRoot 'Backups'
$script:RejectRun = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$script:RejectRun = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:RejectList
BackupDir = $script:RejectBackupDir
ConfigPath = $script:RejectConfig
@@ -538,7 +538,7 @@ Describe '集成:归档内路径冲突会被拒绝执行' -Skip:(-not ($script
}
It 'manifest 里记下这次是 failed,并带上原因' {
$manifest = Read-BaknretManifest -Path (Join-Path $script:RejectBackupDir 'manifest.json')
$manifest = Read-BakNRetManifest -Path (Join-Path $script:RejectBackupDir 'manifest.json')
$record = $manifest.items['collide']
$record.action | Should -Be 'failed'
$record.reason | Should -Match '归档内路径冲突'
@@ -568,7 +568,7 @@ Describe '条目从清单里消失后,旧归档必须被点名为孤儿' -Skip
# 第一次:清单里有 my-app
Write-ListFile -Path $script:OrphanList -Content "my-app`n" | Out-Null
$null = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$null = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:OrphanList
BackupDir = $script:OrphanBackupDir
ConfigPath = $script:OrphanConfig
@@ -578,7 +578,7 @@ Describe '条目从清单里消失后,旧归档必须被点名为孤儿' -Skip
# 第二次:把条目从清单里拿掉,归档留在磁盘上
Write-ListFile -Path $script:OrphanList -Content "# 清单里没有条目了`n" | Out-Null
$script:OrphanRun = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$script:OrphanRun = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:OrphanList
BackupDir = $script:OrphanBackupDir
ConfigPath = $script:OrphanConfig
@@ -588,7 +588,7 @@ Describe '条目从清单里消失后,旧归档必须被点名为孤儿' -Skip
It '归档确实还在磁盘上,manifest 里也还留着历史记录' {
Test-Path -LiteralPath (Join-Path $script:OrphanBackupDir 'my-app.7z') | Should -BeTrue
$manifest = Read-BaknretManifest -Path (Join-Path $script:OrphanBackupDir 'manifest.json')
$manifest = Read-BakNRetManifest -Path (Join-Path $script:OrphanBackupDir 'manifest.json')
$manifest.items.Contains('my-app') | Should -BeTrue
}
@@ -610,16 +610,16 @@ Describe 'manifest 一致性:archive 字段只在文件真的存在时才写'
Set-Content -Encoding UTF8 -LiteralPath (Join-Path $script:SyncRoot 'real.7z') 'x'
$script:SyncManifestPath = Join-Path $script:SyncRoot 'manifest.json'
$manifest = Read-BaknretManifest -Path $script:SyncManifestPath
$manifest = Read-BakNRetManifest -Path $script:SyncManifestPath
$manifest.items['real'] = [pscustomobject]@{ archive = 'real.7z'; action = 'backed-up'; source = 'real' }
$manifest.items['gone'] = [pscustomobject]@{ archive = 'gone.7z'; action = 'missing-source'; source = 'gone' }
$manifest.items['none'] = [pscustomobject]@{ archive = $null; action = 'missing-source'; source = 'none' }
Write-BaknretManifest -Path $script:SyncManifestPath -Manifest $manifest | Out-Null
Write-BakNRetManifest -Path $script:SyncManifestPath -Manifest $manifest | Out-Null
}
It '存在的归档保留 archive,不存在的被清空' {
$manifest = Read-BaknretManifest -Path $script:SyncManifestPath
$cleared = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$manifest = Read-BakNRetManifest -Path $script:SyncManifestPath
$cleared = Sync-BakNRetManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$cleared.Count | Should -Be 1
$cleared[0] | Should -Be 'gone'
@@ -629,16 +629,16 @@ Describe 'manifest 一致性:archive 字段只在文件真的存在时才写'
}
It '清空 archive 时保留条目本身的历史(source / action 不动)' {
$manifest = Read-BaknretManifest -Path $script:SyncManifestPath
$null = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$manifest = Read-BakNRetManifest -Path $script:SyncManifestPath
$null = Sync-BakNRetManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$manifest.items['gone'].source | Should -Be 'gone'
$manifest.items['gone'].action | Should -Be 'missing-source'
}
It '人工删掉归档之后再同步一次,记录会被纠正过来' {
Remove-Item -LiteralPath (Join-Path $script:SyncRoot 'real.7z') -Force
$manifest = Read-BaknretManifest -Path $script:SyncManifestPath
$cleared = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$manifest = Read-BakNRetManifest -Path $script:SyncManifestPath
$cleared = Sync-BakNRetManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$cleared | Should -Contain 'real'
$manifest.items['real'].archive | Should -BeNullOrEmpty
+40 -40
View File
@@ -118,7 +118,7 @@ BeforeAll {
Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue
}
function Invoke-BaknretScript {
function Invoke-BakNRetScript {
<# .SYNOPSIS 用子进程跑 Backup.ps1 / Restore.ps1(两个脚本结尾都 exit,必须独立进程)。 #>
param(
[Parameter(Mandatory = $true)][string]$Script,
@@ -224,31 +224,31 @@ AfterAll {
Describe '排除判定与 7z 的 -x! / -xr! 语义对齐' {
# ============================================================================
It '锚定模式只命中它自己那棵子树' {
Test-BaknretPathExcluded -RelativePath 'Default\Cache' -Patterns @('Default\Cache') | Should -BeTrue
Test-BaknretPathExcluded -RelativePath 'Default\Cache\sub\x.bin' -Patterns @('Default\Cache') | Should -BeFalse
Test-BaknretPathExcluded -RelativePath 'Other\Cache' -Patterns @('Default\Cache') | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath 'Default\Cache' -Patterns @('Default\Cache') | Should -BeTrue
Test-BakNRetPathExcluded -RelativePath 'Default\Cache\sub\x.bin' -Patterns @('Default\Cache') | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath 'Other\Cache' -Patterns @('Default\Cache') | Should -BeFalse
}
It '! 通配按任意层级的组件名匹配(* 不是正则)' {
Test-BaknretPathExcluded -RelativePath 'a\Code Cache\f' -Patterns @('!*Cache') | Should -BeTrue
Test-BaknretPathExcluded -RelativePath 'Code Cache' -Patterns @('!*Cache') | Should -BeTrue
Test-BaknretPathExcluded -RelativePath 'a\teamcache.bin' -Patterns @('!*Cache') | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath 'a\Code Cache\f' -Patterns @('!*Cache') | Should -BeTrue
Test-BakNRetPathExcluded -RelativePath 'Code Cache' -Patterns @('!*Cache') | Should -BeTrue
Test-BakNRetPathExcluded -RelativePath 'a\teamcache.bin' -Patterns @('!*Cache') | Should -BeFalse
}
It '!re: 走正则,且组件名与整条相对路径都算命中' {
Test-BaknretPathExcluded -RelativePath 'x\y\a.log' -Patterns @('!re:\.log$') | Should -BeTrue
Test-BaknretPathExcluded -RelativePath 'x\y\a.txt' -Patterns @('!re:\.log$') | Should -BeFalse
Test-BaknretPathExcluded -RelativePath 'Cache\data' -Patterns @('!re:^Cache$') | Should -BeTrue
Test-BakNRetPathExcluded -RelativePath 'x\y\a.log' -Patterns @('!re:\.log$') | Should -BeTrue
Test-BakNRetPathExcluded -RelativePath 'x\y\a.txt' -Patterns @('!re:\.log$') | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath 'Cache\data' -Patterns @('!re:^Cache$') | Should -BeTrue
}
It '没有模式时一律不排除' {
Test-BaknretPathExcluded -RelativePath 'a\b' -Patterns @() | Should -BeFalse
Test-BaknretPathExcluded -RelativePath '' -Patterns @('!*') | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath 'a\b' -Patterns @() | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath '' -Patterns @('!*') | Should -BeFalse
}
It '模式里的空格按 7z 的规矩当 ? 处理' {
Test-BaknretPathExcluded -RelativePath 'a\Cache' -Patterns @('!*Cache Extras') | Should -BeFalse
Test-BaknretPathExcluded -RelativePath 'a\Cache Extras' -Patterns @('!*Cache Extras') | Should -BeTrue
Test-BakNRetPathExcluded -RelativePath 'a\Cache' -Patterns @('!*Cache Extras') | Should -BeFalse
Test-BakNRetPathExcluded -RelativePath 'a\Cache Extras' -Patterns @('!*Cache Extras') | Should -BeTrue
}
}
@@ -257,19 +257,19 @@ Describe 'SID 映射(跨机恢复)' {
# ============================================================================
It '整 SID 精确替换' {
$sddl = 'O:S-1-5-21-1-2-3-1001G:S-1-5-21-1-2-3-1001D:(A;;FA;;;S-1-5-21-1-2-3-1001)'
$mapped = Convert-BaknretSidMap -Sddl $sddl -SidMap @{ 'S-1-5-21-1-2-3-1001' = 'S-1-5-21-9-8-7-1001' }
$mapped = Convert-BakNRetSidMap -Sddl $sddl -SidMap @{ 'S-1-5-21-1-2-3-1001' = 'S-1-5-21-9-8-7-1001' }
$mapped | Should -Be 'O:S-1-5-21-9-8-7-1001G:S-1-5-21-9-8-7-1001D:(A;;FA;;;S-1-5-21-9-8-7-1001)'
}
It '不会误伤以它为前缀的更长的 SID' {
$sddl = 'D:(A;;FA;;;S-1-5-21-1-2-3-1001)(A;;FA;;;S-1-5-21-1-2-3-10012)'
$mapped = Convert-BaknretSidMap -Sddl $sddl -SidMap @{ 'S-1-5-21-1-2-3-1001' = 'S-1-5-21-9-8-7-1001' }
$mapped = Convert-BakNRetSidMap -Sddl $sddl -SidMap @{ 'S-1-5-21-1-2-3-1001' = 'S-1-5-21-9-8-7-1001' }
$mapped | Should -Be 'D:(A;;FA;;;S-1-5-21-9-8-7-1001)(A;;FA;;;S-1-5-21-1-2-3-10012)'
}
It '空映射表时原样返回' {
$sddl = 'D:(A;;FA;;;SY)'
Convert-BaknretSidMap -Sddl $sddl -SidMap @{} | Should -Be $sddl
Convert-BakNRetSidMap -Sddl $sddl -SidMap @{} | Should -Be $sddl
}
}
@@ -283,7 +283,7 @@ Describe '安全描述符采集' {
}
It 'Full:每个对象一条记录,键是归档内相对路径' {
$capture = Get-BaknretSecurityRecords -Items @($script:CaptureItem) -Mode Full
$capture = Get-BakNRetSecurityRecords -Items @($script:CaptureItem) -Mode Full
$capture.Scanned | Should -Be 3
$capture.Kept | Should -Be 3
$capture.Errors | Should -Be 0
@@ -291,7 +291,7 @@ Describe '安全描述符采集' {
}
It '根记录的 SDDL 保留了 CREATOR OWNER、IO 标志、孤儿 SID 和 protected 位' {
$capture = Get-BaknretSecurityRecords -Items @($script:CaptureItem) -Mode Full
$capture = Get-BakNRetSecurityRecords -Items @($script:CaptureItem) -Mode Full
$root = @($capture.Records | Where-Object { $_.p -eq 'Data' })[0]
$root.s | Should -Match 'D:PAI'
$root.s | Should -Match '\(A;OICIIO;GA;;;CO\)'
@@ -300,23 +300,23 @@ Describe '安全描述符采集' {
}
It 'Smart 比 Full 少,但根永远保留' {
$full = Get-BaknretSecurityRecords -Items @($script:CaptureItem) -Mode Full
$smart = Get-BaknretSecurityRecords -Items @($script:CaptureItem) -Mode Smart
$full = Get-BakNRetSecurityRecords -Items @($script:CaptureItem) -Mode Full
$smart = Get-BakNRetSecurityRecords -Items @($script:CaptureItem) -Mode Smart
$smart.Kept | Should -BeLessOrEqual $full.Kept
@($smart.Records | ForEach-Object { $_.p }) | Should -Contain 'Data'
}
It 'Roots 只存归档项的根,不再往下走' {
$roots = Get-BaknretSecurityRecords -Items @($script:CaptureItem) -Mode Roots
$roots = Get-BakNRetSecurityRecords -Items @($script:CaptureItem) -Mode Roots
$roots.Kept | Should -Be 1
$roots.Records[0].p | Should -Be 'Data'
}
It 'sidecar 往返:条数与 SDDL 原样保留' {
$capture = Get-BaknretSecurityRecords -Items @($script:CaptureItem) -Mode Full
$capture = Get-BakNRetSecurityRecords -Items @($script:CaptureItem) -Mode Full
$path = Join-Path $script:Sandbox 'roundtrip.acl.json'
Save-BaknretSecuritySidecar -Path $path -Records $capture.Records -Mode Full -Errors $capture.Errors -Scanned $capture.Scanned | Out-Null
$sidecar = Read-BaknretSecuritySidecar -Path $path
Save-BakNRetSecuritySidecar -Path $path -Records $capture.Records -Mode Full -Errors $capture.Errors -Scanned $capture.Scanned | Out-Null
$sidecar = Read-BakNRetSecuritySidecar -Path $path
$sidecar.Records.Count | Should -Be 3
$record = @($sidecar.Records | Where-Object { $_.p -eq 'Data\sub\a.txt' })[0]
$record.k | Should -Be 'f'
@@ -324,7 +324,7 @@ Describe '安全描述符采集' {
}
It '旁挂文件不存在时读出 $null(调用方据此打告警,而不是静默当没事)' {
Read-BaknretSecuritySidecar -Path (Join-Path $script:Sandbox 'nope.acl.json') | Should -BeNullOrEmpty
Read-BakNRetSecuritySidecar -Path (Join-Path $script:Sandbox 'nope.acl.json') | Should -BeNullOrEmpty
}
It '排除模式在采集时同样生效(采集树 == 归档树)' {
@@ -336,7 +336,7 @@ Describe '安全描述符采集' {
[System.IO.File]::WriteAllText((Join-Path $walkRoot 'keep.txt'), 'x')
$walkItem = [pscustomobject]@{ ArchivePath = 'Data'; RealPath = $walkRoot }
$capture = Get-BaknretSecurityRecords -Items @($walkItem) -Mode Full -ScopeMap @{ 0 = @('!Cache') }
$capture = Get-BakNRetSecurityRecords -Items @($walkItem) -Mode Full -ScopeMap @{ 0 = @('!Cache') }
@($capture.Records | ForEach-Object { $_.p }) | Should -Not -Contain 'Data\Cache'
@($capture.Records | ForEach-Object { $_.p }) | Should -Contain 'Data\keep.txt'
}
@@ -350,17 +350,17 @@ Describe '安全描述符回放' {
$script:TargetRoot = Join-Path $script:Sandbox 'restore\target'
$script:RestoreFingerprints = New-AclSourceTree -Root $script:RestoreRoot
$capture = Get-BaknretSecurityRecords -Items @([pscustomobject]@{ ArchivePath = 'Data'; RealPath = $script:RestoreRoot }) -Mode Full
$capture = Get-BakNRetSecurityRecords -Items @([pscustomobject]@{ ArchivePath = 'Data'; RealPath = $script:RestoreRoot }) -Mode Full
$script:RestoreSidecarPath = Join-Path $script:Sandbox 'restore.acl.json'
Save-BaknretSecuritySidecar -Path $script:RestoreSidecarPath -Records $capture.Records -Mode Full -Errors $capture.Errors -Scanned $capture.Scanned | Out-Null
$script:RestoreSidecar = Read-BaknretSecuritySidecar -Path $script:RestoreSidecarPath
Save-BakNRetSecuritySidecar -Path $script:RestoreSidecarPath -Records $capture.Records -Mode Full -Errors $capture.Errors -Scanned $capture.Scanned | Out-Null
$script:RestoreSidecar = Read-BakNRetSecuritySidecar -Path $script:RestoreSidecarPath
}
It '回放后根对象的安全描述符与源逐字节一致(protected / CO / 孤儿 SID 全在)' {
# 干净目标:只拷内容,不带 ACL(ACL 是新建对象的默认值)
& robocopy.exe $script:RestoreRoot $script:TargetRoot /E /COPY:DAT /NFL /NDL /NJH /NJS /NP | Out-Null
$result = Restore-BaknretSecurity -Sidecar $script:RestoreSidecar -ArchiveRoot 'Data' -TargetPath $script:TargetRoot
$result = Restore-BakNRetSecurity -Sidecar $script:RestoreSidecar -ArchiveRoot 'Data' -TargetPath $script:TargetRoot
$result.Total | Should -Be 3
$result.Failed | Should -Be 0
$result.Applied | Should -Be 3
@@ -374,7 +374,7 @@ Describe '安全描述符回放' {
$targetPath = if ($relative -eq '.') { $script:TargetRoot } else { Join-Path $script:TargetRoot $relative }
# 唯一允许的差异:陈旧继承 ACE 被"冻结"成显式 + protected 的对象,
# protected 位会从 False 变 True(见 Get-BaknretSecuritySddlWithStale)。
# protected 位会从 False 变 True(见 Get-BakNRetSecuritySddlWithStale)。
$expected = $script:RestoreFingerprints[$relative] -replace ' P=(True|False) ', ' P='
$actual = (Get-AclFingerprint -Path $targetPath) -replace ' P=(True|False) ', ' P='
$actual | Should -Be $expected -Because "$relative 的 ACE 集合应当与源一致"
@@ -382,7 +382,7 @@ Describe '安全描述符回放' {
}
It '目标不存在或不是普通对象时记 Skipped,不记 Failed' {
$result = Restore-BaknretSecurity -Sidecar $script:RestoreSidecar -ArchiveRoot 'Data' `
$result = Restore-BakNRetSecurity -Sidecar $script:RestoreSidecar -ArchiveRoot 'Data' `
-TargetPath (Join-Path $script:Sandbox 'restore\does-not-exist')
$result.Total | Should -Be 3
$result.Skipped | Should -Be 3
@@ -390,7 +390,7 @@ Describe '安全描述符回放' {
}
It '归档根名对不上时一条都不回放(不会把兄弟项的 ACL 倒过来)' {
$result = Restore-BaknretSecurity -Sidecar $script:RestoreSidecar -ArchiveRoot 'Other' -TargetPath $script:TargetRoot
$result = Restore-BakNRetSecurity -Sidecar $script:RestoreSidecar -ArchiveRoot 'Other' -TargetPath $script:TargetRoot
$result.Total | Should -Be 0
$result.Applied | Should -Be 0
}
@@ -406,7 +406,7 @@ Describe '安全描述符回放' {
Records = @([pscustomobject]@{ p = 'Data'; k = 'd'; s = $sddl })
}
$result = Restore-BaknretSecurity -Sidecar $sidecar -ArchiveRoot 'Data' -TargetPath $path
$result = Restore-BakNRetSecurity -Sidecar $sidecar -ArchiveRoot 'Data' -TargetPath $path
$result.Failed | Should -Be 0
($result.Applied + $result.OwnerFailed) | Should -Be 1
(Get-Acl -LiteralPath $path).Sddl | Should -Match '\(A;;FA;;;SY\)'
@@ -416,7 +416,7 @@ Describe '安全描述符回放' {
$record = [pscustomobject]@{ p = 'Data'; k = 'd'; s = $null; e = '读不到' }
$sidecar = [pscustomobject]@{ Records = @($record) }
$path = Join-Path $script:Sandbox 'restore\bogus-group'
$result = Restore-BaknretSecurity -Sidecar $sidecar -ArchiveRoot 'Data' -TargetPath $path
$result = Restore-BakNRetSecurity -Sidecar $sidecar -ArchiveRoot 'Data' -TargetPath $path
$result.Skipped | Should -Be 1
$result.Applied | Should -Be 0
$result.Failed | Should -Be 0
@@ -432,7 +432,7 @@ Describe '与 Backup.ps1 / Restore.ps1 的集成' -Skip:(-not $script:HasSevenZi
}
It '备份会写出 <归档名>.acl.json,并在 manifest 里记下它' {
$result = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$result = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:Harness.ListPath
ConfigPath = $script:Harness.ConfigPath
BackupDir = $script:Harness.BackupDir
@@ -454,7 +454,7 @@ Describe '与 Backup.ps1 / Restore.ps1 的集成' -Skip:(-not $script:HasSevenZi
Reset-AclTree -Path $script:Harness.SourcePath
(Test-Path -LiteralPath $script:Harness.SourcePath) | Should -BeFalse
$result = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$result = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:Harness.ListPath
ConfigPath = $script:Harness.ConfigPath
BackupDir = $script:Harness.BackupDir
@@ -476,7 +476,7 @@ Describe '与 Backup.ps1 / Restore.ps1 的集成' -Skip:(-not $script:HasSevenZi
It '-SkipSecurity 时不回放(目标保持新建对象的默认 ACL)' {
Reset-AclTree -Path $script:Harness.SourcePath
$result = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$result = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:Harness.ListPath
ConfigPath = $script:Harness.ConfigPath
BackupDir = $script:Harness.BackupDir
@@ -491,7 +491,7 @@ Describe '与 Backup.ps1 / Restore.ps1 的集成' -Skip:(-not $script:HasSevenZi
Reset-AclTree -Path $script:Harness.SourcePath
Get-ChildItem -LiteralPath $script:Harness.BackupDir -Filter '*.acl.json' | Remove-Item -Force
$result = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$result = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:Harness.ListPath
ConfigPath = $script:Harness.ConfigPath
BackupDir = $script:Harness.BackupDir
+173 -173
View File
@@ -54,7 +54,7 @@ BeforeAll {
# 于是这里改成:把整条命令写进临时 .cmd,由 cmd 自己把输出重定向到文件,
# 再用 Invoke-ExternalCommand 调 `cmd /c <批处理>`。退出码来自真实进程,
# 输出从文件读回,全程不经过 PowerShell 的管道。
function Invoke-BaknretScript {
function Invoke-BakNRetScript {
param(
[Parameter(Mandatory = $true)][string]$Script,
[hashtable]$Parameters = @{}
@@ -337,29 +337,29 @@ Describe 'BackupList.txt 解析' {
}
It '记号切分:引号内的空白不切分,引号本身留在记号里' {
(@(Split-BaknretToken -Text 'A "B C" D') -join '|') | Should -Be 'A|"B C"|D'
(@(Split-BakNRetToken -Text 'A "B C" D') -join '|') | Should -Be 'A|"B C"|D'
# 引号不配对时按"引号延伸到行尾"处理,不抛异常
(@(Split-BaknretToken -Text 'A "B C') -join '|') | Should -Be 'A|"B C'
(@(Split-BaknretToken -Text '') -join '|') | Should -Be ''
(@(Split-BakNRetToken -Text 'A "B C') -join '|') | Should -Be 'A|"B C'
(@(Split-BakNRetToken -Text '') -join '|') | Should -Be ''
}
It '记号识别只认完整记号' {
(Test-BaknretMarker -Token '::') | Should -Be 'path'
(Test-BaknretMarker -Token ':-') | Should -Be 'exclude'
(Test-BaknretMarker -Token ':+') | Should -Be 'include'
(Test-BaknretMarker -Token ':encrypt') | Should -Be 'encrypt'
(Test-BaknretMarker -Token ':!encrypt') | Should -Be 'noencrypt'
(Test-BaknretMarker -Token '@') | Should -Be 'at'
(Test-BaknretMarker -Token 'C:\a:-b') | Should -BeNullOrEmpty
(Test-BaknretMarker -Token ':') | Should -BeNullOrEmpty
(Test-BakNRetMarker -Token '::') | Should -Be 'path'
(Test-BakNRetMarker -Token ':-') | Should -Be 'exclude'
(Test-BakNRetMarker -Token ':+') | Should -Be 'include'
(Test-BakNRetMarker -Token ':encrypt') | Should -Be 'encrypt'
(Test-BakNRetMarker -Token ':!encrypt') | Should -Be 'noencrypt'
(Test-BakNRetMarker -Token '@') | Should -Be 'at'
(Test-BakNRetMarker -Token 'C:\a:-b') | Should -BeNullOrEmpty
(Test-BakNRetMarker -Token ':') | Should -BeNullOrEmpty
}
It '去引号:成对才去,不成对原样返回' {
(Remove-BaknretQuote -Text '"a"') | Should -Be 'a'
(Remove-BaknretQuote -Text "'a'") | Should -Be 'a'
(Remove-BaknretQuote -Text 'a') | Should -Be 'a'
(Remove-BaknretQuote -Text '"a') | Should -Be '"a'
(Remove-BaknretQuote -Text '""') | Should -Be ''
(Remove-BakNRetQuote -Text '"a"') | Should -Be 'a'
(Remove-BakNRetQuote -Text "'a'") | Should -Be 'a'
(Remove-BakNRetQuote -Text 'a') | Should -Be 'a'
(Remove-BakNRetQuote -Text '"a') | Should -Be '"a'
(Remove-BakNRetQuote -Text '""') | Should -Be ''
}
}
@@ -382,49 +382,49 @@ Describe '归档命名' {
# 注意:Pester 的 It 名字里出现 $( ) 会被求值,所以标题里不要写子表达式。
It '基础命名规则:末级名_from_上级路径用加号连接' {
Get-BackupBaseName -RawPath 'C:\Programs\FooClolor' | Should -Be 'FooClolor_from_C_+Programs'
Get-BakNRetBackupBaseName -RawPath 'C:\Programs\FooClolor' | Should -Be 'FooClolor_from_C_+Programs'
}
It '/ 与 \ 以及重复分隔符结果一致' {
$a = Get-BackupBaseName -RawPath 'C:\a/b'
$b = Get-BackupBaseName -RawPath 'C:\\a\\\\b'
$a = Get-BakNRetBackupBaseName -RawPath 'C:\a/b'
$b = Get-BakNRetBackupBaseName -RawPath 'C:\\a\\\\b'
$a | Should -Be $b
}
It '命名与路径往返' {
$base = Get-BackupBaseName -RawPath 'D:\UserData\Documents\Aria'
Convert-BackupFileNameToPath -FileName ($base + '.7z') | Should -Be 'D:\UserData\Documents\Aria'
$base = Get-BakNRetBackupBaseName -RawPath 'D:\UserData\Documents\Aria'
Convert-BakNRetBackupFileNameToPath -FileName ($base + '.7z') | Should -Be 'D:\UserData\Documents\Aria'
}
It '归档名里不含非法文件名字符' {
$base = Get-BackupBaseName -RawPath 'C:\a<b>c|d?e*f'
$base = Get-BakNRetBackupBaseName -RawPath 'C:\a<b>c|d?e*f'
($base.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars())) | Should -Be -1
}
It '%变量% 写法里的 % 会保留在归档名里' {
Get-BackupBaseName -RawPath '%UserProfile%\.ssh' | Should -Be '.ssh_from_%UserProfile%'
Get-BakNRetBackupBaseName -RawPath '%UserProfile%\.ssh' | Should -Be '.ssh_from_%UserProfile%'
}
It '软件名条目:默认用软件名做归档名' {
$entry = ConvertFrom-BackupListLine -Line 'my-app'
(Get-ItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be 'my-app'
(Get-BakNRetItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be 'my-app'
}
It '字面路径条目:仍用路径命名算法(现有清单无需改写)' {
$entry = ConvertFrom-BackupListLine -Line 'C:\Programs\FooClolor'
(Get-ItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be 'FooClolor_from_C_+Programs'
(Get-BakNRetItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be 'FooClolor_from_C_+Programs'
}
It '@pathname 用名录里的真实路径命名,而不是软件名' {
$entry = ConvertFrom-BackupListLine -Line 'my-app @pathname'
$expected = Get-BackupBaseName -RawPath $script:NamingApp
(Get-ItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be $expected
$expected = Get-BakNRetBackupBaseName -RawPath $script:NamingApp
(Get-BakNRetItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be $expected
}
It '名录里没有该名字:归档名退回可读目录名,并给出 Error' {
$entry = ConvertFrom-BackupListLine -Line 'no-such-thing'
(Get-ItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be 'no-such-thing'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3
(Get-BakNRetItemArchiveName -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3) | Should -Be 'no-such-thing'
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:NamingCatalog -MaxDepth 3
$resolved.Error | Should -Not -BeNullOrEmpty
@($resolved.Items).Count | Should -Be 0
}
@@ -443,142 +443,142 @@ Describe '7z 排除参数翻译' {
function New-TestArchiveItem {
param([string]$ArchivePath, [string]$RealPath = 'C:\nonexistent-path')
New-BaknretArchiveItem -ArchivePath $ArchivePath -RealPath $RealPath -Kind 'path' -Origin 'path' -Exists $false -IsFile $false
New-BakNRetArchiveItem -ArchivePath $ArchivePath -RealPath $RealPath -Kind 'path' -Origin 'path' -Exists $false -IsFile $false
}
}
It '相对模式自动补上归档根目录名' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Foo') -Patterns @('logs\')
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Foo') -Patterns @('logs\')
@($result.Arguments) | Should -Contain '-x!Foo\logs'
$result.Error | Should -BeNullOrEmpty
}
It '模式已带根名前缀时,Split-BaknretPatternScope 先摘掉前缀,结果不重复' {
It '模式已带根名前缀时,Split-BakNRetPatternScope 先摘掉前缀,结果不重复' {
$item = New-TestArchiveItem -ArchivePath 'Foo'
$map = Split-BaknretPatternScope -Items @($item) -Patterns @('Foo\logs\')
$map = Split-BakNRetPatternScope -Items @($item) -Patterns @('Foo\logs\')
(@($map[0]) -join '|') | Should -Be 'logs'
$result = Get-BaknretExcludeArgument -Item $item -Patterns @($map[0])
$result = Get-BakNRetExcludeArgument -Item $item -Patterns @($map[0])
@($result.Arguments) | Should -Contain '-x!Foo\logs'
@($result.Arguments) | Should -Not -Contain '-x!Foo\Foo\logs'
}
It '! 前缀翻译成递归组件匹配(-xr!)' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'User Data') -Patterns @('!*Cache')
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'User Data') -Patterns @('!*Cache')
@($result.Arguments) | Should -Contain '-xr!*Cache'
}
It '模式里的空格转成 ?(归档项自己的名字按原样保留)' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'User Data') -Patterns @('Default\Code Cache')
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'User Data') -Patterns @('Default\Code Cache')
@($result.Arguments) | Should -Contain '-x!User Data\Default\Code?Cache'
$plain = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Foo') -Patterns @('Default\Code Cache')
$plain = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Foo') -Patterns @('Default\Code Cache')
@($plain.Arguments) | Should -Contain '-x!Foo\Default\Code?Cache'
}
It '生成的参数里绝不出现引号(旧实现 -x!"路径" 让排除全部失效)' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'User Data') -Patterns @('!*Cache', 'Default\Code Cache')
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'User Data') -Patterns @('!*Cache', 'Default\Code Cache')
(@($result.Arguments) -join ' ') | Should -Not -Match '"'
}
It '空模式被忽略' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Foo') -Patterns @('', ' ', '!')
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Foo') -Patterns @('', ' ', '!')
@($result.Arguments).Count | Should -Be 0
$result.Error | Should -BeNullOrEmpty
}
It '!re: 会把正则展开成精确的 -x! 参数' {
$item = New-TestArchiveItem -ArchivePath 'Top' -RealPath $script:ExcludeTree
$result = Get-BaknretExcludeArgument -Item $item -Patterns @('!re:^Cache$')
$result = Get-BakNRetExcludeArgument -Item $item -Patterns @('!re:^Cache$')
@($result.Arguments) | Should -Contain '-x!Top\sub\Cache'
# 命中目录后不再往下走(一个命中只产生一条参数,避免命令行爆炸)
$pruned = Get-BaknretExcludeArgument -Item $item -Patterns @('!re:^sub$')
$pruned = Get-BakNRetExcludeArgument -Item $item -Patterns @('!re:^sub$')
@($pruned.Arguments) | Should -Contain '-x!Top\sub'
@($pruned.Arguments).Count | Should -Be 1
}
It '非法正则给出 Error,而不是抛异常' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Top' -RealPath $script:ExcludeTree) -Patterns @('!re:[')
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Top' -RealPath $script:ExcludeTree) -Patterns @('!re:[')
@($result.Arguments).Count | Should -Be 0
$result.Error | Should -Not -BeNullOrEmpty
}
It '正则命中数超过上限时明确报错' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Top' -RealPath $script:ExcludeTree) `
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Top' -RealPath $script:ExcludeTree) `
-Patterns @('!re:.') -MaxRegexMatches 1
$result.Error | Should -Not -BeNullOrEmpty
}
It '参数总长超过安全上限时明确报错' {
$result = Get-BaknretExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Top') `
$result = Get-BakNRetExcludeArgument -Item (New-TestArchiveItem -ArchivePath 'Top') `
-Patterns @('!*zzz') -MaxCommandLineChars 5
$result.Error | Should -Not -BeNullOrEmpty
}
It 'Split-BaknretPatternScope:指名 Slot 的模式只分给那个 Slot' {
It 'Split-BakNRetPatternScope:指名 Slot 的模式只分给那个 Slot' {
$items = @(
New-TestArchiveItem -ArchivePath 'Alpha'
New-TestArchiveItem -ArchivePath 'Beta'
)
$map = Split-BaknretPatternScope -Items $items -Patterns @('Alpha\logs\')
$map = Split-BakNRetPatternScope -Items $items -Patterns @('Alpha\logs\')
(@($map[0]) -join '|') | Should -Be 'logs'
@($map[1]).Count | Should -Be 0
# 用 / 分隔也一样
$slash = Split-BaknretPatternScope -Items $items -Patterns @('Beta/sub')
$slash = Split-BakNRetPatternScope -Items $items -Patterns @('Beta/sub')
(@($slash[1]) -join '|') | Should -Be 'sub'
@($slash[0]).Count | Should -Be 0
}
It 'Split-BaknretPatternScope:没点名任何 Slot 的模式广播给每一项' {
It 'Split-BakNRetPatternScope:没点名任何 Slot 的模式广播给每一项' {
$items = @(
New-TestArchiveItem -ArchivePath 'Alpha'
New-TestArchiveItem -ArchivePath 'Beta'
)
$map = Split-BaknretPatternScope -Items $items -Patterns @('plain\path', 'Alpha')
$map = Split-BakNRetPatternScope -Items $items -Patterns @('plain\path', 'Alpha')
(@($map[0]) -join '|') | Should -Be 'plain\path|Alpha'
(@($map[1]) -join '|') | Should -Be 'plain\path|Alpha'
}
It 'Split-BaknretPatternScope:! 与 !re: 模式广播给每一项' {
It 'Split-BakNRetPatternScope:! 与 !re: 模式广播给每一项' {
$items = @(
New-TestArchiveItem -ArchivePath 'Alpha'
New-TestArchiveItem -ArchivePath 'Beta'
)
$map = Split-BaknretPatternScope -Items $items -Patterns @('!*Cache', '!re:^x$')
$map = Split-BakNRetPatternScope -Items $items -Patterns @('!*Cache', '!re:^x$')
(@($map[0]) -join '|') | Should -Be '!*Cache|!re:^x$'
(@($map[1]) -join '|') | Should -Be '!*Cache|!re:^x$'
}
It 'Split-BaknretPatternScope:每个归档项都有键(没有模式时是空数组)' {
It 'Split-BakNRetPatternScope:每个归档项都有键(没有模式时是空数组)' {
$items = @(
New-TestArchiveItem -ArchivePath 'Alpha'
New-TestArchiveItem -ArchivePath 'Beta'
)
$map = Split-BaknretPatternScope -Items $items -Patterns @()
$map = Split-BakNRetPatternScope -Items $items -Patterns @()
$map.ContainsKey(0) | Should -BeTrue
$map.ContainsKey(1) | Should -BeTrue
@($map[0]).Count | Should -Be 0
}
It 'Merge-BaknretExcludeArgument 去重且保持顺序' {
$merged = Merge-BaknretExcludeArgument -ArgumentLists @(@('-x!a', '-x!b'), @('-x!b', '-x!c'))
It 'Merge-BakNRetExcludeArgument 去重且保持顺序' {
$merged = Merge-BakNRetExcludeArgument -ArgumentLists @(@('-x!a', '-x!b'), @('-x!b', '-x!c'))
(@($merged) -join ' ') | Should -Be '-x!a -x!b -x!c'
}
It '多 Slot 条目:逐项分配 + 翻译 + 去重合成一份参数' {
$items = @(
New-BaknretArchiveItem -ArchivePath 'Alpha' -RealPath $script:ExcludeTree -Kind 'slot' -Slot 'Alpha' -Origin 'catalog' -Exists $true
New-BaknretArchiveItem -ArchivePath 'Beta' -RealPath $script:ExcludeTree -Kind 'slot' -Slot 'Beta' -Origin 'catalog' -Exists $true
New-BakNRetArchiveItem -ArchivePath 'Alpha' -RealPath $script:ExcludeTree -Kind 'slot' -Slot 'Alpha' -Origin 'catalog' -Exists $true
New-BakNRetArchiveItem -ArchivePath 'Beta' -RealPath $script:ExcludeTree -Kind 'slot' -Slot 'Beta' -Origin 'catalog' -Exists $true
)
$patterns = @('Alpha\logs\', '!*Cache', 'Beta\sub', '!*Cache')
$map = Split-BaknretPatternScope -Items $items -Patterns $patterns
$map = Split-BakNRetPatternScope -Items $items -Patterns $patterns
$lists = @()
for ($index = 0; $index -lt $items.Count; $index++) {
$lists += , @((Get-BaknretExcludeArgument -Item $items[$index] -Patterns @($map[$index])).Arguments)
$lists += , @((Get-BakNRetExcludeArgument -Item $items[$index] -Patterns @($map[$index])).Arguments)
}
$effective = @(Merge-BaknretExcludeArgument -ArgumentLists $lists)
$effective = @(Merge-BakNRetExcludeArgument -ArgumentLists $lists)
$effective | Should -Contain '-x!Alpha\logs'
$effective | Should -Contain '-x!Beta\sub'
$effective | Should -Contain '-xr!*Cache'
@@ -600,8 +600,8 @@ Describe '归档项与暂存目录' {
Set-Content -Encoding UTF8 -LiteralPath $script:StagingFile 'file-content'
}
It 'New-BaknretArchiveItem 规范化归档内路径并算出 TopName' {
$item = New-BaknretArchiveItem -ArchivePath '\Alpha\Sub\' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha'
It 'New-BakNRetArchiveItem 规范化归档内路径并算出 TopName' {
$item = New-BakNRetArchiveItem -ArchivePath '\Alpha\Sub\' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha'
$item.ArchivePath | Should -Be 'Alpha\Sub'
$item.TopName | Should -Be 'Alpha'
$item.Kind | Should -Be 'slot'
@@ -610,10 +610,10 @@ Describe '归档项与暂存目录' {
@($item.Exclude).Count | Should -Be 0
}
It 'Get-BaknretArchiveTopName 取归档内相对路径的第一段' {
(Get-BaknretArchiveTopName -ArchivePath 'A\B\c.txt') | Should -Be 'A'
(Get-BaknretArchiveTopName -ArchivePath ' A ') | Should -Be 'A'
(Get-BaknretArchiveTopName -ArchivePath '') | Should -Be ''
It 'Get-BakNRetArchiveTopName 取归档内相对路径的第一段' {
(Get-BakNRetArchiveTopName -ArchivePath 'A\B\c.txt') | Should -Be 'A'
(Get-BakNRetArchiveTopName -ArchivePath ' A ') | Should -Be 'A'
(Get-BakNRetArchiveTopName -ArchivePath '') | Should -Be ''
}
It '暂存半途失败会自己清干净(不留指向真实数据的 junction)' {
@@ -628,12 +628,12 @@ Describe '归档项与暂存目录' {
$items = @(
# 第一项会成功挂上,而且是**目录走 junction** —— 这正是危险的那个物件:它指向
# 真实数据,而临时目录迟早会被某次 Remove-Item -Recurse 扫到。
New-BaknretArchiveItem -ArchivePath 'Good' -RealPath $realDir -Kind 'slot' -Slot 'Good' -Exists $true -IsFile $false
New-BakNRetArchiveItem -ArchivePath 'Good' -RealPath $realDir -Kind 'slot' -Slot 'Good' -Exists $true -IsFile $false
# 第二项必然失败:源文件不存在,硬链接与复制都会失败
New-BaknretArchiveItem -ArchivePath 'Missing.txt' -RealPath (Join-Path $base 'does-not-exist.txt') -Kind 'slot' -Slot 'Missing' -Exists $false -IsFile $true
New-BakNRetArchiveItem -ArchivePath 'Missing.txt' -RealPath (Join-Path $base 'does-not-exist.txt') -Kind 'slot' -Slot 'Missing' -Exists $false -IsFile $true
)
{ New-BaknretArchiveStaging -Items $items -Root (Join-Path $sandbox 'stage') } | Should -Throw
{ New-BakNRetArchiveStaging -Items $items -Root (Join-Path $sandbox 'stage') } | Should -Throw
$left = @(Get-ChildItem -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue)
@($left | Where-Object { $_.LinkType -eq 'Junction' }).Count | Should -Be 0
@@ -644,12 +644,12 @@ Describe '归档项与暂存目录' {
It '目录项用 junction 挂进暂存目录,文件项用硬链接(名字就是归档内路径)' {
$items = @(
New-BaknretArchiveItem -ArchivePath 'Alpha' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha' -Exists $true -IsFile $false
New-BaknretArchiveItem -ArchivePath 'Nested\Deep' -RealPath $script:StagingDir -Kind 'include' -Origin 'include' -Exists $true -IsFile $false
New-BaknretArchiveItem -ArchivePath 'Cfg' -RealPath $script:StagingFile -Kind 'slot' -Slot 'Cfg' -Exists $true -IsFile $true
New-BakNRetArchiveItem -ArchivePath 'Alpha' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha' -Exists $true -IsFile $false
New-BakNRetArchiveItem -ArchivePath 'Nested\Deep' -RealPath $script:StagingDir -Kind 'include' -Origin 'include' -Exists $true -IsFile $false
New-BakNRetArchiveItem -ArchivePath 'Cfg' -RealPath $script:StagingFile -Kind 'slot' -Slot 'Cfg' -Exists $true -IsFile $true
)
$stage = New-BaknretArchiveStaging -Items $items
$stage = New-BakNRetArchiveStaging -Items $items
try {
$alpha = Get-Item -LiteralPath (Join-Path $stage 'Alpha') -Force
$alpha.LinkType | Should -Be 'Junction'
@@ -663,15 +663,15 @@ Describe '归档项与暂存目录' {
(Get-Content -Encoding UTF8 -LiteralPath $cfg.FullName -Raw).Trim() | Should -Be 'file-content'
}
finally {
Remove-BaknretArchiveStaging -Root $stage
Remove-BakNRetArchiveStaging -Root $stage
}
}
It 'Remove-BaknretArchiveStaging 只删连接点,不顺着走进真实目录' {
$stage = New-BaknretArchiveStaging -Items @(
New-BaknretArchiveItem -ArchivePath 'Alpha' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha' -Exists $true -IsFile $false
It 'Remove-BakNRetArchiveStaging 只删连接点,不顺着走进真实目录' {
$stage = New-BakNRetArchiveStaging -Items @(
New-BakNRetArchiveItem -ArchivePath 'Alpha' -RealPath $script:StagingDir -Kind 'slot' -Slot 'Alpha' -Exists $true -IsFile $false
)
Remove-BaknretArchiveStaging -Root $stage
Remove-BakNRetArchiveStaging -Root $stage
Test-Path -LiteralPath $stage | Should -BeFalse
Test-Path -LiteralPath (Join-Path $script:StagingDir 'inner\a.txt') | Should -BeTrue
@@ -683,23 +683,23 @@ Describe '命令行参数拼接' {
# ============================================================================
It '无空格参数原样输出' {
ConvertTo-NativeArgumentString -ArgumentList @('a', '-mx=9') | Should -Be 'a -mx=9'
ConvertTo-BakNRetNativeArgumentString -ArgumentList @('a', '-mx=9') | Should -Be 'a -mx=9'
}
It '含空格参数加引号' {
ConvertTo-NativeArgumentString -ArgumentList @('C:\a b\c') | Should -Be '"C:\a b\c"'
ConvertTo-BakNRetNativeArgumentString -ArgumentList @('C:\a b\c') | Should -Be '"C:\a b\c"'
}
It '引号内的结尾反斜杠翻倍(否则会被当成转义引号)' {
ConvertTo-NativeArgumentString -ArgumentList @('C:\a b\') | Should -Be '"C:\a b\\"'
ConvertTo-BakNRetNativeArgumentString -ArgumentList @('C:\a b\') | Should -Be '"C:\a b\\"'
}
It '内部引号被转义' {
ConvertTo-NativeArgumentString -ArgumentList @('a"b c') | Should -Be '"a\"b c"'
ConvertTo-BakNRetNativeArgumentString -ArgumentList @('a"b c') | Should -Be '"a\"b c"'
}
It '空参数输出一对空引号' {
ConvertTo-NativeArgumentString -ArgumentList @('') | Should -Be '""'
ConvertTo-BakNRetNativeArgumentString -ArgumentList @('') | Should -Be '""'
}
}
@@ -709,11 +709,11 @@ Describe 'manifest 与配置' {
It 'manifest 读写往返' {
$path = Join-Path $script:Sandbox 'roundtrip\manifest.json'
$manifest = Read-BaknretManifest -Path $path
$manifest = Read-BakNRetManifest -Path $path
$manifest.items['demo'] = [pscustomobject]@{ archive = 'demo.7z'; action = 'backed-up'; verified = $true }
Write-BaknretManifest -Path $path -Manifest $manifest | Out-Null
Write-BakNRetManifest -Path $path -Manifest $manifest | Out-Null
$again = Read-BaknretManifest -Path $path
$again = Read-BakNRetManifest -Path $path
$again.items.Count | Should -Be 1
$again.items['demo'].archive | Should -Be 'demo.7z'
$again.items['demo'].verified | Should -BeTrue
@@ -721,7 +721,7 @@ Describe 'manifest 与配置' {
It 'manifest 原样保存 layouts(name/kind),全新恢复时靠它判断目录还是文件' {
$path = Join-Path $script:Sandbox 'layouts\manifest.json'
$manifest = Read-BaknretManifest -Path $path
$manifest = Read-BakNRetManifest -Path $path
$manifest.items['app'] = [pscustomobject]@{
archive = 'app.7z'
layouts = @(
@@ -729,28 +729,28 @@ Describe 'manifest 与配置' {
[pscustomobject]@{ name = 'Cfg'; kind = 'file' }
)
}
Write-BaknretManifest -Path $path -Manifest $manifest | Out-Null
Write-BakNRetManifest -Path $path -Manifest $manifest | Out-Null
$again = Read-BaknretManifest -Path $path
$again = Read-BakNRetManifest -Path $path
@($again.items['app'].layouts).Count | Should -Be 2
(@($again.items['app'].layouts | ForEach-Object { $_.name + ':' + $_.kind }) -join '|') | Should -Be 'Data:dir|Cfg:file'
}
It 'manifest 损坏时不抛异常,而是重建空清单' {
$path = Write-ListFile -Path (Join-Path $script:Sandbox 'broken.json') -Content '{ this is not json'
{ $script:broken = Read-BaknretManifest -Path $path } | Should -Not -Throw
{ $script:broken = Read-BakNRetManifest -Path $path } | Should -Not -Throw
$script:broken.items.Count | Should -Be 0
}
It '配置缺失时返回默认值' {
$config = Get-BaknretConfig -Path (Join-Path $script:Sandbox 'no-such-config.psd1')
$config = Get-BakNRetConfig -Path (Join-Path $script:Sandbox 'no-such-config.psd1')
$config.BackupDir | Should -Be 'Backups'
$config.VerifyArchive | Should -BeTrue
}
It '配置嵌套段落合并且不丢默认键' {
$path = Write-ListFile -Path (Join-Path $script:Sandbox 'cfg.psd1') -Content "@{ MinFreeSpaceGB = 3; Encryption = @{ Enabled = `$true } }"
$config = Get-BaknretConfig -Path $path
$config = Get-BakNRetConfig -Path $path
$config.MinFreeSpaceGB | Should -Be 3
$config.Encryption.Enabled | Should -BeTrue
# 没写到的子键要保留默认值。Encryption 是哈希表,用 ContainsKey 查。
@@ -761,9 +761,9 @@ Describe 'manifest 与配置' {
$saved = $env:BAKNRET_PASSWORD
try {
$env:BAKNRET_PASSWORD = 'sekrit'
(Get-BaknretPassword) | Should -Be 'sekrit'
(Get-BakNRetPassword) | Should -Be 'sekrit'
$env:BAKNRET_PASSWORD = $null
(Get-BaknretPassword) | Should -BeNullOrEmpty
(Get-BakNRetPassword) | Should -BeNullOrEmpty
}
finally {
$env:BAKNRET_PASSWORD = $saved
@@ -827,7 +827,7 @@ Describe '软件名录' {
}
It '名录解析:条目与 Slot 的字段集合就是新契约' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$entry = $catalog['single']
(@($entry.PSObject.Properties.Name) -join ',') | Should -Be 'Name,Path,Description,Slots,Kind,Missing,Error,Raw'
$entry.Name | Should -Be 'single'
@@ -849,7 +849,7 @@ Describe '软件名录' {
}
It '一个软件多个 Slot:Kind=Multi,Slot 按名字排序' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog['multi'].Kind | Should -Be 'Multi'
@($catalog['multi'].Slots).Count | Should -Be 2
(@($catalog['multi'].Slots | ForEach-Object { $_.Name }) -join ',') | Should -Be 'Alpha,Zeta'
@@ -858,7 +858,7 @@ Describe '软件名录' {
}
It 'Kind:Single / Multi / Partial / Unresolved / Invalid' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog['single'].Kind | Should -Be 'Single'
$catalog['multi'].Kind | Should -Be 'Multi'
$catalog['partial'].Kind | Should -Be 'Partial'
@@ -873,7 +873,7 @@ Describe '软件名录' {
}
It '前缀补全:<名>_<后缀> 与 <名>-<后缀> 都会被补全' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog['legendary'].Slots[0].Resolved | Should -Be (Join-Path $script:CatRoot 'legendary_2.0.4')
$catalog['legendary'].Slots[0].Suffixed | Should -BeTrue
@@ -882,12 +882,12 @@ Describe '软件名录' {
}
It '不会把 Legendary 误配成 LegendarySomething' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog['legendary'].Slots[0].Resolved | Should -Not -Be (Join-Path $script:CatRoot 'LegendarySomething')
}
It '一个 Slot 命中多个候选目录:报 Error,绝不悄悄挑一棵树' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$entry = $catalog['dupslot']
$entry.Error | Should -Match '只能对应一个目录'
$entry.Error | Should -Match 'dup_1'
@@ -897,14 +897,14 @@ Describe '软件名录' {
}
It '文件 Slot:Path 指向文件时 IsFile 为真,Encrypt 也带上' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$slot = $catalog['fileslot'].Slots[0]
$slot.IsFile | Should -BeTrue
$slot.Encrypt | Should -BeTrue
}
It '%变量% 会在名录路径里展开' {
$catalog = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$catalog['envpath'].Slots[0].Resolved | Should -Be ([Environment]::ExpandEnvironmentVariables('%TEMP%'))
}
@@ -916,39 +916,39 @@ Describe '软件名录' {
}
It '读取结果按"路径 + 时间戳 + 长度 + 内容 MD5"缓存' {
$first = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3
$second = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3
$first = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3
$second = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3
[object]::ReferenceEquals($first, $second) | Should -BeTrue
$fresh = Get-SoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
$fresh = Get-BakNRetSoftwareCatalog -Path $script:CatFile -MaxDepth 3 -NoCache
[object]::ReferenceEquals($first, $fresh) | Should -BeFalse
}
It '名录文件内容变了以后缓存自动失效' {
$path = Write-ListFile -Path (Join-Path $script:CatRoot 'cache.psd1') -Content "@{ 'x' = @{ S = @{ Path = '$script:CatReal'; Description = 'one' } } }"
(Get-SoftwareCatalog -Path $path -MaxDepth 3)['x'].Description | Should -Be 'one'
(Get-BakNRetSoftwareCatalog -Path $path -MaxDepth 3)['x'].Description | Should -Be 'one'
Write-ListFile -Path $path -Content "@{ 'x' = @{ S = @{ Path = '$script:CatReal'; Description = 'two' } } }" | Out-Null
(Get-SoftwareCatalog -Path $path -MaxDepth 3)['x'].Description | Should -Be 'two'
(Get-BakNRetSoftwareCatalog -Path $path -MaxDepth 3)['x'].Description | Should -Be 'two'
}
It 'Includes:分文件维护的名录会被合并' {
Write-ListFile -Path (Join-Path $script:CatRoot 'Extra.psd1') -Content "@{ 'extra-app' = @{ S = @{ Path = '$script:CatReal' } } }" | Out-Null
$main = Write-ListFile -Path (Join-Path $script:CatRoot 'Main.psd1') -Content "@{ Includes = @('Extra.psd1'); 'main-app' = @{ S = @{ Path = '$script:CatReal' } } }"
$catalog = Get-SoftwareCatalog -Path $main -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $main -MaxDepth 3 -NoCache
$catalog.ContainsKey('main-app') | Should -BeTrue
$catalog.ContainsKey('extra-app') | Should -BeTrue
}
It '旧的裸字符串 / 字符串数组 / 对象数组写法都会报 ERROR 并被跳过' {
$catalog = Get-SoftwareCatalog -Path $script:LegacyFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:LegacyFile -MaxDepth 3 -NoCache
$catalog.ContainsKey('bare') | Should -BeFalse
$catalog.ContainsKey('arr') | Should -BeFalse
$catalog.ContainsKey('objarr') | Should -BeFalse
}
It '旧的 @{ Dirs = @(...) } 写法不再展开:留下 Invalid 条目和原因' {
$catalog = Get-SoftwareCatalog -Path $script:LegacyFile -MaxDepth 3 -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $script:LegacyFile -MaxDepth 3 -NoCache
$catalog.ContainsKey('dirstyle') | Should -BeTrue
$catalog['dirstyle'].Kind | Should -Be 'Invalid'
$catalog['dirstyle'].Error | Should -Not -BeNullOrEmpty
@@ -956,14 +956,14 @@ Describe '软件名录' {
}
It '软件名会被规范化成合法文件名' {
$clean = Format-CatalogName -Name 'a<b>c:d/e'
$clean = Format-BakNRetCatalogName -Name 'a<b>c:d/e'
($clean.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars())) | Should -Be -1
(Format-CatalogName -Name ' My App ') | Should -Be 'My App'
(Format-BakNRetCatalogName -Name ' My App ') | Should -Be 'My App'
}
}
# ============================================================================
Describe 'Resolve-BackupEntry:条目解析' {
Describe 'Resolve-BakNRetBackupEntry:条目解析' {
# ============================================================================
BeforeAll {
@@ -976,7 +976,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It 'resolve 与归档项的字段集合就是新契约(旧字段已删除)' {
$entry = ConvertFrom-BackupListLine -Line 'single'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
(@($resolved.PSObject.Properties.Name) -join ',') | Should -Be 'IsName,CatalogEntry,BaseName,ArchiveFlavor,Direction,Items,Encrypt,ExcludePatterns,HasExcludeOverride,Includes,HasIncludeOverride,Source,Error,Blocking'
@($resolved.Items)[0].PSObject.Properties.Name | Should -Contain 'ArchivePath'
@($resolved.Items)[0].PSObject.Properties.Name | Should -Contain 'Origin'
@@ -985,7 +985,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '软件名条目:isName/CatalogEntry/BaseName/ArchiveFlavor/Source' {
$entry = ConvertFrom-BackupListLine -Line 'single'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.IsName | Should -BeTrue
$resolved.BaseName | Should -Be 'single'
$resolved.ArchiveFlavor | Should -Be 'name'
@@ -997,7 +997,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '软件名条目:Items 来自 Slot(ArchivePath=Slot 名,Kind=slot,Origin=catalog)' {
$entry = ConvertFrom-BackupListLine -Line 'multi'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
@($resolved.Items).Count | Should -Be 2
(@($resolved.Items | ForEach-Object { $_.ArchivePath }) -join ',') | Should -Be 'Alpha,Zeta'
(@($resolved.Items | ForEach-Object { $_.Kind }) -join ',') | Should -Be 'slot,slot'
@@ -1009,7 +1009,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '字面路径条目:一个 path 项(ArchivePath 是末级名)' {
$entry = ConvertFrom-BackupListLine -Line 'C:\Programs\FooClolor'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.IsName | Should -BeFalse
$resolved.ArchiveFlavor | Should -Be 'path'
@($resolved.Items).Count | Should -Be 1
@@ -1021,7 +1021,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '字面路径条目上的 @ Path= 覆盖目标路径,但归档名仍按原路径' {
$entry = ConvertFrom-BackupListLine -Line "C:\Programs\Foo @ Path='D:\Elsewhere'"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.Items[0].RealPath | Should -Be 'D:\Elsewhere'
$resolved.BaseName | Should -Be 'Foo_from_C_+Programs'
}
@@ -1029,7 +1029,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '名录里的路径不存在时仍给出 Items(恢复要靠它还原回原位)' {
$missingCatalog = Write-ListFile -Path (Join-Path $script:Sandbox 'catalog\Missing.psd1') -Content "@{ 'gone' = @{ S = @{ Path = 'C:\definitely-not-here-98765' } } }"
$entry = ConvertFrom-BackupListLine -Line 'gone'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $missingCatalog -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $missingCatalog -MaxDepth 3
@($resolved.Items).Count | Should -Be 1
$resolved.Items[0].Exists | Should -BeFalse
$resolved.Items[0].RealPath | Should -Be 'C:\definitely-not-here-98765'
@@ -1037,7 +1037,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It ':: 覆盖 Path:单 Slot 条目直接生效' {
$entry = ConvertFrom-BackupListLine -Line "single :: $script:CatOther"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
@($resolved.Items).Count | Should -Be 1
$resolved.Items[0].ArchivePath | Should -Be 'Main'
$resolved.Items[0].RealPath | Should -Be $script:CatOther
@@ -1047,7 +1047,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It ':: / @ Path= 覆盖遇到多 Slot 条目 -> Blocking(不猜是哪一个)' {
foreach ($line in @("multi :: $script:CatOther", "multi @ Path='$script:CatOther'")) {
$entry = ConvertFrom-BackupListLine -Line $line
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
@($resolved.Items).Count | Should -Be 0
$resolved.Blocking | Should -Match '不能用一个'
}
@@ -1055,14 +1055,14 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '条目级 :- 覆盖名录里的排除:HasExcludeOverride 为真' {
$entry = ConvertFrom-BackupListLine -Line 'single :- logs\,!*Cache'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.HasExcludeOverride | Should -BeTrue
(@($resolved.ExcludePatterns) -join '|') | Should -Be 'logs\|!*Cache'
}
It '名录 Slot 的 Include 会追加成 include 项' {
$entry = ConvertFrom-BackupListLine -Line 'incapp'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
@($resolved.Items).Count | Should -Be 2
$include = @($resolved.Items | Where-Object { $_.Origin -eq 'include' })
$include.Count | Should -Be 1
@@ -1074,7 +1074,7 @@ Describe 'Resolve-BackupEntry:条目解析' {
It '条目级 :+ / @ Include= 覆盖名录里的 Include' {
foreach ($line in @("incapp :+ Other:$script:CatOther", "incapp @ Include='Other:$script:CatOther'")) {
$entry = ConvertFrom-BackupListLine -Line $line
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.HasIncludeOverride | Should -BeTrue
(@($resolved.Items | ForEach-Object { $_.ArchivePath }) -join ',') | Should -Be 'A,Other'
@($resolved.Items)[1].RealPath | Should -Be $script:CatOther
@@ -1082,34 +1082,34 @@ Describe 'Resolve-BackupEntry:条目解析' {
}
It '加密:名录里各 Slot 取或;条目级 :encrypt / :!encrypt 覆盖' {
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'single') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeFalse
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'encapp') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeTrue
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'encapp :!encrypt') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeFalse
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'single :encrypt') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeTrue
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line "single @ Encrypt='true'") -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeTrue
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'single') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeFalse
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'encapp') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeTrue
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'encapp :!encrypt') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeFalse
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'single :encrypt') -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeTrue
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line "single @ Encrypt='true'") -CatalogPath $script:CatFile -MaxDepth 3).Encrypt | Should -BeTrue
}
It '方向标记会传递到 Resolve-BackupEntry.Direction' {
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line '+ single') -CatalogPath $script:CatFile -MaxDepth 3).Direction | Should -Be 'backup'
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line '- single') -CatalogPath $script:CatFile -MaxDepth 3).Direction | Should -Be 'restore'
(Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'single') -CatalogPath $script:CatFile -MaxDepth 3).Direction | Should -Be 'both'
It '方向标记会传递到 Resolve-BakNRetBackupEntry.Direction' {
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line '+ single') -CatalogPath $script:CatFile -MaxDepth 3).Direction | Should -Be 'backup'
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line '- single') -CatalogPath $script:CatFile -MaxDepth 3).Direction | Should -Be 'restore'
(Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'single') -CatalogPath $script:CatFile -MaxDepth 3).Direction | Should -Be 'both'
}
It '归档内路径冲突(include 与 Slot 同名)-> Blocking' {
$entry = ConvertFrom-BackupListLine -Line "conflict :+ Data:$script:CatOther"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.Blocking | Should -Match '归档内路径冲突'
}
It '归档内路径冲突(父子关系)-> Blocking' {
$entry = ConvertFrom-BackupListLine -Line "conflict :+ Data\sub:$script:CatOther"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.Blocking | Should -Match '父子关系'
}
It '名录里没有该名字:Error 给出,Items 为空' {
$entry = ConvertFrom-BackupListLine -Line 'no-such-thing'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $script:CatFile -MaxDepth 3
$resolved.Error | Should -Match 'no-such-thing'
@($resolved.Items).Count | Should -Be 0
}
@@ -1125,13 +1125,13 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
$sentinel = 'SENTINEL-PW-9f3a'
$logPath = $null
try {
$logPath = Start-BaknretLog -Directory $sandbox -Prefix 'secret'
Set-BaknretDebug
$logPath = Start-BakNRetLog -Directory $sandbox -Prefix 'secret'
Set-BakNRetDebug
$null = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', 'exit 0', "-p$sentinel")
}
finally {
Set-BaknretDebug -Enabled:$false
Stop-BaknretLog
Set-BakNRetDebug -Enabled:$false
Stop-BakNRetLog
}
$logText = Get-Content -Encoding UTF8 -LiteralPath $logPath -Raw
@@ -1150,21 +1150,21 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
$first = $null
$second = $null
try {
$first = Enter-BaknretRunLock -Directory $dir
$first = Enter-BakNRetRunLock -Directory $dir
$first | Should -Not -BeNullOrEmpty
$second = Enter-BaknretRunLock -Directory $dir
$second = Enter-BakNRetRunLock -Directory $dir
$second | Should -BeNullOrEmpty
Test-Path -LiteralPath (Get-BaknretRunLockPath -Directory $dir) | Should -BeTrue
Test-Path -LiteralPath (Get-BakNRetRunLockPath -Directory $dir) | Should -BeTrue
}
finally {
Exit-BaknretRunLock -Lock $second
Exit-BaknretRunLock -Lock $first
Exit-BakNRetRunLock -Lock $second
Exit-BakNRetRunLock -Lock $first
}
# 锁文件是独占打开的(FileShare.None),内容只能在释放之后读
(Get-Content -Encoding UTF8 -LiteralPath (Get-BaknretRunLockPath -Directory $dir) -Raw) |
(Get-Content -Encoding UTF8 -LiteralPath (Get-BakNRetRunLockPath -Directory $dir) -Raw) |
Should -Match "pid=$PID"
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
@@ -1173,13 +1173,13 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
It '运行锁:释放之后可以重新取得' {
$dir = Join-Path $env:TEMP ("bnr-lock-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
try {
$first = Enter-BaknretRunLock -Directory $dir
$first = Enter-BakNRetRunLock -Directory $dir
$first | Should -Not -BeNullOrEmpty
Exit-BaknretRunLock -Lock $first
Exit-BakNRetRunLock -Lock $first
$second = Enter-BaknretRunLock -Directory $dir
$second = Enter-BakNRetRunLock -Directory $dir
$second | Should -Not -BeNullOrEmpty
Exit-BaknretRunLock -Lock $second
Exit-BakNRetRunLock -Lock $second
}
finally {
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
@@ -1192,7 +1192,7 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
$empty = Join-Path $env:TEMP ("bnr-empty-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Path $empty -Force | Out-Null
try {
$summary = Get-FolderSummary -FolderPath $empty
$summary = Get-BakNRetFolderSummary -FolderPath $empty
$summary.FileCount | Should -Be 0
$summary.TotalSize | Should -Not -BeNullOrEmpty
$summary.TotalSize | Should -Be 0
@@ -1207,18 +1207,18 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
New-Item -ItemType Directory -Path $base -Force | Out-Null
$target = Join-Path $base 'text.json'
try {
Write-BaknretAtomicText -Path $target -Text 'FIRST' | Out-Null
Write-BakNRetAtomicText -Path $target -Text 'FIRST' | Out-Null
(Get-Content -Encoding UTF8 -LiteralPath $target -Raw) | Should -Be 'FIRST'
# 目标已存在时走 File.Replace
Write-BaknretAtomicText -Path $target -Text 'SECOND' | Out-Null
Write-BakNRetAtomicText -Path $target -Text 'SECOND' | Out-Null
(Get-Content -Encoding UTF8 -LiteralPath $target -Raw) | Should -Be 'SECOND'
"$target.tmp" | Should -Not -Exist
# 目标只读 -> 替换必然失败 -> 旧内容必须还在。这正是 File.Replace 与"先删后移"
# 的区别:后者失败时旧文件已经没了,而 manifest 丢了只能靠文件名反推。
(Get-Item -LiteralPath $target).IsReadOnly = $true
{ Write-BaknretAtomicText -Path $target -Text 'SHOULD-NOT-LAND' } | Should -Throw
{ Write-BakNRetAtomicText -Path $target -Text 'SHOULD-NOT-LAND' } | Should -Throw
(Get-Content -Encoding UTF8 -LiteralPath $target -Raw) | Should -Be 'SECOND'
}
finally {
@@ -1260,9 +1260,9 @@ Describe '集成:真实 7z 压缩与排除规则' -Skip:(-not $script:HasSeven
It '排除规则与空格处理在真实归档上生效' {
$patterns = @('!*Cache', 'component_crx_cache', 'Default\Code Cache', 'Default\Extensions', 'Default\IndexedDB')
$item = New-BaknretArchiveItem -ArchivePath $script:IntegrationItem -RealPath $script:IntegrationPath `
$item = New-BakNRetArchiveItem -ArchivePath $script:IntegrationItem -RealPath $script:IntegrationPath `
-Kind 'path' -Origin 'path' -Exists $true -IsFile $false
$excludeArguments = @((Get-BaknretExcludeArgument -Item $item -Patterns $patterns).Arguments)
$excludeArguments = @((Get-BakNRetExcludeArgument -Item $item -Patterns $patterns).Arguments)
$archive = Join-Path $script:IntegrationRoot 'excl.7z'
$arguments = @('a', '-t7z', '-mx=1', '-bso0', '-bsp0') + $excludeArguments + @($archive, $script:IntegrationItem)
@@ -1316,14 +1316,14 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
$script:E2EHashes = New-VerifiableSourceTree -Root $script:E2ESource
Write-ListFile -Path $script:E2EList -Content "# e2e`n$script:E2ESource :- logs\,!*Cache`n" | Out-Null
$script:BackupRun = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$script:BackupRun = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:E2EList
BackupDir = $script:E2EBackupDir
Force = $true
QuietTool = $true
}
$script:E2EManifestPath = Join-Path $script:E2EBackupDir 'manifest.json'
$script:E2EManifest = Read-BaknretManifest -Path $script:E2EManifestPath
$script:E2EManifest = Read-BakNRetManifest -Path $script:E2EManifestPath
$script:E2EArchive = @(Get-ChildItem -LiteralPath $script:E2EBackupDir -File -Filter *.7z)[0]
}
@@ -1352,7 +1352,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
}
It '字面路径条目沿用 <末级名> 布局;归档里保留应保留内容、不含被排除项' {
(Get-BaknretArchiveTopName -ArchivePath 'My Code Space\keep.txt') | Should -Be 'My Code Space'
(Get-BakNRetArchiveTopName -ArchivePath 'My Code Space\keep.txt') | Should -Be 'My Code Space'
$verify = Join-Path $script:E2ERoot 'verify'
New-Item -ItemType Directory -Path $verify -Force | Out-Null
@@ -1370,7 +1370,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
$record.roots | Should -Contain 'My Code Space'
# 和归档里的真实内容对一次账,而不是只信 manifest 自己
$topLevel = @(Get-ArchiveTopLevelNames -ArchivePath $script:E2EArchive.FullName -SevenZip $script:SevenZip)
$topLevel = @(Get-BakNRetArchiveTopLevelNames -ArchivePath $script:E2EArchive.FullName -SevenZip $script:SevenZip)
(@($topLevel | Sort-Object) -join ',') | Should -Be 'My Code Space'
}
@@ -1378,7 +1378,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
$before = (Get-FileHash -LiteralPath $script:E2EManifestPath -Algorithm SHA256).Hash
Remove-Item -LiteralPath $script:E2ESource -Recurse -Force
$run = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:E2EList
BackupDir = $script:E2EBackupDir
DryRun = $true
@@ -1390,7 +1390,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
}
It 'Restore -WhatIf 同样不写盘' {
$run = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:E2EList
BackupDir = $script:E2EBackupDir
WhatIf = $true
@@ -1402,7 +1402,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
It '真实恢复:退出码 0,文件逐字节一致,被排除项没有被恢复出来' {
Test-Path -LiteralPath $script:E2ESource | Should -BeFalse # 保证不是空操作
$run = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:E2EList
BackupDir = $script:E2EBackupDir
Force = $true
@@ -1420,7 +1420,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
}
It '真实恢复之后 manifest 才被更新(lastRestoreAt)' {
$manifest = Read-BaknretManifest -Path $script:E2EManifestPath
$manifest = Read-BakNRetManifest -Path $script:E2EManifestPath
$record = $manifest.items[$script:E2EArchive.BaseName]
$record.lastRestoreAt | Should -Not -BeNullOrEmpty
}
@@ -1428,7 +1428,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
It '孤儿归档会被点名报告,但不影响退出码' {
Copy-Item -LiteralPath $script:E2EArchive.FullName -Destination (Join-Path $script:E2EBackupDir 'Orphaned-Archive.7z') -Force
$run = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:E2EList
BackupDir = $script:E2EBackupDir
QuietTool = $true
@@ -1440,7 +1440,7 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
}
It '带 -Only 时不做孤儿审计(避免把未选中的归档误报成孤儿)' {
$run = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:E2EList
BackupDir = $script:E2EBackupDir
Only = @('My Code Space')
@@ -1451,20 +1451,20 @@ Describe '集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)' -
It '源路径不存在时记为 missing-source,退出码仍为 0(跳过不算失败)' {
$missingList = Write-ListFile -Path (Join-Path $script:E2ERoot 'missing.txt') -Content "Z:\definitely-not-here-12345`n"
$run = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $missingList
BackupDir = $script:E2EBackupDir
QuietTool = $true
}
$run.ExitCode | Should -Be 0
$manifest = Read-BaknretManifest -Path $script:E2EManifestPath
$manifest = Read-BakNRetManifest -Path $script:E2EManifestPath
$manifest.items['definitely-not-here-12345_from_Z_'].action | Should -Be 'missing-source'
}
It '归档名重复时直接报失败(退出码 1),不静默互相覆盖' {
$duplicateList = Write-ListFile -Path (Join-Path $script:E2ERoot 'dup.txt') -Content "$script:E2ESource`n$script:E2ESource`n"
$run = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$run = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $duplicateList
BackupDir = $script:E2EBackupDir
Force = $true
@@ -1493,7 +1493,7 @@ Describe '集成:方向标记与孤儿审计' -Skip:(-not ($script:HasSevenZip
# 正确方向:先正常备份出归档
Write-ListFile -Path $script:DirectionList -Content "$script:DirectionSource`n" | Out-Null
$null = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$null = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:DirectionList
BackupDir = $script:DirectionBackupDir
Force = $true
@@ -1507,7 +1507,7 @@ Describe '集成:方向标记与孤儿审计' -Skip:(-not ($script:HasSevenZip
# 行首 -:仅恢复,备份端应当跳过它,但要把它算作"有主"
Write-ListFile -Path $script:DirectionList -Content "- $script:DirectionSource`n" | Out-Null
$script:BackupDirectionRun = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
$script:BackupDirectionRun = Invoke-BakNRetScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:DirectionList
BackupDir = $script:DirectionBackupDir
QuietTool = $true
@@ -1516,7 +1516,7 @@ Describe '集成:方向标记与孤儿审计' -Skip:(-not ($script:HasSevenZip
# 行首 +:仅备份,恢复端应当跳过它,同样要登记归档名
Remove-Item -LiteralPath $script:DirectionSource -Recurse -Force
Write-ListFile -Path $script:DirectionList -Content "+ $script:DirectionSource`n" | Out-Null
$script:RestoreDirectionRun = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
$script:RestoreDirectionRun = Invoke-BakNRetScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:DirectionList
BackupDir = $script:DirectionBackupDir
Force = $true
+4 -4
View File
@@ -82,8 +82,8 @@ if (-not (Test-Path -LiteralPath $restoreScript)) {
exit 1
}
$config = Get-BaknretConfig -Path $ConfigPath
$catalogPath = Resolve-CatalogPath -Configured $config.SoftwareCatalog -Root $projectRoot
$config = Get-BakNRetConfig -Path $ConfigPath
$catalogPath = Resolve-BakNRetCatalogPath -Configured $config.SoftwareCatalog -Root $projectRoot
if (-not $BackupDir) {
$BackupDir = $config.BackupDir
@@ -100,7 +100,7 @@ if (-not $WorkRoot) {
}
New-Item -ItemType Directory -Path $WorkRoot -Force | Out-Null
$manifest = Read-BaknretManifest -Path (Join-Path $BackupDir 'manifest.json')
$manifest = Read-BakNRetManifest -Path (Join-Path $BackupDir 'manifest.json')
$archiveFiles = @(Get-ChildItem -LiteralPath $BackupDir -File -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Extension.ToLower() -in @('.7z', '.rar', '.zip', '.tar') })
@@ -322,7 +322,7 @@ foreach ($name in $Entries) {
$entry = ConvertFrom-BackupListLine -Line $name
if (-not $entry) { continue }
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $catalogPath -MaxDepth $config.CatalogMaxDepth
$resolved = Resolve-BakNRetBackupEntry -Entry $entry -CatalogPath $catalogPath -MaxDepth $config.CatalogMaxDepth
if (-not $resolved.BaseName) {
$failures += "$name :解析不出归档名"
+10 -10
View File
@@ -167,7 +167,7 @@ $manifestPath = Join-Path $backupDir 'manifest.json'
Test-Case 'manifest 记录了条目、动作、校验结果、roots 与 layouts' {
Assert-FileExists $manifestPath
$manifest = Read-BaknretManifest -Path $manifestPath
$manifest = Read-BakNRetManifest -Path $manifestPath
Assert-Equal 1 $manifest.items.Count
$record = $manifest.items[$archives[0].BaseName]
@@ -251,7 +251,7 @@ $cleanExitCode = $LASTEXITCODE
$cleanArchive = Get-ChildItem -LiteralPath $lockBackupDir -File -Filter *.7z | Select-Object -First 1
$cleanSize = $cleanArchive.Length
$lockManifestPath = Join-Path $lockBackupDir 'manifest.json'
$cleanRecord = (Read-BaknretManifest -Path $lockManifestPath).items[$cleanArchive.BaseName]
$cleanRecord = (Read-BakNRetManifest -Path $lockManifestPath).items[$cleanArchive.BaseName]
Test-Case '干净归档:退出码 0,manifest 记 warnings=false' {
Assert-Equal 0 $cleanExitCode
@@ -269,7 +269,7 @@ try {
& $backupScript -BackupListPath $lockList -BackupDir $lockBackupDir -ConfigPath $cfg2 -Force -QuietTool
$warnExitCode = $LASTEXITCODE
$afterArchive = Get-ChildItem -LiteralPath $lockBackupDir -File -Filter *.7z | Select-Object -First 1
$afterRecord = (Read-BaknretManifest -Path $lockManifestPath).items[$afterArchive.BaseName]
$afterRecord = (Read-BakNRetManifest -Path $lockManifestPath).items[$afterArchive.BaseName]
Test-Case '有警告时拒绝覆盖完整归档:退出码 1,旧归档原样保留' {
Assert-Equal 1 $warnExitCode
@@ -282,7 +282,7 @@ try {
# 明确接受之后才允许覆盖
& $backupScript -BackupListPath $lockList -BackupDir $lockBackupDir -ConfigPath $cfg2 -Force -QuietTool -AcceptWarnings
$acceptExitCode = $LASTEXITCODE
$acceptedRecord = (Read-BaknretManifest -Path $lockManifestPath).items[$afterArchive.BaseName]
$acceptedRecord = (Read-BakNRetManifest -Path $lockManifestPath).items[$afterArchive.BaseName]
Test-Case '-AcceptWarnings 允许覆盖,并在 manifest 记 warnings=true' {
Assert-Equal 0 $acceptExitCode
@@ -357,7 +357,7 @@ Test-Case '名录条目:一个软件一个归档,归档名 = 软件名;独
}
Test-Case 'manifest.roots 列出各归档项的顶层名,layouts 标出 dir / file' {
$record = (Read-BaknretManifest -Path (Join-Path $catBackupDir 'manifest.json')).items['my-app']
$record = (Read-BakNRetManifest -Path (Join-Path $catBackupDir 'manifest.json')).items['my-app']
Assert-True ($null -ne $record)
$roots = @($record.roots | Sort-Object)
Assert-Equal 3 $roots.Count
@@ -438,7 +438,7 @@ Test-Case '@pathname 覆盖:用名录里的真实路径跑命名算法(独
[System.IO.File]::WriteAllText($pathList, "my-app @pathname :+ Modules:$settingsFile`n", [System.Text.UTF8Encoding]::new($true))
# settings.json 刚才被删了,重建一份,让 Include 的宿主机路径存在
Set-Content -LiteralPath $settingsFile -Value '{"slot":"file"}' -Encoding UTF8
$expectedBase = Get-BackupBaseName -RawPath $dirA
$expectedBase = Get-BakNRetBackupBaseName -RawPath $dirA
& $backupScript -BackupListPath $pathList -BackupDir $pathNameDir -ConfigPath $catConfig -Force -QuietTool
Assert-Equal 0 $LASTEXITCODE
Assert-FileExists (Join-Path $pathNameDir ($expectedBase + '.7z'))
@@ -578,7 +578,7 @@ if ($sevenZip) {
[System.IO.File]::WriteAllText($legacyList, "legacy-file`n", [System.Text.UTF8Encoding]::new($true))
New-E2EConfig -Path $legacyConfig -LogDir $legacyLogDir -SoftwareCatalog $legacyCatalog
$legacyManifest = Read-BaknretManifest -Path (Join-Path $legacyBackupDir 'manifest.json')
$legacyManifest = Read-BakNRetManifest -Path (Join-Path $legacyBackupDir 'manifest.json')
$legacyManifest.items['legacy-file'] = [ordered]@{
baseName = 'legacy-file'
source = 'legacy-file'
@@ -586,7 +586,7 @@ $legacyManifest.items['legacy-file'] = [ordered]@{
action = 'backed-up'
encrypted = $false
}
Write-BaknretManifest -Path (Join-Path $legacyBackupDir 'manifest.json') -Manifest $legacyManifest | Out-Null
Write-BakNRetManifest -Path (Join-Path $legacyBackupDir 'manifest.json') -Manifest $legacyManifest | Out-Null
# 让"恢复确实做了事"可验证:把活文件改成别的内容,恢复后应回到归档里的内容
Set-Content -LiteralPath $legacyLive -Value '{"version":"changed-after-backup"}' -Encoding UTF8
@@ -646,7 +646,7 @@ Test-Case '字面路径不受名录影响,仍走路径命名' {
[System.IO.File]::WriteAllText($literalList, "$dirA2`n", [System.Text.UTF8Encoding]::new($true))
& $backupScript -BackupListPath $literalList -BackupDir $literalDir -ConfigPath $catConfig -Force -QuietTool
Assert-Equal 0 $LASTEXITCODE
Assert-FileExists (Join-Path $literalDir ((Get-BackupBaseName -RawPath $dirA2) + '.7z'))
Assert-FileExists (Join-Path $literalDir ((Get-BakNRetBackupBaseName -RawPath $dirA2) + '.7z'))
}
$missingList = Join-Path $WorkRoot 'missing.txt'
@@ -656,7 +656,7 @@ $missingExitCode = $LASTEXITCODE
Test-Case '源路径不存在被记为 missing-source,退出码仍为 0(跳过不算失败)' {
Assert-Equal 0 $missingExitCode
$after = Read-BaknretManifest -Path $manifestPath
$after = Read-BakNRetManifest -Path $manifestPath
$record = $after.items['definitely-not-here-12345_from_Z_']
Assert-True ($null -ne $record) 'manifest 里应有该条目的记录'
Assert-Equal 'missing-source' $record.action
+6 -6
View File
@@ -58,13 +58,13 @@ if (-not (Test-Path -LiteralPath $manifestPath)) {
exit 0
}
$config = Get-BaknretConfig -Path $configPath
$catalogPath = Resolve-CatalogPath -Configured $config.SoftwareCatalog -Root $projectRoot
$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 {
function Invoke-BakNRetCaptured {
param([string]$Script, [string[]]$Arguments)
$hostExe = if ($PSVersionTable.PSEdition -eq 'Core') { 'pwsh' } else { 'powershell' }
@@ -130,7 +130,7 @@ Test-Case '真实清单里每个条目都能解析出方向,且软件名都能
if ($item.Direction -eq 'backup') { $backupOnly++ }
if ($item.Direction -eq 'restore') { $restoreOnly++ }
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $catalogPath -MaxDepth $config.CatalogMaxDepth
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $catalogPath -MaxDepth $config.CatalogMaxDepth
if ($resolved.Error -and $resolved.Error -like '*名录里没有*') { $catalogMiss += $resolved.Error }
}
@@ -144,7 +144,7 @@ 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
$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 仍然把某些条目当成查不到的软件名"
@@ -153,7 +153,7 @@ Test-Case 'Backup.ps1 -DryRun 退出 0、不再有"名录里没有"告警、mani
Test-Case 'Restore.ps1 -VerifyOnly 退出 0、把行首 + 识别成"仅备份"、manifest 不变' {
$before = Get-ManifestHash
$run = Invoke-BaknretCaptured -Script $restoreScript -Arguments @(
$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))
+166 -166
View File
@@ -13,7 +13,7 @@
* 软件名录新结构(`软件名 -> Slot -> @{ Path = ... }`):Slot 排序、前缀补全、
多候选报错、`%VAR%` / `$( ... )` 展开、缓存与 -NoCache
* 归档命名与逆向解析(软件名 / 字面路径 / @pathname)
* 归档项与归档内布局(New-BaknretArchiveItem / 暂存 junction / 硬链接)
* 归档项与归档内布局(New-BakNRetArchiveItem / 暂存 junction / 硬链接)
* 7z 排除参数翻译(!前缀、!re: 正则展开、Slot 前缀分配、去重)
* Windows 命令行拼接
* manifest 读写往返与 layouts
@@ -271,18 +271,18 @@ 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')
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 '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 '记号里有空格时目标可以带空格(第一个修饰符之前全是目标)' {
@@ -308,8 +308,8 @@ 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''')
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]
@@ -318,19 +318,19 @@ Test-Case 'Split-BaknretToken:引号内的空白不切分,引号保留' {
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 '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 '')
Test-Case 'Test-BakNRetLiteralPath:带分隔符或 % 的按路径,其余按软件名' {
Assert-True (Test-BakNRetLiteralPath -Path 'C:\a')
Assert-True (Test-BakNRetLiteralPath -Path 'C:/a')
Assert-True (Test-BakNRetLiteralPath -Path '%UserProfile%\.ssh')
Assert-False (Test-BakNRetLiteralPath -Path 'MyApp')
Assert-True (Test-BakNRetLiteralPath -Path '')
}
# ============================================================================
@@ -338,19 +338,19 @@ 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')
Assert-Equal 'legendary_from_%UserProfile%+.config' (Get-BakNRetBackupBaseName -RawPath '%UserProfile%/.config/legendary')
Assert-Equal 'FooClolor_from_C_+Programs' (Get-BakNRetBackupBaseName -RawPath 'C:\Programs\FooClolor')
Assert-Equal 'settings.json_from_C_+Programs' (Get-BakNRetBackupBaseName -RawPath 'C:\Programs\settings.json')
}
Test-Case '盘符冒号被归一化成下划线' {
Assert-Equal 'a_from_C_' (Get-BackupBaseName -RawPath 'C:\a')
Assert-Equal 'a_from_C_' (Get-BakNRetBackupBaseName -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'
$a = Get-BakNRetBackupBaseName -RawPath '%UserProfile%/.config/scoop'
$b = Get-BakNRetBackupBaseName -RawPath '%UserProfile%\.config\scoop'
$c = Get-BakNRetBackupBaseName -RawPath '%UserProfile%\\Documents\\WindowsPowerShell'
Assert-Equal $a $b
Assert-Equal 'WindowsPowerShell_from_%UserProfile%+Documents' $c
}
@@ -362,26 +362,26 @@ Test-Case '命名 <-> 路径往返' {
'%LocalAppData%\Microsoft\Edge\User Data'
)
foreach ($case in $cases) {
$base = Get-BackupBaseName -RawPath $case
$back = Convert-BackupFileNameToPath -FileName "$base.7z"
$base = Get-BakNRetBackupBaseName -RawPath $case
$back = Convert-BakNRetBackupFileNameToPath -FileName "$base.7z"
Assert-Equal ($case -replace '[/\\]+', '\') $back "往返 $case"
}
}
Test-Case '归档名里不含非法文件名字符(保留 & % +)' {
$base = Get-BackupBaseName -RawPath 'C:\a\b?c*d|e'
$base = Get-BakNRetBackupBaseName -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')
Assert-Equal 'keep%and&and+plus' (Format-BakNRetCatalogName -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 ' ')
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 ' ')
}
# ============================================================================
@@ -390,8 +390,8 @@ 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'
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
@@ -400,13 +400,13 @@ Test-Case 'New-BaknretArchiveItem:归档内路径被清理,TopName 是第一
Assert-False $item.IsFile
Assert-Equal 0 $item.Exclude.Count
$nested = New-BaknretArchiveItem -ArchivePath 'Alpha\sub' -RealPath 'C:\y' -Kind 'include' -Origin 'include'
$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'
$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"
}
@@ -428,13 +428,13 @@ Test-Case '暂存半途失败会自己清干净(不留指向真实数据的 ju
$items = @(
# 第一项会成功挂上,而且是**目录走 junction** —— 这正是危险的那个物件:它指向
# 真实数据,而临时目录迟早会被某次 Remove-Item -Recurse 扫到。
(New-BaknretArchiveItem -ArchivePath 'Good' -RealPath $realDir -Kind 'slot' -Slot 'Good' -Exists $true -IsFile $false)
(New-BakNRetArchiveItem -ArchivePath 'Good' -RealPath $realDir -Kind 'slot' -Slot 'Good' -Exists $true -IsFile $false)
# 第二项必然失败:源文件不存在,硬链接与复制都会失败
(New-BaknretArchiveItem -ArchivePath 'Missing.txt' -RealPath (Join-Path $base 'does-not-exist.txt') -Kind 'slot' -Slot 'Missing' -Exists $false -IsFile $true)
(New-BakNRetArchiveItem -ArchivePath 'Missing.txt' -RealPath (Join-Path $base 'does-not-exist.txt') -Kind 'slot' -Slot 'Missing' -Exists $false -IsFile $true)
)
$threw = $false
try { New-BaknretArchiveStaging -Items $items -Root (Join-Path $sandbox 'stage') | Out-Null } catch { $threw = $true }
try { New-BakNRetArchiveStaging -Items $items -Root (Join-Path $sandbox 'stage') | Out-Null } catch { $threw = $true }
Assert-True $threw '第二项应当抛错(源文件不存在,硬链接与复制都会失败)'
$left = @(Get-ChildItem -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue)
@@ -445,18 +445,18 @@ Test-Case '暂存半途失败会自己清干净(不留指向真实数据的 ju
Remove-Item -LiteralPath $base -Recurse -Force -ErrorAction SilentlyContinue
}
Test-Case 'New-BaknretArchiveStaging:目录走 junction、文件走硬链接,按归档内名字挂载' {
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 -Encoding UTF8 -LiteralPath (Join-Path $root 'App\f.txt') -Value 'x'
Set-Content -Encoding UTF8 -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)
(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')
$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
@@ -465,44 +465,44 @@ Test-Case 'New-BaknretArchiveStaging:目录走 junction、文件走硬链接
Assert-True (Test-Path -LiteralPath (Join-Path $staging 'AlphaData\f.txt')) '应能穿过 junction 看到内容'
}
finally {
Remove-BaknretArchiveStaging -Root $staging
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:只删连接点,不动目标' {
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 -Encoding UTF8 -LiteralPath (Join-Path $target 'keep.txt') -Value 'keep'
New-BaknretJunction -Path $link -Target $target | Out-Null
New-BakNRetJunction -Path $link -Target $target | Out-Null
Assert-Equal 'Junction' (Get-Item -LiteralPath $link -Force).LinkType
Remove-BaknretJunction -Path $link
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
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
Remove-BakNRetJunction -Path $link
}
}
Test-Case 'New-BaknretJunction:目标已存在时明确抛错' {
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 }
try { New-BakNRetJunction -Path $link -Target $target | Out-Null } catch { $threw = $true }
Assert-True $threw '同名实体存在时必须抛错,不能静默替换'
}
@@ -515,35 +515,35 @@ Test-Case '旧的 Get-ArchiveExcludeArgument 已经不存在(契约破坏点
}
Test-Case '相对模式自动补上归档项名(锚定在归档根)' {
$item = New-BaknretArchiveItem -ArchivePath 'March7thAssistant' -RealPath 'C:\x'
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('logs\')
$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\')
$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')
$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')
$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'
$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
$exclude = Get-BakNRetExcludeArgument -Item $item -Patterns $patterns
Assert-Equal 4 $exclude.Arguments.Count
foreach ($argument in $exclude.Arguments) {
Assert-False ($argument.Contains('"')) "参数里出现了引号: $argument"
@@ -551,8 +551,8 @@ Test-Case '生成的参数里绝不出现引号(旧实现 -x!"路径" 让排
}
Test-Case '空模式被忽略' {
$item = New-BaknretArchiveItem -ArchivePath 'x' -RealPath 'C:\x'
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('', ' ', 'real\')
$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]
}
@@ -562,9 +562,9 @@ Test-Case '!re:<正则> 展开成源目录里命中的精确路径' {
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
$item = New-BakNRetArchiveItem -ArchivePath 'Data' -RealPath $root
$exclude = Get-BaknretExcludeArgument -Item $item -Patterns @('!re:^Cache\d+$')
$exclude = Get-BakNRetExcludeArgument -Item $item -Patterns @('!re:^Cache\d+$')
Assert-Null $exclude.Error
$sorted = @($exclude.Arguments | Sort-Object)
Assert-Equal 3 $sorted.Count
@@ -575,34 +575,34 @@ Test-Case '!re:<正则> 展开成源目录里命中的精确路径' {
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')
$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:')
$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
$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:`<项名>\` 前缀把模式分配给对应归档项' {
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')
(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')
$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] '没点名任何项的模式对每一项各展开一份'
@@ -612,8 +612,8 @@ Test-Case 'Split-BaknretPatternScope:`<项名>\` 前缀把模式分配给对
}
}
Test-Case 'Merge-BaknretExcludeArgument:去重且保序' {
$merged = Merge-BaknretExcludeArgument -ArgumentLists @(@('-x!a', '-xr!c'), @('-x!a', '-x!b'))
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]
@@ -625,23 +625,23 @@ Write-Host "`n== 命令行拼接 ==" -ForegroundColor Cyan
# ============================================================================
Test-Case '无空格参数原样输出' {
Assert-Equal 'a -t7z' (ConvertTo-NativeArgumentString -ArgumentList @('a', '-t7z'))
Assert-Equal 'a -t7z' (ConvertTo-BakNRetNativeArgumentString -ArgumentList @('a', '-t7z'))
}
Test-Case '含空格参数加引号' {
Assert-Equal '"C:\Program Files\x"' (ConvertTo-NativeArgumentString -ArgumentList @('C:\Program Files\x'))
Assert-Equal '"C:\Program Files\x"' (ConvertTo-BakNRetNativeArgumentString -ArgumentList @('C:\Program Files\x'))
}
Test-Case '引号内的结尾反斜杠翻倍(否则会被当成转义引号)' {
Assert-Equal '"C:\Program Files\\"' (ConvertTo-NativeArgumentString -ArgumentList @('C:\Program Files\'))
Assert-Equal '"C:\Program Files\\"' (ConvertTo-BakNRetNativeArgumentString -ArgumentList @('C:\Program Files\'))
}
Test-Case '内部引号被转义' {
Assert-Equal '"say \"hi\""' (ConvertTo-NativeArgumentString -ArgumentList @('say "hi"'))
Assert-Equal '"say \"hi\""' (ConvertTo-BakNRetNativeArgumentString -ArgumentList @('say "hi"'))
}
Test-Case '空参数输出一对空引号' {
Assert-Equal '""' (ConvertTo-NativeArgumentString -ArgumentList @(''))
Assert-Equal '""' (ConvertTo-BakNRetNativeArgumentString -ArgumentList @(''))
}
# ============================================================================
@@ -654,7 +654,7 @@ Test-Case 'manifest 读写往返(含 layouts)' {
New-Item -ItemType Directory -Path $sandbox -Force | Out-Null
$path = Join-Path $sandbox 'manifest.json'
$m = Read-BaknretManifest -Path $path
$m = Read-BakNRetManifest -Path $path
Assert-Equal 0 $m.items.Count
$m.compressor = [pscustomobject]@{ name = '7z'; version = '26.03' }
@@ -667,9 +667,9 @@ Test-Case 'manifest 读写往返(含 layouts)' {
[ordered]@{ name = 'BetaFile'; kind = 'file' }
)
}
Write-BaknretManifest -Path $path -Manifest $m | Out-Null
Write-BakNRetManifest -Path $path -Manifest $m | Out-Null
$again = Read-BaknretManifest -Path $path
$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
@@ -683,12 +683,12 @@ Test-Case 'manifest 读写往返(含 layouts)' {
Test-Case 'manifest 损坏时不抛异常' {
$path = Join-Path $sandbox 'broken.json'
[System.IO.File]::WriteAllText($path, '{ this is not json')
$m = Read-BaknretManifest -Path $path
$m = Read-BakNRetManifest -Path $path
Assert-Equal 0 $m.items.Count
}
Test-Case '配置缺失时返回默认值' {
$config = Get-BaknretConfig -Path (Join-Path $sandbox 'nope.psd1')
$config = Get-BakNRetConfig -Path (Join-Path $sandbox 'nope.psd1')
Assert-Equal 'Backups' $config.BackupDir
Assert-True ($config.MinFreeSpaceGB -gt 0)
}
@@ -696,7 +696,7 @@ Test-Case '配置缺失时返回默认值' {
Test-Case '配置嵌套段落合并且不丢默认键' {
$path = Join-Path $sandbox 'cfg.psd1'
Set-Content -Encoding UTF8 -LiteralPath $path -Value "@{ Encryption = @{ Enabled = `$true }; CompressionLevel = 5 }"
$config = Get-BaknretConfig -Path $path
$config = Get-BakNRetConfig -Path $path
Assert-Equal 5 $config.CompressionLevel
Assert-True $config.Encryption.Enabled
Assert-True $config.Encryption.EncryptHeaders '未覆盖的嵌套键应保留默认值'
@@ -706,9 +706,9 @@ Test-Case '口令:环境变量可读取,取不到返回 $null' {
$old = $env:BAKNRET_PASSWORD
try {
Remove-Item Env:BAKNRET_PASSWORD -ErrorAction SilentlyContinue
Assert-Null (Get-BaknretPassword -PasswordFile (Join-Path $sandbox 'nope'))
Assert-Null (Get-BakNRetPassword -PasswordFile (Join-Path $sandbox 'nope'))
$env:BAKNRET_PASSWORD = 'from-env'
Assert-Equal 'from-env' (Get-BaknretPassword)
Assert-Equal 'from-env' (Get-BakNRetPassword)
}
finally {
Remove-Item Env:BAKNRET_PASSWORD -ErrorAction SilentlyContinue
@@ -731,7 +731,7 @@ Test-Case '名录解析:裸键、引号键、带 - 与 . 的名字' {
$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($true))
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $catPath -NoCache
Assert-Equal 3 $catalog.Count
Assert-True $catalog.ContainsKey('FooClolor')
Assert-True $catalog.ContainsKey('scoop-config') '带连字符的键必须能解析'
@@ -743,7 +743,7 @@ 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($true))
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $catPath -NoCache
Assert-Equal 'Multi' $catalog['FooClolor'].Kind
Assert-Equal 2 $catalog['FooClolor'].Slots.Count
Assert-Equal 'Alpha' $catalog['FooClolor'].Slots[0].Name
@@ -756,7 +756,7 @@ Test-Case '名录:Slot 对象字段与契约一致' {
Set-Content -Encoding UTF8 -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($true))
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -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"
@@ -789,7 +789,7 @@ Test-Case '名录解析:目录带版本后缀时按前缀补全(legendary_2.
$catPath = Join-Path $catalogSandbox 'suffix.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n legendary = @{ DefaultConfig = @{ Path = '$root\legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -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'
@@ -801,20 +801,20 @@ Test-Case '名录解析:不会把 Legendary 误配成 LegendarySomething' {
$catPath = Join-Path $catalogSandbox 'strict.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n Legendary = @{ DefaultConfig = @{ Path = '$root\Legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -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:_ / - 边界之外不补全' {
Test-Case 'Find-BakNRetChildDirectoryByName:_ / - 边界之外不补全' {
$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')
$found = @(Find-BakNRetChildDirectoryByName -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)
@@ -827,7 +827,7 @@ Test-Case '名录:一个 Slot 补全出多个目录时明确报错(否则会
$catPath = Join-Path $catalogSandbox 'multicand.psd1'
[System.IO.File]::WriteAllText($catPath, "@{`n Legendary = @{ DefaultConfig = @{ Path = '$root\Legendary' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $catPath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $catPath -NoCache
$entry = $catalog['Legendary']
Assert-True ($null -ne $entry.Error)
Assert-True ($entry.Error -like '*拆成多个 Slot*') '错误里要给出可操作的指引'
@@ -836,7 +836,7 @@ Test-Case '名录:一个 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($true))
$catalog = Get-SoftwareCatalog -Path $bad -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $bad -NoCache
Assert-Equal 'Invalid' $catalog['A'].Kind
Assert-True ($catalog['A'].Error -like '*Slot S*')
Assert-Equal 'Invalid' $catalog['B'].Kind
@@ -846,12 +846,12 @@ Test-Case '名录:Slot 写法不对 / 缺少 Path 都记成 Error,而不是
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($true))
$catalog = Get-SoftwareCatalog -Path $legacy -NoCache
$catalog = Get-BakNRetSoftwareCatalog -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($true))
$catalog = Get-SoftwareCatalog -Path $dirs -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $dirs -NoCache
Assert-Equal 1 $catalog.Count
Assert-Equal 'Invalid' $catalog['C'].Kind
Assert-True ($catalog['C'].Error -like '*Dirs*')
@@ -869,7 +869,7 @@ Test-Case '名录:Includes 分文件维护会被合并' {
[System.IO.File]::WriteAllText($extraPath, "@{`n 'extra-app' = @{ D = @{ Path = '$appDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
[System.IO.File]::WriteAllText($basePath, "@{`n Includes = @('Extra.psd1')`n 'base-app' = @{ D = @{ Path = '$configDir' } }`n}`n", [System.Text.UTF8Encoding]::new($true))
$catalog = Get-SoftwareCatalog -Path $basePath -NoCache
$catalog = Get-BakNRetSoftwareCatalog -Path $basePath -NoCache
Assert-True $catalog.ContainsKey('base-app')
Assert-True $catalog.ContainsKey('extra-app') 'Includes 引入的条目也应在'
}
@@ -878,20 +878,20 @@ 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($true))
$first = Get-SoftwareCatalog -Path $catPath
$second = Get-SoftwareCatalog -Path $catPath
$first = Get-BakNRetSoftwareCatalog -Path $catPath
$second = Get-BakNRetSoftwareCatalog -Path $catPath
Assert-True ([object]::ReferenceEquals($first, $second)) '同一文件同一指纹应命中缓存'
$fresh = Get-SoftwareCatalog -Path $catPath -NoCache
$fresh = Get-BakNRetSoftwareCatalog -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($true))
$changed = Get-SoftwareCatalog -Path $catPath
$changed = Get-BakNRetSoftwareCatalog -Path $catPath
Assert-Equal $configDir $changed['Cached'].Slots[0].Resolved '文件变了缓存必须失效'
}
# ============================================================================
Write-Host "`n== Resolve-BackupEntry(归档项解析) ==" -ForegroundColor Cyan
Write-Host "`n== Resolve-BakNRetBackupEntry(归档项解析) ==" -ForegroundColor Cyan
# ============================================================================
$resolveSandbox = Join-Path $env:TEMP ("baknret-resolve-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
@@ -909,7 +909,7 @@ $resolveCatalog = Join-Path $resolveSandbox 'catalog.psd1'
Test-Case '软件名条目:一个 Slot -> 一个归档项,路径是 Slot 名,归档名是软件名' {
$item = ConvertFrom-BackupListLine -Line 'FooClolor'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-True $resolved.IsName
Assert-Equal 'name' $resolved.ArchiveFlavor
@@ -925,9 +925,9 @@ Test-Case '软件名条目:一个 Slot -> 一个归档项,路径是 Slot 名
Assert-True $resolved.Items[0].Exists
}
Test-Case 'Resolve-BackupEntry 的返回值里没有旧的 Sources / RootName 等属性' {
Test-Case 'Resolve-BakNRetBackupEntry 的返回值里没有旧的 Sources / RootName 等属性' {
$item = ConvertFrom-BackupListLine -Line 'FooClolor'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -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"
}
@@ -938,7 +938,7 @@ Test-Case 'Resolve-BackupEntry 的返回值里没有旧的 Sources / RootName
Test-Case '字面路径条目:仍用路径命名算法,归档内是历史布局 <末级名>' {
$item = ConvertFrom-BackupListLine -Line 'C:\Programs\FooClolor'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-False $resolved.IsName
Assert-Equal 'path' $resolved.ArchiveFlavor
Assert-Equal 'FooClolor_from_C_+Programs' $resolved.BaseName
@@ -949,34 +949,34 @@ Test-Case '字面路径条目:仍用路径命名算法,归档内是历史布
Test-Case '@pathname 用名录里的真实路径跑命名算法' {
$item = ConvertFrom-BackupListLine -Line 'FooClolor @pathname'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-Equal (Get-BackupBaseName -RawPath $dirA) $resolved.BaseName
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-Equal (Get-BakNRetBackupBaseName -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
$resolved = Resolve-BakNRetBackupEntry -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
$atResolved = Resolve-BakNRetBackupEntry -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
$resolved = Resolve-BakNRetBackupEntry -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
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-Equal 'no-such-thing' $resolved.BaseName
Assert-True ($null -ne $resolved.Error) '应给出错误说明'
Assert-Equal 0 $resolved.Items.Count
@@ -986,7 +986,7 @@ 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($true))
$item = ConvertFrom-BackupListLine -Line 'gone-app'
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $catPath
$resolved = Resolve-BakNRetBackupEntry -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
@@ -995,7 +995,7 @@ Test-Case '名录里的路径不存在时仍给出归档项(恢复要靠它还
Test-Case ':+ 追加项:<归档内相对路径>:<宿主机绝对路径>' {
$item = ConvertFrom-BackupListLine -Line "FooClolor :+ Modules:$dirExtra"
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -Entry $item -CatalogPath $resolveCatalog
Assert-Equal 2 $resolved.Items.Count
Assert-Equal 'DefaultData' $resolved.Items[0].ArchivePath
Assert-Equal 'Modules' $resolved.Items[1].ArchivePath
@@ -1011,11 +1011,11 @@ Test-Case '条目级 :+ 覆盖名录里的 Include(不是叠加)' {
$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($true))
$plain = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'IncApp') -CatalogPath $catPath
$plain = Resolve-BakNRetBackupEntry -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
$overridden = Resolve-BakNRetBackupEntry -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
@@ -1023,45 +1023,45 @@ Test-Case '条目级 :+ 覆盖名录里的 Include(不是叠加)' {
Test-Case '归档内路径冲突:同名追加项 -> Blocking' {
$item = ConvertFrom-BackupListLine -Line "FooClolor :+ DefaultData:$dirExtra"
$resolved = Resolve-BackupEntry -Entry $item -CatalogPath $resolveCatalog
$resolved = Resolve-BakNRetBackupEntry -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
$resolved = Resolve-BakNRetBackupEntry -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
$catalogEncrypt = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'FooClolor') -CatalogPath $resolveCatalog
Assert-True $catalogEncrypt.Encrypt
$off = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'FooClolor :!encrypt') -CatalogPath $resolveCatalog
$off = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'FooClolor :!encrypt') -CatalogPath $resolveCatalog
Assert-False $off.Encrypt
$on = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'MultiApp :encrypt') -CatalogPath $resolveCatalog
$on = Resolve-BakNRetBackupEntry -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 '方向标记随条目一路传到 Resolve-BakNRetBackupEntry' {
Assert-Equal 'backup' (Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line '+ FooClolor') -CatalogPath $resolveCatalog).Direction
Assert-Equal 'restore' (Resolve-BakNRetBackupEntry -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($true))
$plain = Resolve-BackupEntry -Entry (ConvertFrom-BackupListLine -Line 'ExcApp') -CatalogPath $catPath
$plain = Resolve-BakNRetBackupEntry -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
$overridden = Resolve-BakNRetBackupEntry -Entry (ConvertFrom-BackupListLine -Line 'ExcApp :- !*Listed') -CatalogPath $catPath
Assert-True $overridden.HasExcludeOverride
Assert-Equal '!*Listed' $overridden.ExcludePatterns[0]
}
@@ -1093,8 +1093,8 @@ else {
Set-Content -Encoding UTF8 -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)
$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'
@@ -1124,8 +1124,8 @@ else {
Set-Content -Encoding UTF8 -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)
$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'
@@ -1159,7 +1159,7 @@ else {
Assert-True (Test-Path -LiteralPath (Join-Path $verify "$itemName\Default\Cache\c.bin")) '对照组应当包含被排除的那个文件'
}
Test-Case 'Get-ArchiveTopLevelNames 能列出归档内的顶层条目名' {
Test-Case 'Get-BakNRetArchiveTopLevelNames 能列出归档内的顶层条目名' {
$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
@@ -1169,7 +1169,7 @@ else {
$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)
$names = @(Get-BakNRetArchiveTopLevelNames -ArchivePath $archive -SevenZip $sevenZip)
$sorted = @($names | Sort-Object)
Assert-Equal 2 $sorted.Count
Assert-Equal 'Alpha' $sorted[0]
@@ -1203,13 +1203,13 @@ Test-Case '口令不会进日志:DEBUG 下打印的命令行要遮蔽 -p 参
$sentinel = 'SENTINEL-PW-9f3a'
$logPath = $null
try {
$logPath = Start-BaknretLog -Directory $sandbox -Prefix 'secret'
Set-BaknretDebug
$logPath = Start-BakNRetLog -Directory $sandbox -Prefix 'secret'
Set-BakNRetDebug
$null = Invoke-ExternalCommand -FilePath 'cmd.exe' -ArgumentList @('/c', 'exit 0', "-p$sentinel")
}
finally {
Set-BaknretDebug -Enabled:$false
Stop-BaknretLog
Set-BakNRetDebug -Enabled:$false
Stop-BakNRetLog
}
$logText = Get-Content -Encoding UTF8 -LiteralPath $logPath -Raw
@@ -1236,7 +1236,7 @@ Test-Case '空目录的摘要给 0 而不是 $null(否则空间守卫会静默
$empty = Join-Path $env:TEMP ("bnr-empty-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Path $empty -Force | Out-Null
try {
$summary = Get-FolderSummary -FolderPath $empty
$summary = Get-BakNRetFolderSummary -FolderPath $empty
Assert-Equal 0 $summary.FileCount
Assert-True ($null -ne $summary.TotalSize) 'TotalSize 不能是 $null'
Assert-True ($summary.TotalSize -is [long]) 'TotalSize 应当是整数'
@@ -1252,11 +1252,11 @@ Test-Case '原子替换:成功时新内容到位且不留 .tmp;失败时旧
New-Item -ItemType Directory -Path $base -Force | Out-Null
$target = Join-Path $base 'text.json'
try {
Write-BaknretAtomicText -Path $target -Text 'FIRST' | Out-Null
Write-BakNRetAtomicText -Path $target -Text 'FIRST' | Out-Null
Assert-Equal 'FIRST' (Get-Content -Encoding UTF8 -LiteralPath $target -Raw)
# 目标已存在时走 File.Replace
Write-BaknretAtomicText -Path $target -Text 'SECOND' | Out-Null
Write-BakNRetAtomicText -Path $target -Text 'SECOND' | Out-Null
Assert-Equal 'SECOND' (Get-Content -Encoding UTF8 -LiteralPath $target -Raw)
Assert-FileMissing "$target.tmp"
@@ -1264,7 +1264,7 @@ Test-Case '原子替换:成功时新内容到位且不留 .tmp;失败时旧
# 后者失败时旧文件已经没了,而 manifest 丢了就只能靠文件名反推。
(Get-Item -LiteralPath $target).IsReadOnly = $true
$threw = $false
try { Write-BaknretAtomicText -Path $target -Text 'SHOULD-NOT-LAND' | Out-Null } catch { $threw = $true }
try { Write-BakNRetAtomicText -Path $target -Text 'SHOULD-NOT-LAND' | Out-Null } catch { $threw = $true }
Assert-True $threw '目标只读时替换应当抛错'
Assert-Equal 'SECOND' (Get-Content -Encoding UTF8 -LiteralPath $target -Raw)
}
@@ -1286,22 +1286,22 @@ Test-Case '运行锁:同一份备份目录同时只能有一个持有者' {
$first = $null
$second = $null
try {
$first = Enter-BaknretRunLock -Directory $dir
$first = Enter-BakNRetRunLock -Directory $dir
Assert-True ($null -ne $first) '第一次应当拿到锁'
$second = Enter-BaknretRunLock -Directory $dir
$second = Enter-BakNRetRunLock -Directory $dir
Assert-True ($null -eq $second) '同一目录的第二次不应当拿到锁'
Assert-FileExists (Get-BaknretRunLockPath -Directory $dir)
Assert-FileExists (Get-BakNRetRunLockPath -Directory $dir)
}
finally {
Exit-BaknretRunLock -Lock $second
Exit-BaknretRunLock -Lock $first
Exit-BakNRetRunLock -Lock $second
Exit-BakNRetRunLock -Lock $first
}
# 锁文件是**独占**打开的(FileShare.None),内容只能在释放之后读 —— 而它本来就该
# 留在那里给排查的人看:"到底是谁占着"这个问题不该靠猜。
$info = Get-Content -Encoding UTF8 -LiteralPath (Get-BaknretRunLockPath -Directory $dir) -Raw
$info = Get-Content -Encoding UTF8 -LiteralPath (Get-BakNRetRunLockPath -Directory $dir) -Raw
Assert-True ($info -match "pid=$PID") "锁文件里应当写明持有进程,实际内容是:$info"
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
@@ -1310,13 +1310,13 @@ Test-Case '运行锁:同一份备份目录同时只能有一个持有者' {
Test-Case '运行锁:释放之后可以重新取得' {
$dir = Join-Path $env:TEMP ("bnr-lock-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
try {
$first = Enter-BaknretRunLock -Directory $dir
$first = Enter-BakNRetRunLock -Directory $dir
Assert-True ($null -ne $first) '第一次应当拿到锁'
Exit-BaknretRunLock -Lock $first
Exit-BakNRetRunLock -Lock $first
$second = Enter-BaknretRunLock -Directory $dir
$second = Enter-BakNRetRunLock -Directory $dir
Assert-True ($null -ne $second) '释放之后应当能重新拿到锁'
Exit-BaknretRunLock -Lock $second
Exit-BakNRetRunLock -Lock $second
}
finally {
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue