tests/Get-DFCachedCommandOutput.Tests.ps1
|
BeforeAll { . "$PSScriptRoot/TestSupport.ps1" foreach ($f in Get-DFTestModuleFile) { . $f } } Describe 'Get-DFCachedCommandOutput' { BeforeEach { Set-DFTestXdg Remove-Item $Env:XDG_CACHE_HOME -Recurse -Force -ErrorAction Ignore # A real file stands in for the resolved executable so LastWriteTimeUtc # is real data, not a mocked property on a fake path. $script:FakeExe = Join-Path $TestDrive 'fake-tool.exe' Set-Content -Path $script:FakeExe -Value 'binary-stand-in' -Encoding UTF8 Mock Get-Command { [PSCustomObject]@{ Source = $script:FakeExe } } } AfterEach { Restore-DFTestXdg } It 'calls -Generate and caches the result on a cold cache' { $script:calls = 0 $result = Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { $script:calls++ 'generated-output' } $result | Should -Be 'generated-output' $script:calls | Should -Be 1 Test-Path (Join-Path $Env:XDG_CACHE_HOME 'dotforge' 'test-tool.txt') | Should -BeTrue } It 'writes atomically, the content before the key that vouches for it' { # Two shells starting at once must never read a half-written init script, # and a crash between the writes must leave a key that does NOT match. $script:writes = [System.Collections.Generic.List[string]]::new() Mock Write-DFFileAtomic { $script:writes.Add((Split-Path $Path -Leaf)) } Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'generated-output' } | Out-Null $script:writes | Should -Be @('test-tool.txt', 'test-tool.key') } It 'reuses the cache on a second call -- does not re-invoke -Generate' { Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'first' } | Out-Null $result = Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { throw '-Generate should not run on a cache hit' } $result | Should -Be 'first' } It 'regenerates when the executable file changes (LastWriteTimeUtc differs)' { Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'stale' } | Out-Null Start-Sleep -Milliseconds 50 Set-Content -Path $script:FakeExe -Value 'a rebuilt binary' -Encoding UTF8 $result = Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'fresh' } $result | Should -Be 'fresh' } It 'regenerates when the resolved path changes even if the old file is untouched' { Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'from-old-path' } | Out-Null $newExe = Join-Path $TestDrive 'different-tool.exe' Set-Content -Path $newExe -Value 'binary-stand-in' -Encoding UTF8 Mock Get-Command { [PSCustomObject]@{ Source = $newExe } } $result = Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'from-new-path' } $result | Should -Be 'from-new-path' } It 'regenerates when -ExtraKey changes, even though the executable is untouched' { Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -ExtraKey 'specs:a' -Generate { 'old' } | Out-Null Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -ExtraKey 'specs:a' -Generate { throw '-Generate should not run while -ExtraKey is unchanged' } | Should -Be 'old' Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -ExtraKey 'specs:a,b' -Generate { 'new' } | Should -Be 'new' } It 'bypasses a valid cache when -Force is passed' { Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'first' } | Out-Null $result = Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Force -Generate { 'forced' } $result | Should -Be 'forced' } It 'never caches an empty result, and retries -Generate next call' { Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { '' } | Out-Null Test-Path (Join-Path $Env:XDG_CACHE_HOME 'dotforge' 'test-tool.txt') | Should -BeFalse $script:calls = 0 Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { $script:calls++; 'now-real' } | Out-Null $script:calls | Should -Be 1 } It 'falls back to always calling -Generate, uncached, when the resolved command has no backing file (e.g. a function stand-in)' { # Regression: tests/scoop.Tests.ps1 stubs scoop-search as a PowerShell # function, not a real binary. Get-Command on a function does not # populate .Source with a usable file path -- Get-Item on it must not # throw, it must fall back to uncached generation. Mock Get-Command { [PSCustomObject]@{ Source = '' } } $script:calls = 0 Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'stub-function' -Generate { $script:calls++; 'x' } | Out-Null Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'stub-function' -Generate { $script:calls++; 'x' } | Out-Null $script:calls | Should -Be 2 } It 'falls back to always calling -Generate, uncached, when the executable does not resolve' { Mock Get-Command { $null } $script:calls = 0 Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'missing-tool' -Generate { $script:calls++; 'x' } | Out-Null Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'missing-tool' -Generate { $script:calls++; 'x' } | Out-Null $script:calls | Should -Be 2 } Context 'scoop shims' { BeforeEach { # Scoop never rewrites shims\<name>.exe on upgrade; the sibling # <name>.shim names the real binary, whose file identity changes. $script:ShimTarget = Join-Path $TestDrive 'apps' 'real-tool.exe' New-Item -ItemType Directory -Path (Split-Path $script:ShimTarget) -Force | Out-Null Set-Content -Path $script:ShimTarget -Value 'real-binary-v1' -Encoding UTF8 Set-Content -Path ([IO.Path]::ChangeExtension($script:FakeExe, '.shim')) ` -Value "path = `"$script:ShimTarget`"" -Encoding UTF8 } AfterEach { Remove-Item ([IO.Path]::ChangeExtension($script:FakeExe, '.shim')) -ErrorAction Ignore } It 'regenerates when the shim target is upgraded, even though the shim itself is untouched' { Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'v1-init' } | Out-Null Start-Sleep -Milliseconds 50 Set-Content -Path $script:ShimTarget -Value 'real-binary-v2' -Encoding UTF8 Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'v2-init' } | Should -Be 'v2-init' } It 'fingerprints the shim target path, not the shim' { Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'x' } | Out-Null (Get-Content (Join-Path $Env:XDG_CACHE_HOME 'dotforge' 'test-tool.key') -Raw) | Should -BeLike "$script:ShimTarget|*" } It 'falls back to fingerprinting the shim when the .shim file is malformed' { Set-Content -Path ([IO.Path]::ChangeExtension($script:FakeExe, '.shim')) -Value 'garbage' -Encoding UTF8 { Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'x' } } | Should -Not -Throw (Get-Content (Join-Path $Env:XDG_CACHE_HOME 'dotforge' 'test-tool.key') -Raw) | Should -BeLike "$script:FakeExe|*" } It 'falls back to fingerprinting the shim when the shim target is missing' { Remove-Item $script:ShimTarget { Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'x' } } | Should -Not -Throw (Get-Content (Join-Path $Env:XDG_CACHE_HOME 'dotforge' 'test-tool.key') -Raw) | Should -BeLike "$script:FakeExe|*" } } It 'keeps separate cache entries for different -Name values' { Get-DFCachedCommandOutput -Name 'tool-a' -Executable 'test-tool' -Generate { 'output-a' } | Out-Null Get-DFCachedCommandOutput -Name 'tool-b' -Executable 'test-tool' -Generate { 'output-b' } | Out-Null Get-DFCachedCommandOutput -Name 'tool-a' -Executable 'test-tool' -Generate { throw 'should not run' } | Should -Be 'output-a' Get-DFCachedCommandOutput -Name 'tool-b' -Executable 'test-tool' -Generate { throw 'should not run' } | Should -Be 'output-b' } } |