feat(sprint7): §5.1 — Pester smoke tests (no real VM required)

Four test files under tests/ using fake vmrun.cmd (exit code via
%FAKE_VMRUN_EXIT% env var) — no VMware or network dependency:

  _Common.Tests.ps1     — New-CISessionOption TLS flags, Resolve-VmrunPath
                          throw/return, Invoke-Vmrun ExitCode/Output/ThrowOnError
  New-BuildVM.Tests.ps1 — throw on missing template, throw+cleanup on clone
                          failure, Clone_{JobId}_{timestamp} name format
  Remove-BuildVM.Tests.ps1 — no-op on missing VMX, partial dir cleanup,
                             -WhatIf preservation, full destroy removes dir
  Wait-VMReady.Tests.ps1 — throw on missing vmrun, timeout throw,
                           IP ValidatePattern accept/reject

Run: Invoke-Pester tests\ -Output Detailed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simone
2026-05-10 20:12:19 +02:00
parent 552eb628b7
commit f46e4a8ca3
5 changed files with 365 additions and 16 deletions
+102
View File
@@ -0,0 +1,102 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Pester v5 tests for scripts/New-BuildVM.ps1
#>
BeforeAll {
$script:ScriptPath = Join-Path $PSScriptRoot '..\scripts\New-BuildVM.ps1'
$script:CloneBaseDir = Join-Path $env:TEMP "CI-Tests-CloneBase-$PID"
New-Item -ItemType Directory -Path $script:CloneBaseDir -Force | Out-Null
# Fake vmrun.cmd — exits with %FAKE_VMRUN_EXIT% (default 0)
# On success it also creates the destination VMX so the post-clone Test-Path passes.
$script:FakeVmrun = Join-Path $env:TEMP "fake-vmrun-$PID.cmd"
Set-Content $script:FakeVmrun -Value @'
@echo off
if "%FAKE_VMRUN_EXIT%"=="" ( set _exit=0 ) else ( set _exit=%FAKE_VMRUN_EXIT% )
if "%_exit%"=="0" (
REM Create the destination VMX file so the caller's Test-Path check passes.
REM The destination path is the 4th positional arg (after -T ws clone <template>).
echo. > %5
)
exit /b %_exit%
'@
# Fake template VMX — just needs to exist
$script:FakeTemplate = Join-Path $env:TEMP "fake-template-$PID.vmx"
Set-Content $script:FakeTemplate -Value 'config.version = "8"'
}
AfterAll {
Remove-Item $script:CloneBaseDir -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item $script:FakeVmrun -Force -ErrorAction SilentlyContinue
Remove-Item $script:FakeTemplate -Force -ErrorAction SilentlyContinue
$env:FAKE_VMRUN_EXIT = $null
}
# ─────────────────────────────────────────────────────────────────────────────
Describe 'New-BuildVM — parameter validation' {
It 'throws when TemplatePath does not exist' {
{
& $script:ScriptPath `
-TemplatePath 'C:\DoesNotExist\template.vmx' `
-CloneBaseDir $script:CloneBaseDir `
-JobId 'test-job-1' `
-VmrunPath $script:FakeVmrun
} | Should -Throw
}
}
# ─────────────────────────────────────────────────────────────────────────────
Describe 'New-BuildVM — vmrun failure' {
It 'throws and cleans up clone dir when vmrun clone exits non-zero' {
$env:FAKE_VMRUN_EXIT = '1'
{
& $script:ScriptPath `
-TemplatePath $script:FakeTemplate `
-CloneBaseDir $script:CloneBaseDir `
-JobId 'test-job-fail' `
-VmrunPath $script:FakeVmrun
} | Should -Throw -ExpectedMessage '*clone failed*'
# No leftover clone dir
$leftover = Get-ChildItem $script:CloneBaseDir -Directory |
Where-Object { $_.Name -like '*test-job-fail*' }
$leftover | Should -BeNullOrEmpty
}
AfterEach { $env:FAKE_VMRUN_EXIT = $null }
}
# ─────────────────────────────────────────────────────────────────────────────
Describe 'New-BuildVM — clone name format' {
It 'clone folder is named Clone_{JobId}_{yyyyMMdd_HHmmss}' {
$env:FAKE_VMRUN_EXIT = '0'
$vmx = & $script:ScriptPath `
-TemplatePath $script:FakeTemplate `
-CloneBaseDir $script:CloneBaseDir `
-JobId 'run-42' `
-VmrunPath $script:FakeVmrun
$vmx | Should -Match 'Clone_run-42_\d{8}_\d{6}'
}
It 'returns a string ending in .vmx' {
$env:FAKE_VMRUN_EXIT = '0'
$vmx = & $script:ScriptPath `
-TemplatePath $script:FakeTemplate `
-CloneBaseDir $script:CloneBaseDir `
-JobId 'run-99' `
-VmrunPath $script:FakeVmrun
$vmx | Should -Match '\.vmx$'
}
AfterEach {
$env:FAKE_VMRUN_EXIT = $null
# Clean up clones created by successful tests
Get-ChildItem $script:CloneBaseDir -Directory -ErrorAction SilentlyContinue |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}
}
+68
View File
@@ -0,0 +1,68 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Pester v5 tests for scripts/Remove-BuildVM.ps1
#>
BeforeAll {
$script:ScriptPath = Join-Path $PSScriptRoot '..\scripts\Remove-BuildVM.ps1'
# Fake vmrun: accepts any args, always exits 0 (best-effort ops don't need to fail here)
$script:FakeVmrun = Join-Path $env:TEMP "fake-vmrun-remove-$PID.cmd"
Set-Content $script:FakeVmrun -Value '@echo off & exit /b 0'
}
AfterAll {
Remove-Item $script:FakeVmrun -Force -ErrorAction SilentlyContinue
}
# ─────────────────────────────────────────────────────────────────────────────
Describe 'Remove-BuildVM — missing VMX' {
It 'returns without error when VMX does not exist' {
{ & $script:ScriptPath -VMPath 'C:\DoesNotExist\clone.vmx' -VmrunPath $script:FakeVmrun } |
Should -Not -Throw
}
It 'removes partial clone directory even when VMX is missing' {
$partialDir = Join-Path $env:TEMP "partial-clone-$PID"
New-Item -ItemType Directory -Path $partialDir -Force | Out-Null
$fakeMissingVmx = Join-Path $partialDir 'missing.vmx'
& $script:ScriptPath -VMPath $fakeMissingVmx -VmrunPath $script:FakeVmrun
Test-Path $partialDir | Should -Be $false
}
}
# ─────────────────────────────────────────────────────────────────────────────
Describe 'Remove-BuildVM — WhatIf' {
It 'does not remove clone directory when -WhatIf is passed' {
$cloneDir = Join-Path $env:TEMP "whatif-clone-$PID"
$cloneVmx = Join-Path $cloneDir 'whatif-clone.vmx'
New-Item -ItemType Directory -Path $cloneDir -Force | Out-Null
Set-Content $cloneVmx -Value 'config.version = "8"'
& $script:ScriptPath -VMPath $cloneVmx -VmrunPath $script:FakeVmrun -WhatIf
Test-Path $cloneDir | Should -Be $true
Remove-Item $cloneDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
# ─────────────────────────────────────────────────────────────────────────────
Describe 'Remove-BuildVM — successful destroy' {
It 'removes the clone directory when VMX exists and vmrun succeeds' {
$cloneDir = Join-Path $env:TEMP "live-clone-$PID"
$cloneVmx = Join-Path $cloneDir 'live-clone.vmx'
New-Item -ItemType Directory -Path $cloneDir -Force | Out-Null
Set-Content $cloneVmx -Value 'config.version = "8"'
& $script:ScriptPath `
-VMPath $cloneVmx `
-VmrunPath $script:FakeVmrun `
-GracefulTimeoutSeconds 1 # short timeout for test speed
Test-Path $cloneDir | Should -Be $false
}
}
+92
View File
@@ -0,0 +1,92 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Pester v5 tests for scripts/Wait-VMReady.ps1
#>
BeforeAll {
$script:ScriptPath = Join-Path $PSScriptRoot '..\scripts\Wait-VMReady.ps1'
# Fake vmrun: getGuestIPAddress exits with %FAKE_VMRUN_EXIT% (default 1 = not ready)
$script:FakeVmrun = Join-Path $env:TEMP "fake-vmrun-wait-$PID.cmd"
Set-Content $script:FakeVmrun -Value @'
@echo off
if "%FAKE_VMRUN_EXIT%"=="" ( exit /b 1 ) else ( exit /b %FAKE_VMRUN_EXIT% )
'@
# Fake VMX file — just needs to exist
$script:FakeVmx = Join-Path $env:TEMP "fake-wait-$PID.vmx"
Set-Content $script:FakeVmx -Value 'config.version = "8"'
}
AfterAll {
Remove-Item $script:FakeVmrun -Force -ErrorAction SilentlyContinue
Remove-Item $script:FakeVmx -Force -ErrorAction SilentlyContinue
$env:FAKE_VMRUN_EXIT = $null
}
# ─────────────────────────────────────────────────────────────────────────────
Describe 'Wait-VMReady — missing vmrun' {
It 'throws when VmrunPath does not exist' {
{
& $script:ScriptPath `
-VMPath $script:FakeVmx `
-IPAddress '192.168.79.101' `
-VmrunPath 'C:\DoesNotExist\vmrun.exe'
} | Should -Throw
}
}
# ─────────────────────────────────────────────────────────────────────────────
Describe 'Wait-VMReady — timeout' {
It 'throws with timeout message when VM never becomes ready' {
$env:FAKE_VMRUN_EXIT = '1' # vmrun getGuestIPAddress always fails → VM not running
{
& $script:ScriptPath `
-VMPath $script:FakeVmx `
-IPAddress '192.168.79.101' `
-VmrunPath $script:FakeVmrun `
-TimeoutSeconds 5 `
-PollIntervalSeconds 2 `
-SkipPing
} | Should -Throw -ExpectedMessage '*Timed out*'
}
AfterEach { $env:FAKE_VMRUN_EXIT = $null }
}
# ─────────────────────────────────────────────────────────────────────────────
Describe 'Wait-VMReady — IP validation' {
It 'rejects an invalid IP address' {
{
& $script:ScriptPath `
-VMPath $script:FakeVmx `
-IPAddress '999.999.999.999' `
-VmrunPath $script:FakeVmrun
} | Should -Throw
}
It 'accepts a valid IP address without immediate error from parameter binding' {
# This will still time out (vmrun fake exits 1), but the IP itself must not
# cause a parameter validation error — that would throw before the timeout.
$env:FAKE_VMRUN_EXIT = '1'
$err = $null
try {
& $script:ScriptPath `
-VMPath $script:FakeVmx `
-IPAddress '192.168.79.101' `
-VmrunPath $script:FakeVmrun `
-TimeoutSeconds 3 `
-PollIntervalSeconds 2 `
-SkipPing `
-ErrorAction Stop
} catch {
$err = $_
}
# Error must be the timeout, not a parameter validation error
"$err" | Should -Match 'Timed out'
"$err" | Should -Not -Match 'Cannot validate'
$env:FAKE_VMRUN_EXIT = $null
}
}
+90
View File
@@ -0,0 +1,90 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Pester v5 tests for scripts/_Common.psm1
#>
BeforeAll {
Import-Module (Join-Path $PSScriptRoot '..\scripts\_Common.psm1') -Force
# Fake vmrun: accepts any args, exits with %FAKE_VMRUN_EXIT% (default 0)
$script:FakeVmrun = Join-Path $env:TEMP "fake-vmrun-$PID.cmd"
Set-Content $script:FakeVmrun -Value @'
@echo off
echo fake-vmrun-output
if "%FAKE_VMRUN_EXIT%"=="" ( exit /b 0 ) else ( exit /b %FAKE_VMRUN_EXIT% )
'@
}
AfterAll {
Remove-Item $script:FakeVmrun -Force -ErrorAction SilentlyContinue
}
# ─────────────────────────────────────────────────────────────────────────────
Describe 'New-CISessionOption' {
It 'returns a PSSessionOption' {
$opt = New-CISessionOption
$opt | Should -BeOfType [System.Management.Automation.Remoting.PSSessionOption]
}
It 'has SkipCACheck = true' {
(New-CISessionOption).SkipCACheck | Should -Be $true
}
It 'has SkipCNCheck = true' {
(New-CISessionOption).SkipCNCheck | Should -Be $true
}
It 'has SkipRevocationCheck = true' {
(New-CISessionOption).SkipRevocationCheck | Should -Be $true
}
}
# ─────────────────────────────────────────────────────────────────────────────
Describe 'Resolve-VmrunPath' {
It 'returns the path when the file exists' {
$tmp = [System.IO.Path]::GetTempFileName()
try {
Resolve-VmrunPath -VmrunPath $tmp | Should -Be $tmp
} finally {
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
}
}
It 'throws with a descriptive message when file is missing' {
{ Resolve-VmrunPath -VmrunPath 'C:\DoesNotExist\vmrun.exe' } |
Should -Throw -ExpectedMessage '*vmrun.exe not found*'
}
}
# ─────────────────────────────────────────────────────────────────────────────
Describe 'Invoke-Vmrun' {
It 'returns ExitCode 0 and Output when command succeeds' {
$env:FAKE_VMRUN_EXIT = '0'
$r = Invoke-Vmrun -VmrunPath $script:FakeVmrun -Operation 'list'
$r.ExitCode | Should -Be 0
"$($r.Output)" | Should -Match 'fake-vmrun-output'
}
It 'returns non-zero ExitCode without throwing by default' {
$env:FAKE_VMRUN_EXIT = '1'
$r = Invoke-Vmrun -VmrunPath $script:FakeVmrun -Operation 'clone'
$r.ExitCode | Should -Be 1
}
It 'throws when -ThrowOnError and ExitCode is non-zero' {
$env:FAKE_VMRUN_EXIT = '1'
{ Invoke-Vmrun -VmrunPath $script:FakeVmrun -Operation 'clone' -ThrowOnError } |
Should -Throw -ExpectedMessage '*clone failed*'
}
It 'does not throw when -ThrowOnError and ExitCode is 0' {
$env:FAKE_VMRUN_EXIT = '0'
{ Invoke-Vmrun -VmrunPath $script:FakeVmrun -Operation 'start' -ThrowOnError } |
Should -Not -Throw
}
AfterEach {
$env:FAKE_VMRUN_EXIT = $null
}
}