test/qbuild-process.tests.ps1
|
BeforeAll { Get-Module ConfigMap -ErrorAction SilentlyContinue | Remove-Module Import-Module $PSScriptRoot\..\configmap.psm1 -Force } Describe 'qbuild _settings.process=qrun 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 = 'qrun' } 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 'does not delegate a process entry when an in-memory map has no source file' { $map = @{ bootstrap = @{ _settings = @{ process = 'qrun' } 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 = 'qrun' } 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=qrun 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' { 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' $names = @(Get-ConfigMapProcessNamesInMap -Map (Import-ConfigMap -Map $MapFile)) $names | Should -Contain 'qrun' } -Parameters @{ MapFile = $script:mapFile } } 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=qrun 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 'qrun' qbuild -map $localMap clean qbuild -map $localMap bootstrap.prepare Test-Path (Join-Path $script:workDir 'workspace' 'prepare.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 } } } |