test/qrun.tests.ps1
|
BeforeAll { Get-Module ConfigMap -ErrorAction SilentlyContinue | Remove-Module Import-Module $PSScriptRoot\..\configmap.psm1 -Force } Describe 'qrun process runner' { BeforeEach { $script:workDir = Join-Path $TestDrive ("qrun-" + [guid]::NewGuid().ToString('n')) New-Item -ItemType Directory -Path $script:workDir -Force | Out-Null Push-Location $script:workDir $script:map = [ordered]@{ setup = [ordered]@{ description = 'test process' 'step-a' = { New-Item -ItemType Directory -Path 'marker-a' -Force | Out-Null 'a' | Set-Content (Join-Path 'marker-a' 'done.txt') } 'step-b' = { if (!(Test-Path 'marker-a/done.txt')) { throw 'step-a missing' } 'b' | Set-Content 'marker-b.txt' } 'step-c' = { 'c' | Set-Content 'marker-c.txt' } } } } AfterEach { Pop-Location } It 'runs all steps in order and persists success state' { qrun -map $script:map setup Test-Path 'marker-a/done.txt' | Should -BeTrue Test-Path 'marker-b.txt' | Should -BeTrue Test-Path 'marker-c.txt' | Should -BeTrue $stateFiles = @(Get-ChildItem -Path (Join-Path $script:workDir '.configmap' 'process') -Recurse -Filter '*.state.json') $stateFiles.Count | Should -Be 1 $state = Get-Content $stateFiles[0].FullName -Raw | ConvertFrom-Json $state.steps.'step-a'.status | Should -Be 'Succeeded' $state.steps.'step-b'.status | Should -Be 'Succeeded' $state.steps.'step-c'.status | Should -Be 'Succeeded' } It 'skips already succeeded steps on re-run' { qrun -map $script:map setup Remove-Item 'marker-b.txt', 'marker-c.txt' -Force qrun -map $script:map setup Test-Path 'marker-b.txt' | Should -BeFalse Test-Path 'marker-c.txt' | Should -BeFalse Test-Path 'marker-a/done.txt' | Should -BeTrue } It 'reruns succeeded steps when -Force is specified' { qrun -map $script:map setup Remove-Item 'marker-b.txt' -Force qrun -map $script:map setup -Force Test-Path 'marker-b.txt' | Should -BeTrue Test-Path 'marker-c.txt' | Should -BeTrue } It 'runs a single step via process.step path and respects skip rules' { qrun -map $script:map setup.step-a Test-Path 'marker-a/done.txt' | Should -BeTrue Test-Path 'marker-b.txt' | Should -BeFalse qrun -map $script:map setup.step-a Test-Path 'marker-b.txt' | Should -BeFalse qrun -map $script:map setup.step-b Test-Path 'marker-b.txt' | Should -BeTrue Test-Path 'marker-c.txt' | Should -BeFalse } It 'treats process.all as the full process' { qrun -map $script:map setup.all Test-Path 'marker-a/done.txt' | Should -BeTrue Test-Path 'marker-b.txt' | Should -BeTrue Test-Path 'marker-c.txt' | Should -BeTrue } It 'runs from a step onward with -Follow' { qrun -map $script:map setup.step-a qrun -map $script:map setup.step-b -Follow Test-Path 'marker-b.txt' | Should -BeTrue Test-Path 'marker-c.txt' | Should -BeTrue } It 'rejects -Follow without a step path' { { qrun -map $script:map setup -Follow } | Should -Throw '*-Follow requires a step path*' } It 'blocks starting mid-process when prior steps are incomplete' { { qrun -map $script:map setup.step-b } | Should -Throw '*prior steps are incomplete*' Test-Path 'marker-b.txt' | Should -BeFalse } It 'allows skipping incomplete prior steps with -Force' { # independent steps (no artifact coupling) so -Force only exercises the runner gate $indep = [ordered]@{ setup = [ordered]@{ a = { 'a' | Set-Content 'a.txt' } b = { 'b' | Set-Content 'b.txt' } c = { 'c' | Set-Content 'c.txt' } } } qrun -map $indep setup.b -Force Test-Path 'b.txt' | Should -BeTrue Test-Path 'a.txt' | Should -BeFalse } It 'allows -Follow mid-process with -Force when priors are incomplete' { $indep = [ordered]@{ setup = [ordered]@{ a = { 'a' | Set-Content 'a.txt' } b = { 'b' | Set-Content 'b.txt' } c = { 'c' | Set-Content 'c.txt' } } } qrun -map $indep setup.b -Follow -Force Test-Path 'b.txt' | Should -BeTrue Test-Path 'c.txt' | Should -BeTrue Test-Path 'a.txt' | Should -BeFalse } It 'records failure and resumes from the failed step' { $failMap = [ordered]@{ setup = [ordered]@{ ok = { 'ok' | Set-Content 'ok.txt' } boom = { throw 'boom-error' } after = { 'after' | Set-Content 'after.txt' } } } { qrun -map $failMap setup } | Should -Throw '*boom-error*' Test-Path 'ok.txt' | Should -BeTrue Test-Path 'after.txt' | Should -BeFalse $stateFiles = @(Get-ChildItem -Path (Join-Path $script:workDir '.configmap' 'process') -Recurse -Filter '*.state.json') $state = Get-Content $stateFiles[0].FullName -Raw | ConvertFrom-Json $state.steps.ok.status | Should -Be 'Succeeded' $state.steps.boom.status | Should -Be 'Failed' $state.steps.boom.error | Should -Match 'boom-error' # fix boom and resume $failMap.setup.boom = { 'fixed' | Set-Content 'boom.txt' } qrun -map $failMap setup Test-Path 'boom.txt' | Should -BeTrue Test-Path 'after.txt' | Should -BeTrue } It 'resets state with -Reset' { qrun -map $script:map setup qrun -map $script:map setup -Reset $stateFiles = @(Get-ChildItem -Path (Join-Path $script:workDir '.configmap' 'process') -Recurse -Filter '*.state.json' -ErrorAction SilentlyContinue) $stateFiles.Count | Should -Be 0 Remove-Item 'marker-b.txt', 'marker-c.txt' -Force qrun -map $script:map setup Test-Path 'marker-b.txt' | Should -BeTrue } It 'shows status with -Status' { qrun -map $script:map setup.step-a { qrun -map $script:map setup -Status } | Should -Not -Throw } It 'shows state with !state and !state show' { # list-like: all processes/steps appear even before any run { qrun -map $script:map '!state' } | Should -Not -Throw qrun -map $script:map setup.step-a { qrun -map $script:map '!state' show } | Should -Not -Throw { qrun -map $script:map '!state' show setup } | Should -Not -Throw } It 'treats bare qrun as !state show' { InModuleScope ConfigMap { param($Map) Mock Write-Host Invoke-QRun -map $Map Should -Invoke Write-Host -ParameterFilter { $Object -eq 'PROCESSES:' -and $ForegroundColor -eq 'Yellow' } Should -Invoke Write-Host -ParameterFilter { $Object -eq 'Process state' } } -Parameters @{ Map = $script:map } } It 'cleans state with !state clean' { qrun -map $script:map setup $stateFiles = @(Get-ChildItem -Path (Join-Path $script:workDir '.configmap' 'process') -Recurse -Filter '*.state.json') $stateFiles.Count | Should -Be 1 qrun -map $script:map '!state' clean setup $stateFiles = @(Get-ChildItem -Path (Join-Path $script:workDir '.configmap' 'process') -Recurse -Filter '*.state.json' -ErrorAction SilentlyContinue) $stateFiles.Count | Should -Be 0 } It 'cleans all state with !state clean' { qrun -map $script:map setup qrun -map $script:map '!state' clean $stateRoot = Join-Path $script:workDir '.configmap' 'process' if (Test-Path $stateRoot) { @(Get-ChildItem -Path $stateRoot -Recurse -Filter '*.state.json' -ErrorAction SilentlyContinue).Count | Should -Be 0 } } It 'supports exec list ordering' { $log = [System.Collections.Generic.List[string]]::new() $execMap = [ordered]@{ setup = [ordered]@{ exec = @('second', 'first') second = { $log.Add('second') } first = { $log.Add('first') } } } qrun -map $execMap setup $log | Should -Be @('second', 'first') } It 'creates a sample process map with !init' { $mapFile = Join-Path $script:workDir '.process.map.ps1' qrun -map $mapFile '!init' Test-Path $mapFile | Should -BeTrue (Get-Content $mapFile -Raw) | Should -Match 'setup' } It 'supports !complete -params -entry without double-binding' { $mapFile = Join-Path $script:workDir '.process.map.ps1' @' [ordered]@{ setup = [ordered]@{ prepare = { 'ok' | Set-Content 'ok.txt' } restore = { param([string]$Environment = 'dev') "env:$Environment" | Set-Content 'env.txt' } } } '@ | Set-Content -LiteralPath $mapFile -Encoding utf8 $names = @(qrun -map $mapFile '!complete' -Params -Entry setup) $global:LASTEXITCODE | Should -Be 0 $names | Should -Contain 'Follow' $names | Should -Contain 'Force' $names | Should -Contain 'Environment' } It 'supports !complete -word and named -entry run' { $mapFile = Join-Path $script:workDir '.process.map.ps1' @' [ordered]@{ setup = [ordered]@{ prepare = { 'ok' | Set-Content 'ok.txt' } restore = { 'r' | Set-Content 'r.txt' } } } '@ | Set-Content -LiteralPath $mapFile -Encoding utf8 $words = @(qrun -map $mapFile '!complete' -Word 'setup.') $global:LASTEXITCODE | Should -Be 0 $words | Should -Contain 'setup.prepare' $words | Should -Contain 'setup.restore' qrun -map $mapFile -entry setup.prepare Test-Path 'ok.txt' | Should -BeTrue } It 'executes skipped scriptblock and shows output when step is skipped' { $comments = [System.Collections.Generic.List[string]]::new() $skipMap = [ordered]@{ setup = [ordered]@{ 'step-a' = @{ exec = { 'a' | Set-Content 'a.txt' } skipped = { return @{ comment = 'rule for port 123 exists' } } } 'step-b' = @{ exec = { 'b' | Set-Content 'b.txt' } skipped = { return @{ comment = @('first comment', 'second comment') } } } } } qrun -map $skipMap setup # capture output on the second run (all steps skipped) $output = qrun -map $skipMap setup 6>&1 | Out-String $output | Should -Match 'rule for port 123 exists' $output | Should -Match 'first comment' $output | Should -Match 'second comment' } It 'shows skipped scriptblock comments in Write-ProcessStatus tree view' { $skipMap = [ordered]@{ setup = [ordered]@{ description = 'check process' 'web' = @{ exec = { 'w' | Set-Content 'w.txt' } skipped = { return @{ comment = 'firewall rule ok' } } } 'dashboard' = @{ exec = { 'd' | Set-Content 'd.txt' } skipped = { return @{ comment = 'dashboard rule ok' } } } } } qrun -map $skipMap setup $output = qrun -map $skipMap '!state' show 6>&1 | Out-String $output | Should -Match 'firewall rule ok' $output | Should -Match 'dashboard rule ok' } Context '-Check' { BeforeEach { $script:checkMap = [ordered]@{ setup = [ordered]@{ 'step-a' = @{ exec = { 'a' | Set-Content 'a.txt' } check = { Test-Path 'a.txt' } } 'step-b' = @{ exec = { 'b' | Set-Content 'b.txt' } check = { Test-Path 'b.txt' } } 'step-c' = { 'c' | Set-Content 'c.txt' } } } function script:Get-TestState { $file = Get-ChildItem -Path (Join-Path $script:workDir '.configmap' 'process') -Recurse -Filter '*.state.json' | Select-Object -First 1 Get-Content $file.FullName -Raw | ConvertFrom-Json } } It 'does not list check as a step' { qrun -map $script:checkMap setup (Get-TestState).steps.PSObject.Properties.Name | Should -Not -Contain 'check' } It 'marks a step Succeeded when its check passes even without a cached run' { 'a' | Set-Content 'a.txt' qrun -map $script:checkMap setup.step-a -Check (Get-TestState).steps.'step-a'.status | Should -Be 'Succeeded' Test-Path 'b.txt' | Should -BeFalse } It 'never runs exec' { { qrun -map $script:checkMap setup -Check } | Should -Throw Test-Path 'a.txt' | Should -BeFalse Test-Path 'b.txt' | Should -BeFalse Test-Path 'c.txt' | Should -BeFalse } It 'resets a cached Succeeded step to Pending when its check fails and throws' { qrun -map $script:checkMap setup Remove-Item 'a.txt' -Force { qrun -map $script:checkMap setup.step-a -Check } | Should -Throw (Get-TestState).steps.'step-a'.status | Should -Be 'Pending' qrun -map $script:checkMap setup Test-Path 'a.txt' | Should -BeTrue } It 'checks only the requested step for a step path' { qrun -map $script:checkMap setup Remove-Item 'a.txt', 'b.txt' -Force { qrun -map $script:checkMap setup.step-b -Check } | Should -Throw $state = Get-TestState $state.steps.'step-a'.status | Should -Be 'Succeeded' $state.steps.'step-b'.status | Should -Be 'Pending' } It 'runs the checks of all steps in a process and reports unchecked steps without failing' { qrun -map $script:checkMap setup $output = qrun -map $script:checkMap setup -Check 6>&1 | Out-String $output | Should -Match 'UNCHECKED\s+step-c' (Get-TestState).steps.'step-c'.status | Should -Be 'Succeeded' } It 'runs a process-level check instead of the step checks' { $script:calls = [System.Collections.Generic.List[string]]::new() $groupMap = [ordered]@{ setup = [ordered]@{ check = { $true } 'step-a' = @{ exec = { 'a' | Set-Content 'a.txt' } check = { throw 'step check must not run' } } 'step-b' = { 'b' | Set-Content 'b.txt' } } } qrun -map $groupMap setup -Check $state = Get-TestState $state.steps.'step-a'.status | Should -Be 'Succeeded' $state.steps.'step-b'.status | Should -Be 'Succeeded' } It 'resets all steps to Pending when the process-level check fails' { $groupMap = [ordered]@{ setup = [ordered]@{ check = { $false } 'step-a' = { 'a' | Set-Content 'a.txt' } 'step-b' = { 'b' | Set-Content 'b.txt' } } } qrun -map $groupMap setup { qrun -map $groupMap setup -Check } | Should -Throw $state = Get-TestState $state.steps.'step-a'.status | Should -Be 'Pending' $state.steps.'step-b'.status | Should -Be 'Pending' } It 'exposes Check in !complete -params' { @(qrun -map $script:checkMap '!complete' -params -entry setup) | Should -Contain 'Check' } } } |