test/qbuild-process.tests.ps1
|
BeforeAll { Get-Module ConfigMap -ErrorAction SilentlyContinue | Remove-Module Import-Module $PSScriptRoot\..\configmap.psm1 -Force } Describe 'qbuild _settings.process=steps forwarding' { BeforeEach { $script:workDir = Join-Path $TestDrive ("qbuild-process-" + [guid]::NewGuid().ToString('n')) New-Item -ItemType Directory -Path $script:workDir -Force | Out-Null Push-Location $script:workDir # Contract is path-only handoff; write the map to a file for all invokes. $script:mapFile = Join-Path $script:workDir '.build.map.ps1' @' [ordered]@{ build = { param( [ValidateSet('Debug', 'Release')] [string]$Configuration = 'Debug' ) "built:$Configuration" | Set-Content 'build.txt' } bootstrap = [ordered]@{ _settings = @{ process = 'steps' } description = 'test process' prepare = { New-Item -ItemType Directory -Path 'marker-a' -Force | Out-Null 'a' | Set-Content (Join-Path 'marker-a' 'done.txt') } restore = { param( [ValidateSet('dev', 'prod')] [string]$Environment = 'dev' ) if (!(Test-Path 'marker-a/done.txt')) { throw 'prepare missing' } "restored:$Environment" | Set-Content 'marker-b.txt' } verify = { 'c' | Set-Content 'marker-c.txt' } } } '@ | Set-Content -LiteralPath $script:mapFile -Encoding utf8 } AfterEach { Pop-Location } It 'still runs default qbuild entries' { qbuild -map $script:mapFile build -Configuration Release Get-Content 'build.txt' -Raw | Should -Match 'built:Release' } It 'forwards full process run and persists success state' { qbuild -map $script:mapFile bootstrap 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.prepare.status | Should -Be 'Succeeded' $state.steps.restore.status | Should -Be 'Succeeded' $state.steps.verify.status | Should -Be 'Succeeded' } It 'forwards -Check for a process and for a step path' { @' [ordered]@{ bootstrap = [ordered]@{ _settings = @{ process = 'steps' } prepare = @{ exec = { 'a' | Set-Content 'a.txt' } check = { Test-Path 'a.txt' } } restore = @{ exec = { 'b' | Set-Content 'b.txt' } check = { Test-Path 'b.txt' } } } } '@ | Set-Content -LiteralPath $script:mapFile -Encoding utf8 'a' | Set-Content 'a.txt' qbuild -map $script:mapFile bootstrap.prepare -check { qbuild -map $script:mapFile bootstrap -check } | Should -Throw '*not in desired state*' { qbuild -map $script:mapFile bootstrap.restore -check } | Should -Throw '*not in desired state*' Test-Path 'b.txt' | Should -BeFalse $stateFile = Get-ChildItem -Path (Join-Path $script:workDir '.configmap' 'process') -Recurse -Filter '*.state.json' | Select-Object -First 1 $state = Get-Content $stateFile.FullName -Raw | ConvertFrom-Json $state.steps.prepare.status | Should -Be 'Succeeded' $state.steps.restore.status | Should -Be 'Pending' } It 'Given _settings.forceCheck, when qbuild runs the process again, then it reruns steps whose check fails and skips the rest' { @' [ordered]@{ bootstrap = [ordered]@{ _settings = @{ process = 'steps'; forceCheck = $true } prepare = @{ exec = { 'a' | Set-Content 'a.txt' } check = { Test-Path 'a.txt' } } restore = @{ exec = { 'b' | Set-Content 'b.txt' } check = { Test-Path 'b.txt' } } } } '@ | Set-Content -LiteralPath $script:mapFile -Encoding utf8 qbuild -map $script:mapFile bootstrap Remove-Item 'a.txt' -Force 'manual' | Set-Content 'b.txt' qbuild -map $script:mapFile bootstrap Test-Path 'a.txt' | Should -BeTrue Get-Content 'b.txt' -Raw | Should -Match 'manual' } It 'Given _settings.useWorkDir on a nested process entry, when qbuild runs from a subdirectory, then steps and checks run there and _context.mapDir points at the map' { @' [ordered]@{ build = [ordered]@{ incremental = [ordered]@{ _settings = @{ process = 'steps'; useWorkDir = $true } compile = @{ exec = { param($_context) "$($_context.mapDir)" | Set-Content 'out.txt' } check = { Test-Path 'out.txt' } } } } } '@ | Set-Content -LiteralPath $script:mapFile -Encoding utf8 $subDir = New-Item -ItemType Directory -Path (Join-Path $script:workDir 'sub') -Force Push-Location $subDir try { qbuild -map $script:mapFile build.incremental } finally { Pop-Location } Test-Path (Join-Path $script:workDir 'out.txt') | Should -BeFalse (Get-Content (Join-Path $subDir 'out.txt') -Raw).Trim() | Should -Be $script:workDir } It 'Given _settings.useWorkDir on an exec entry, when qbuild runs from a subdirectory, then it runs there' { @' [ordered]@{ stamp = @{ _settings = @{ useWorkDir = $true } exec = { param($_context) "$($_context.mapDir)" | Set-Content 'out.txt' } } } '@ | Set-Content -LiteralPath $script:mapFile -Encoding utf8 $subDir = New-Item -ItemType Directory -Path (Join-Path $script:workDir 'sub') -Force Push-Location $subDir try { qbuild -map $script:mapFile stamp } finally { Pop-Location } Test-Path (Join-Path $script:workDir 'out.txt') | Should -BeFalse (Get-Content (Join-Path $subDir 'out.txt') -Raw).Trim() | Should -Be $script:workDir } It 'does not delegate a process entry when an in-memory map has no source file' { $map = @{ bootstrap = @{ _settings = @{ process = 'steps' } prepare = { 'prepared' | Set-Content 'in-memory-process.txt' } } } { qbuild -map $map bootstrap } | Should -Throw '*the map has no _sourceFile*' Test-Path 'in-memory-process.txt' | Should -BeFalse } It 'skips already succeeded steps on re-run' { qbuild -map $script:mapFile bootstrap Remove-Item 'marker-b.txt', 'marker-c.txt' -Force qbuild -map $script:mapFile bootstrap 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' { qbuild -map $script:mapFile bootstrap Remove-Item 'marker-b.txt' -Force qbuild -map $script:mapFile bootstrap -Force Test-Path 'marker-b.txt' | Should -BeTrue Test-Path 'marker-c.txt' | Should -BeTrue } It 'runs a single step via process.step path' { qbuild -map $script:mapFile bootstrap.prepare Test-Path 'marker-a/done.txt' | Should -BeTrue Test-Path 'marker-b.txt' | Should -BeFalse qbuild -map $script:mapFile bootstrap.restore Test-Path 'marker-b.txt' | Should -BeTrue Test-Path 'marker-c.txt' | Should -BeFalse } It 'runs from a step onward with -Follow' { qbuild -map $script:mapFile bootstrap.prepare qbuild -map $script:mapFile bootstrap.restore -Follow Test-Path 'marker-b.txt' | Should -BeTrue Test-Path 'marker-c.txt' | Should -BeTrue } It 'rejects -Follow without a step path' { { qbuild -map $script:mapFile bootstrap -Follow } | Should -Throw '*-Follow requires a step path*' } It 'blocks starting mid-process when prior steps are incomplete' { { qbuild -map $script:mapFile bootstrap.restore } | Should -Throw '*prior steps are incomplete*' Test-Path 'marker-b.txt' | Should -BeFalse } It 'allows skipping incomplete prior steps with -Force' { $indepFile = Join-Path $script:workDir 'indep.build.map.ps1' @' [ordered]@{ setup = [ordered]@{ _settings = @{ process = 'steps' } a = { 'a' | Set-Content 'a.txt' } b = { 'b' | Set-Content 'b.txt' } c = { 'c' | Set-Content 'c.txt' } } } '@ | Set-Content -LiteralPath $indepFile -Encoding utf8 qbuild -map $indepFile setup.b -Force Test-Path 'b.txt' | Should -BeTrue Test-Path 'a.txt' | Should -BeFalse } It 'completes step paths under an _settings.process=steps entry' { $map = Import-ConfigMap -Map $script:mapFile $completions = @(Get-EntryCompletion -map $map -language build -wordToComplete 'bootstrap.') $completions | Should -Contain 'bootstrap.prepare' $completions | Should -Contain 'bootstrap.restore' $completions | Should -Contain 'bootstrap.verify' } It 'routes entry completion through process !complete -word via the qrun CLI' { InModuleScope ConfigMap { param($MapFile) $words = @(Get-ConfigMapProcessCompleteWord -ProcessName 'qrun' -MapPath $MapFile -Word 'bootstrap.') $words | Should -Contain 'bootstrap.prepare' $words | Should -Contain 'bootstrap.restore' $words | Should -Contain 'bootstrap.verify' # steps is an in-process handler; external process-name discovery stays empty $names = @(Get-ConfigMapProcessNamesInMap -Map (Import-ConfigMap -Map $MapFile)) $names | Should -Not -Contain 'steps' $names | Should -Not -Contain 'qrun' (Get-ConfigMapProcessCanonicalName -ProcessName 'qrun') | Should -Be 'steps' (Get-ConfigMapProcessHandler -Name 'qrun').Name | Should -Be 'steps' (Get-ConfigMapProcessHandler -Name 'steps').Name | Should -Be 'steps' } -Parameters @{ MapFile = $script:mapFile } } It 'adds external process !complete words to qbuild entry completion when imported as a module' { $externalFile = Join-Path $script:workDir 'external.build.map.ps1' @' [ordered]@{ deploy = [ordered]@{ _settings = @{ process = 'cmtest-external' } exec = { } } } '@ | Set-Content -LiteralPath $externalFile -Encoding utf8 function global:cmtest-external { param([Parameter(ValueFromRemainingArguments = $true)]$Arguments) $global:LASTEXITCODE = 0 'from-external' } try { $line = "qbuild -map '$externalFile' " (TabExpansion2 $line $line.Length).CompletionMatches.CompletionText | Should -Contain 'from-external' } finally { Remove-Item function:global:cmtest-external -ErrorAction SilentlyContinue } } It 'accepts legacy process=qrun as an alias for steps' { $legacyFile = Join-Path $script:workDir 'legacy.build.map.ps1' @' [ordered]@{ bootstrap = [ordered]@{ _settings = @{ process = 'qrun' } prepare = { 'legacy' | Set-Content 'legacy.txt' } } } '@ | Set-Content -LiteralPath $legacyFile -Encoding utf8 qbuild -map $legacyFile bootstrap.prepare Get-Content 'legacy.txt' -Raw | Should -Match 'legacy' } It 'delegates run with explicit -entry' { InModuleScope ConfigMap { param($MapFile) $splat = ConvertTo-ConfigMapProcessSplat -MapPath $MapFile -Entry 'bootstrap.prepare' $splat.entry | Should -Be 'bootstrap.prepare' $splat.Keys | Should -Not -Contain 'command' } -Parameters @{ MapFile = $script:mapFile } qbuild -map $script:mapFile bootstrap.prepare Test-Path 'marker-a/done.txt' | Should -BeTrue } It 'forwards process step parameters end-to-end' { qbuild -map $script:mapFile bootstrap.prepare qbuild -map $script:mapFile bootstrap.restore -Environment prod Get-Content 'marker-b.txt' -Raw | Should -Match 'restored:prod' } It 'lists _settings.process=steps entries with status output' { qbuild -map $script:mapFile bootstrap.prepare Mock Write-Host -ModuleName ConfigMap qbuild -map $script:mapFile list Should -Invoke Write-Host -ModuleName ConfigMap -ParameterFilter { "$Object" -match 'bootstrap' } Should -Invoke Write-Host -ModuleName ConfigMap -ParameterFilter { "$Object" -match 'prepare' } } It 'runs an entry that explicitly selects the exec process' { @' [ordered]@{ task = @{ _settings = @{ process = 'exec' } exec = { 'ran' | Set-Content 'qbuild-process.txt' } } } '@ | Set-Content -LiteralPath $script:mapFile -Encoding utf8 qbuild -map $script:mapFile task Get-Content 'qbuild-process.txt' -Raw | Should -Match 'ran' } It 'registers exec as the default in-process handler' { InModuleScope ConfigMap { $handler = Get-ConfigMapProcessHandler -Name 'exec' $handler.Name | Should -Be 'exec' $handler.Invoke | Should -Not -BeNullOrEmpty $handler.GetDynamicParam | Should -Not -BeNullOrEmpty $handler.WriteListEntry | Should -BeNullOrEmpty } } It 'dispatches a registered in-process handler without a source file' { $probeFile = Join-Path $script:workDir 'probed.txt' InModuleScope ConfigMap { param($ProbeFile) $script:QProbeFile = $ProbeFile Register-ConfigMapProcess -Name 'qprobe' -Invoke { param($Context) "probed:$($Context.Entry)" | Set-Content -LiteralPath $script:QProbeFile } } -Parameters @{ ProbeFile = $probeFile } try { $map = @{ task = @{ _settings = @{ process = 'qprobe' } exec = { 'should-not-run' | Set-Content 'ran.txt' } } } qbuild -map $map task Get-Content -LiteralPath $probeFile -Raw | Should -Match 'probed:task' Test-Path 'ran.txt' | Should -BeFalse } finally { InModuleScope ConfigMap { Unregister-ConfigMapProcess -Name 'qprobe' Remove-Variable -Name QProbeFile -Scope Script -ErrorAction SilentlyContinue } } } It 'smoke-loads the process-mixed sample map' { $sampleDir = Join-Path $PSScriptRoot '..' 'samples' 'process-mixed' $sampleFile = Join-Path $sampleDir '.build.map.ps1' $localMap = Join-Path $script:workDir '.build.map.ps1' Copy-Item -LiteralPath $sampleFile -Destination $localMap $sampleMap = Import-ConfigMap -Map $localMap $sampleMap.Keys | Should -Contain 'build' $sampleMap.Keys | Should -Contain 'bootstrap' $sampleMap.bootstrap._settings.process | Should -Be 'steps' qbuild -map $localMap clean qbuild -map $localMap bootstrap.prepare Test-Path (Join-Path $script:workDir 'workspace' 'prepare.txt') | Should -BeTrue } It 'filters qbuild list by process type via -Type and list.<type> shorthand' { $localMap = Join-Path $script:workDir 'mixed.build.map.ps1' @' [ordered]@{ clean = { 'clean' } steps1 = [ordered]@{ _settings = @{ process = 'steps' }; one = { 'one' } } setting = [ordered]@{ _settings = @{ process = 'options' } get = { 'x' } set = { param($_context, $value, $key) } options = @{ local = 'x' } } } '@ | Set-Content -LiteralPath $localMap -Encoding utf8 $byParam = qbuild -map $localMap list -Type qconf 6>&1 | Out-String $byShorthand = qbuild -map $localMap list.qconf 6>&1 | Out-String $steps = qbuild -map $localMap list.qrun 6>&1 | Out-String $exec = qbuild -map $localMap list.exec 6>&1 | Out-String $byParam | Should -Match 'setting' $byParam | Should -Not -Match 'steps1' $byParam | Should -Not -Match '\bclean\b' $byShorthand | Should -Be $byParam $steps | Should -Match 'steps1' $steps | Should -Not -Match 'setting' $exec | Should -Match '\bclean\b' $exec | Should -Not -Match 'setting|steps1' } It 'runs entries that declare ValidateSet with an @sibling reference' { $localMap = Join-Path $script:workDir 'sibling.build.map.ps1' @' [ordered]@{ deploy = @{ envs = @('dev', 'prod') exec = { param([ValidateSet('@envs')][string]$Env) "deployed:$Env" | Set-Content 'deployed.txt' } } } '@ | Set-Content -LiteralPath $localMap -Encoding utf8 qbuild -map $localMap deploy -Env prod Get-Content (Join-Path $script:workDir 'deployed.txt') | Should -Be 'deployed:prod' { qbuild -map $localMap deploy -Env nope } | Should -Throw '*nope*' } It 'resolves an @sibling ValidateSet from a scriptblock sibling in a map file' { $localMap = Join-Path $script:workDir 'sibling-sb.build.map.ps1' @' [ordered]@{ deploy = [ordered]@{ envs = { Add-Content -LiteralPath (Join-Path $PSScriptRoot 'envs-calls.txt') -Value 'call'; 'dev', 'prod' } exec = { param([ValidateSet('@envs')][string]$Env) "deployed:$Env" | Set-Content 'deployed.txt' } } } '@ | Set-Content -LiteralPath $localMap -Encoding utf8 $callsFile = Join-Path $script:workDir 'envs-calls.txt' $countCalls = { @(Get-Content -LiteralPath $callsFile -ErrorAction SilentlyContinue).Count } $map = Import-ConfigMap -Map $localMap @(Get-EntryCompletion $map -language build -wordToComplete '') | Should -Be @('deploy') $line = "qbuild -map $localMap deploy " $null = TabExpansion2 $line $line.Length & $countCalls | Should -Be 0 $line = "qbuild -map $localMap deploy -Env " (TabExpansion2 $line $line.Length).CompletionMatches.CompletionText | Should -Be @('dev', 'prod') & $countCalls | Should -Be 1 $catalog = Get-ConfigMapCommandCatalog -Map $map -Path deploy -Language build ($catalog.Parameters | Where-Object Name -EQ 'Env').ValidateSet | Should -Be @('dev', 'prod') qbuild -map $localMap deploy -Env prod Get-Content (Join-Path $script:workDir 'deployed.txt') | Should -Be 'deployed:prod' { qbuild -map $localMap deploy -Env nope } | Should -Throw '*nope*does not belong to the set*' } It 'completes list types for -Type and the list.<type> shorthand' { $localMap = Join-Path $script:workDir 'mixed.build.map.ps1' @' [ordered]@{ clean = { 'clean' } steps1 = [ordered]@{ _settings = @{ process = 'steps' }; one = { 'one' } } setting = [ordered]@{ _settings = @{ process = 'options' } get = { 'x' } set = { param($_context, $value, $key) } options = @{ local = 'x' } } } '@ | Set-Content -LiteralPath $localMap -Encoding utf8 $line = "qbuild -map '$localMap' list -Type " (TabExpansion2 $line $line.Length).CompletionMatches.CompletionText | Should -Be @('exec', 'options', 'qconf', 'qrun', 'steps') $line = "qbuild -map '$localMap' list -Type q" (TabExpansion2 $line $line.Length).CompletionMatches.CompletionText | Should -Be @('qconf', 'qrun') $line = "qbuild -map '$localMap' list -Type o" (TabExpansion2 $line $line.Length).CompletionMatches.CompletionText | Should -Be @('options') # canonical names stay valid for -Type, but get no list.<type> shorthand { qbuild -map $localMap list -Type options 6>$null } | Should -Not -Throw { qbuild -map $localMap list -Type steps 6>$null } | Should -Not -Throw { qbuild -map $localMap list -Type qcnof 6>$null } | Should -Throw '*qcnof*' $line = "qbuild -map '$localMap' list." (TabExpansion2 $line $line.Length).CompletionMatches.CompletionText | Should -Be @('list.exec', 'list.qconf', 'list.qrun') } } Describe 'nested qbuild does not inherit the caller''s settings' { BeforeEach { $script:workDir = Join-Path $TestDrive ("qbuild-nested-" + [guid]::NewGuid().ToString('n')) New-Item -ItemType Directory -Path $script:workDir -Force | Out-Null Push-Location $script:workDir $script:mapFile = Join-Path $script:workDir '.build.map.ps1' } AfterEach { Pop-Location } It 'Given a steps step exec that calls qbuild, when the process runs, then the nested target runs as a plain exec entry' { @" [ordered]@{ outer = [ordered]@{ _settings = @{ process = 'steps' } step1 = { qbuild -map '$script:mapFile' inner } } inner = { 'inner' | Set-Content '$script:workDir/inner.txt' } } "@ | Set-Content -LiteralPath $script:mapFile -Encoding utf8 { qbuild -map $script:mapFile outer 6>$null } | Should -Not -Throw Test-Path (Join-Path $script:workDir 'inner.txt') | Should -BeTrue } It 'Given a steps step check that calls qbuild, when the process is checked, then the nested target runs as a plain exec entry' { @" [ordered]@{ outer = [ordered]@{ _settings = @{ process = 'steps' } step1 = @{ exec = { } check = { qbuild -map '$script:mapFile' inner `$true } } } inner = { 'inner' | Set-Content '$script:workDir/inner.txt' } } "@ | Set-Content -LiteralPath $script:mapFile -Encoding utf8 qbuild -map $script:mapFile outer 6>$null { qbuild -map $script:mapFile outer -check 6>$null } | Should -Not -Throw Test-Path (Join-Path $script:workDir 'inner.txt') | Should -BeTrue } It 'Given an entry with useWorkDir that calls qbuild from a subdirectory, when it runs, then the nested target runs in its own map directory' { $subDir = New-Item -ItemType Directory -Path (Join-Path $script:workDir 'sub') -Force @" [ordered]@{ outer = @{ _settings = @{ useWorkDir = `$true } exec = { qbuild -map '$script:mapFile' inner } } inner = { 'inner' | Set-Content 'inner.txt' } } "@ | Set-Content -LiteralPath $script:mapFile -Encoding utf8 Push-Location $subDir try { qbuild -map $script:mapFile outer } finally { Pop-Location } Test-Path (Join-Path $subDir 'inner.txt') | Should -BeFalse Test-Path (Join-Path $script:workDir 'inner.txt') | Should -BeTrue } } Describe 'deprecated standalone process CLIs' { BeforeEach { $script:workDir = Join-Path $TestDrive ("deprecated-cli-" + [guid]::NewGuid().ToString('n')) New-Item -ItemType Directory -Path $script:workDir -Force | Out-Null Push-Location $script:workDir } AfterEach { Pop-Location } It 'warns when qrun is invoked directly' { $mapFile = Join-Path $script:workDir '.process.map.ps1' @' [ordered]@{ setup = [ordered]@{ a = { 'a' | Set-Content 'a.txt' } } } '@ | Set-Content -LiteralPath $mapFile -Encoding utf8 $warnings = $null qrun -map $mapFile setup.a -WarningVariable warnings -WarningAction SilentlyContinue "$warnings" | Should -Match "qrun' is deprecated" "$warnings" | Should -Match "process = 'steps'" } It 'warns when qconf is invoked directly' { $mapFile = Join-Path $script:workDir '.configuration.map.ps1' @' [ordered]@{ storage = [ordered]@{ options = @{ local = 'L' } get = { param($ctx, $options) 'L' } set = { param($ctx, $key, $value) } } } '@ | Set-Content -LiteralPath $mapFile -Encoding utf8 $warnings = $null qconf -map $mapFile storage get -WarningVariable warnings -WarningAction SilentlyContinue "$warnings" | Should -Match "qconf' is deprecated" "$warnings" | Should -Match "process = 'options'" } It 'does not warn when qbuild runs a steps entry' { $mapFile = Join-Path $script:workDir '.build.map.ps1' @' [ordered]@{ setup = [ordered]@{ _settings = @{ process = 'steps' } a = { 'a' | Set-Content 'a.txt' } } } '@ | Set-Content -LiteralPath $mapFile -Encoding utf8 $warnings = $null qbuild -map $mapFile setup.a -WarningVariable warnings -WarningAction SilentlyContinue @($warnings).Count | Should -Be 0 Test-Path 'a.txt' | Should -BeTrue } } Describe 'qbuild in-process process handlers' { BeforeEach { $script:workDir = Join-Path $TestDrive ("qbuild-handler-" + [guid]::NewGuid().ToString('n')) New-Item -ItemType Directory -Path $script:workDir -Force | Out-Null Push-Location $script:workDir } AfterEach { Pop-Location } It 'runs an entry when process is explicitly exec' { $mapFile = Join-Path $script:workDir '.build.map.ps1' @' @{ build = @{ _settings = @{ process = 'exec' } exec = { 'explicit-exec' | Set-Content 'explicit.txt' } } } '@ | Set-Content -LiteralPath $mapFile -Encoding utf8 qbuild -map $mapFile build Get-Content 'explicit.txt' -Raw | Should -Match 'explicit-exec' } It 'registers exec with Invoke and GetDynamicParam and no WriteListEntry' { InModuleScope ConfigMap { $handler = Get-ConfigMapProcessHandler -Name exec $null -ne $handler.Invoke | Should -BeTrue $null -ne $handler.GetDynamicParam | Should -BeTrue $null -eq $handler.WriteListEntry | Should -BeTrue (Get-ConfigMapProcessHandler -Name Exec).Name | Should -Be 'exec' } } It 'runs a registered handler from an in-memory map without exec' { InModuleScope ConfigMap { Register-ConfigMapProcess -Name qprobe -Invoke { param($Context) $Context.Entry | Set-Content 'probe-entry.txt' } try { $map = @{ job = @{ _settings = @{ process = 'qprobe' } exec = { 'exec-ran' | Set-Content 'exec.txt' } } } qbuild -map $map job Get-Content 'probe-entry.txt' -Raw | Should -Match 'job' Test-Path 'exec.txt' | Should -BeFalse } finally { Unregister-ConfigMapProcess -Name qprobe } } } It 'does not delegate a missing exec handler to an external command' { $mapFile = Join-Path $script:workDir '.build.map.ps1' @' @{ build = { 'ran' | Set-Content 'ran.txt' } } '@ | Set-Content -LiteralPath $mapFile -Encoding utf8 InModuleScope ConfigMap { param($MapFile) Unregister-ConfigMapProcess -Name exec Mock Invoke-ConfigMapProcessCli { throw 'cli-was-called' } try { { qbuild -map $MapFile build } | Should -Throw '*no in-process handler*' Test-Path 'ran.txt' | Should -BeFalse } finally { Register-ConfigMapExecProcess } } -Parameters @{ MapFile = $mapFile } } It 'does not delegate process=qbuild back to the qbuild host alias' { $mapFile = Join-Path $script:workDir '.build.map.ps1' @' @{ build = @{ _settings = @{ process = 'qbuild' } exec = { 'ran' | Set-Content 'ran.txt' } } } '@ | Set-Content -LiteralPath $mapFile -Encoding utf8 InModuleScope ConfigMap { param($MapFile) Mock Invoke-ConfigMapProcessCli { throw 'cli-was-called' } { qbuild -map $MapFile build } | Should -Throw '*no in-process handler*' Test-Path 'ran.txt' | Should -BeFalse } -Parameters @{ MapFile = $mapFile } } } Describe 'check keyword scope' { BeforeAll { $script:checkScopeMap = [ordered]@{ check = { 'top-check' } group = [ordered]@{ check = { 'group-check' } other = { 'group-other' } } bootstrap = [ordered]@{ _settings = @{ process = 'steps' } check = { $true } prepare = @{ exec = { 'prepared' } check = { $true } } restore = { 'restored' } } } } It 'Given a top-level check entry, when the map is listed, then check is a command' { $names = @((Get-CompletionList -map $script:checkScopeMap -language build).Keys) $names | Should -Contain 'check' } It 'Given a check entry in a plain group, when the map is listed, then group.check is a command' { $names = @((Get-CompletionList -map $script:checkScopeMap -language build).Keys) $names | Should -Contain 'group.check' } It 'Given a top-level check entry, when the catalog is built, then it describes check' { $entry = Get-ConfigMapCommandCatalog -Map $script:checkScopeMap -Path check -Language build $entry.IsParent | Should -BeFalse } It 'Given a top-level check entry, when qbuild runs it, then its exec runs' { qbuild -map $script:checkScopeMap check | Should -Be 'top-check' } It 'Given a steps process, when the map is listed, then its process-level check is not a command' { $names = @((Get-CompletionList -map $script:checkScopeMap -language build).Keys) $names | Should -Not -Contain 'bootstrap.check' } It 'Given a step with exec and check, when the map is listed, then the step is a leaf without a check child' { $names = @((Get-CompletionList -map $script:checkScopeMap -language build).Keys) $names | Should -Contain 'bootstrap.prepare' $names | Should -Not -Contain 'bootstrap.prepare.check' } It 'Given a step with exec and check, then it is not a parent entry' { Test-IsParentEntry $script:checkScopeMap.bootstrap.prepare | Should -BeFalse } It 'Given a map loaded from a file, when baseDir is added, then a top-level check is wrapped like any entry' { $map = Add-BaseDir ([ordered]@{ check = { 'top' } }) (Join-Path $TestDrive '.build.map.ps1') $map.check | Should -BeOfType [System.Collections.IDictionary] $map.check.exec | Should -BeOfType [scriptblock] } It 'Given a map loaded from a file, when baseDir is added, then steps check handlers stay scriptblocks' { $map = Add-BaseDir ([ordered]@{ bootstrap = [ordered]@{ _settings = @{ process = 'steps' } check = { $true } prepare = @{ exec = { 'p' }; check = { $true } } } }) (Join-Path $TestDrive '.build.map.ps1') $map.bootstrap.check | Should -BeOfType [scriptblock] $map.bootstrap.prepare.check | Should -BeOfType [scriptblock] } It 'Given a steps process with a process-level check, then check is not a step' { InModuleScope ConfigMap -Parameters @{ ProcessEntry = $script:checkScopeMap.bootstrap } { param($ProcessEntry) Get-ProcessStepNames -ProcessEntry $ProcessEntry | Should -Be @('prepare', 'restore') } } } |