Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Fixed

- [**#102**](https://github.com/psake/PowerShellBuild/issues/102)
`Test-PSBuildPester` no longer raises a parameter-binding error from its
cleanup logic when the optional `ModuleName` parameter is not supplied.
- [**#102**](https://github.com/psake/PowerShellBuild/issues/102)
`Test-PSBuildPester` now respects a Pester module that is already loaded in
the session instead of unconditionally importing the newest installed
version on top of it, which crashed with a Pester.dll version conflict when
two Pester versions were installed side by side. When no Pester is loaded,
the newest installed version (5.0.0 minimum) is imported as before, and a
loaded Pester older than 5.0.0 now produces a clear error.

## [0.8.2] 2026-07-08

### Fixed
Expand Down
1 change: 1 addition & 0 deletions PowerShellBuild/PowerShellBuild.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ FolderDoesNotExist=Folder does not exist: {0}
PathArgumentMustBeAFolder=The Path argument must be a folder. File paths are not allowed.
UnableToFindModuleManifest=Unable to find module manifest [{0}]. Can't import module
PesterTestsFailed=One or more Pester tests failed
PesterVersionNotSupported=Pester version [{0}] is loaded, but Test-PSBuildPester requires Pester 5.0.0 or newer.
CodeCoverage=Code Coverage
Type=Type
CodeCoverageLessThanThreshold=Code coverage: [{0}] is [{1:p}], which is less than the threshold of [{2:p}]
Expand Down
18 changes: 14 additions & 4 deletions PowerShellBuild/Public/Test-PSBuildPester.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,15 @@ function Test-PSBuildPester {
[string]$OutputVerbosity = 'Detailed'
)

if (-not (Get-Module -Name Pester)) {
Import-Module -Name Pester -ErrorAction Stop
# Respect an already-loaded Pester so callers can pin a specific version; importing again
# would load the newest installed Pester on top of it, which crashes when two Pester
# versions are installed side by side. Only load Pester ourselves when none is loaded.
$loadedPester = Get-Module -Name Pester
if (-not $loadedPester) {
$loadedPester = Import-Module -Name Pester -MinimumVersion 5.0.0 -ErrorAction Stop -PassThru
}
if ($loadedPester.Version -lt [version]'5.0.0') {
throw ($LocalizedData.PesterVersionNotSupported -f $loadedPester.Version)
}

try {
Expand All @@ -84,7 +91,6 @@ function Test-PSBuildPester {

Push-Location -LiteralPath $Path

Import-Module Pester -MinimumVersion 5.0.0
$configuration = [PesterConfiguration]::Default
$configuration.Output.Verbosity = $OutputVerbosity
$configuration.Run.PassThru = $true
Expand Down Expand Up @@ -142,6 +148,10 @@ function Test-PSBuildPester {
}
} finally {
Pop-Location
Remove-Module $ModuleName -ErrorAction SilentlyContinue
# ModuleName is optional; Remove-Module with an empty -Name raises a parameter-binding
# error that -ErrorAction SilentlyContinue cannot suppress.
if ($ModuleName) {
Remove-Module -Name $ModuleName -ErrorAction SilentlyContinue
}
}
}
1 change: 1 addition & 0 deletions PowerShellBuild/en-US/Messages.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ FolderDoesNotExist=Folder does not exist: {0}
PathArgumentMustBeAFolder=The Path argument must be a folder. File paths are not allowed.
UnableToFindModuleManifest=Unable to find module manifest [{0}]. Can't import module
PesterTestsFailed=One or more Pester tests failed
PesterVersionNotSupported=Pester version [{0}] is loaded, but Test-PSBuildPester requires Pester 5.0.0 or newer.
CodeCoverage=Code Coverage
Type=Type
CodeCoverageLessThanThreshold=Code coverage: [{0}] is [{1:p}], which is less than the threshold of [{2:p}]
Expand Down
3 changes: 3 additions & 0 deletions build.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[cmdletbinding(DefaultParameterSetName = 'Task')]

Check warning on line 1 in build.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (cmdletbinding)
param(
# Build task(s) to execute
[parameter(ParameterSetName = 'task', position = 0)]
Expand Down Expand Up @@ -28,7 +28,7 @@
[parameter(ParameterSetName = 'Help')]
[switch]$Help,

[pscredential]$PSGalleryApiKey

Check warning on line 31 in build.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (pscredential) Suggestions: (credential, prudential)
)

$ErrorActionPreference = 'Stop'
Expand All @@ -42,6 +42,9 @@
}
Import-Module -Name PSDepend -Verbose:$false
Invoke-PSDepend -Path './requirements.psd1' -Install -Import -Force -WarningAction SilentlyContinue
# Install-only, never imported: importing a second Pester major into this session would
# crash with a Pester.dll version conflict. See requirements.pester-matrix.psd1.
Invoke-PSDepend -Path './requirements.pester-matrix.psd1' -Install -Force -WarningAction SilentlyContinue
}

# Execute psake task(s)
Expand All @@ -55,6 +58,6 @@
if ($PSGalleryApiKey) {
$parameters['galleryApiKey'] = $PSGalleryApiKey
}
Invoke-psake -buildFile $psakeFile -taskList $Task -nologo -parameters $parameters

Check warning on line 61 in build.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (nologo) Suggestions: (nolo, nolock, nonego, neology, onload)
exit ( [int]( -not $psake.build_success ) )
}
18 changes: 18 additions & 0 deletions requirements.pester-matrix.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Install-only dependencies. The bootstrap in build.ps1 installs this file WITHOUT importing:
# these modules exist so the Test-PSBuildPester integration tests can pin them inside
# subprocesses, and importing a second Pester major into the bootstrap session would crash
# with a Pester.dll version conflict against the Pester version from requirements.psd1.
@{
PSDependOptions = @{
Target = 'CurrentUser'
}
# Newest Pester 5.x, installed side by side with the pinned 6.x so the shipped
# Test-PSBuildPester function is verified against both supported majors.
PesterLegacy = @{
Name = 'Pester'
Version = '5.9.0'
Parameters = @{
SkipPublisherCheck = $true
}
}
}
256 changes: 256 additions & 0 deletions tests/Test-PSBuildPester.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
# Integration tests for Test-PSBuildPester (psake/PowerShellBuild#102).
#
# Test-PSBuildPester wraps Invoke-Pester, so these tests are Pester-testing-Pester. Every
# invocation runs in a Start-Job subprocess: two Pester versions cannot coexist in one session,
# and the subprocess lets each test pin the inner Pester version independently of the outer
# framework. The scenarios run against every installed Pester major (5.x and 6.x) to verify the
# shipped function keeps supporting Pester 5 consumers.
#
# The crash fixtures are generated into $TestDrive at runtime, never checked in, so the
# repository's own Pester run can never discover them (see #97 for the convention).

BeforeDiscovery {
# Newest installed Pester of each supported major version. CI installs 6.x (Pester) and
# 5.x (PesterLegacy) via requirements.psd1; locally, absent majors simply produce fewer
# matrix legs.
$script:innerPesterVersions = @(
foreach ($majorVersion in 5, 6) {
$newestOfMajor = Get-Module -Name 'Pester' -ListAvailable |
Where-Object { $_.Version.Major -eq $majorVersion } |
Sort-Object -Property 'Version' -Descending |
Select-Object -First 1
if ($newestOfMajor) {
$newestOfMajor.Version.ToString()
}
}
)
}

Describe 'Test-PSBuildPester' {

BeforeAll {
$script:moduleRoot = Split-Path -Path $PSScriptRoot -Parent
$script:builtModulePath = [IO.Path]::Combine($script:moduleRoot, 'Output', 'PowerShellBuild')

Import-Module -Name ([IO.Path]::Combine($PSScriptRoot, 'fixtures', 'FixtureHelpers.psm1')) -Force

# Runs Test-PSBuildPester in a subprocess with a pinned inner Pester version and reports
# what happened. Returns an object with Threw, ErrorMessage, and the Pester version that
# was loaded in the subprocess after the call.
function script:Invoke-TestPSBuildPesterJob {
param(
[string]$InnerPesterVersion,
[string]$Path,
[hashtable]$AdditionalParameters = @{}
)

$job = Start-Job -ScriptBlock {
param($innerPesterVersion, $builtModulePath, $path, $additionalParameters)

Import-Module -Name 'Pester' -RequiredVersion $innerPesterVersion -ErrorAction Stop
Import-Module -Name $builtModulePath -Force -ErrorAction Stop

$testPSBuildPesterParameters = @{
Path = $path
OutputVerbosity = 'None'
ErrorAction = 'Stop'
}
foreach ($key in $additionalParameters.Keys) {
$testPSBuildPesterParameters[$key] = $additionalParameters[$key]
}

$threw = $false
$errorMessage = $null
try {
Test-PSBuildPester @testPSBuildPesterParameters
} catch {
$threw = $true
$errorMessage = $_.Exception.Message
}

[PSCustomObject]@{
Threw = $threw
ErrorMessage = $errorMessage
LoadedPesterVersions = @((Get-Module -Name 'Pester').Version.ToString())
}
} -ArgumentList $InnerPesterVersion, $script:builtModulePath, $Path, $AdditionalParameters

$jobResult = $job | Wait-Job | Receive-Job
Remove-Job -Job $job -Force
$jobResult
}

# Scenario directories, generated at runtime.
$script:healthyPath = Join-Path -Path $TestDrive -ChildPath 'healthy'
$script:failingTestPath = Join-Path -Path $TestDrive -ChildPath 'failingtest'

Check warning on line 85 in tests/Test-PSBuildPester.tests.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (failingtest) Suggestions: (failings, faintest, failing's)
$script:beforeAllCrashPath = Join-Path -Path $TestDrive -ChildPath 'beforeallcrash'

Check warning on line 86 in tests/Test-PSBuildPester.tests.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (beforeallcrash)
$script:discoveryCrashPath = Join-Path -Path $TestDrive -ChildPath 'discoverycrash'

Check warning on line 87 in tests/Test-PSBuildPester.tests.ps1

View workflow job for this annotation

GitHub Actions / CI / Run Linters

Unknown word (discoverycrash)
$script:coveragePath = Join-Path -Path $TestDrive -ChildPath 'coverage'
$script:outputPath = Join-Path -Path $TestDrive -ChildPath 'out'
foreach ($directory in @(
$script:healthyPath
$script:failingTestPath
$script:beforeAllCrashPath
$script:discoveryCrashPath
$script:coveragePath
$script:outputPath
)) {
New-Item -Path $directory -ItemType Directory -Force > $null
}

Set-Content -Path (Join-Path -Path $script:healthyPath -ChildPath 'Healthy.tests.ps1') -Value @'
Describe 'Healthy suite' {
It 'passes' {
1 | Should -Be 1
}
}
'@

Set-Content -Path (Join-Path -Path $script:failingTestPath -ChildPath 'FailingTest.tests.ps1') -Value @'
Describe 'Suite with a failing test' {
It 'fails' {
1 | Should -Be 2
}
}
'@

Set-Content -Path (Join-Path -Path $script:beforeAllCrashPath -ChildPath 'BeforeAllCrash.tests.ps1') -Value @'
Describe 'Suite with a broken setup' {
BeforeAll {
throw 'BeforeAll exploded'
}

It 'never executes' {
1 | Should -Be 1
}
}
'@

Set-Content -Path (Join-Path -Path $script:discoveryCrashPath -ChildPath 'DiscoveryCrash.tests.ps1') -Value @'
throw 'file exploded during discovery'

Describe 'Unreachable suite' {
It 'is never discovered' {
1 | Should -Be 1
}
}
'@

# Coverage scenario: tests exercising the fixture module, with coverage measured on the
# fixture's public functions.
$script:fixturePath = Copy-PSBuildTestFixture -Destination $TestDrive
$fixtureManifestPath = Join-Path -Path $script:fixturePath -ChildPath 'PSBuildTestFixture.psd1'
Set-Content -Path (Join-Path -Path $script:coveragePath -ChildPath 'Coverage.tests.ps1') -Value @"
BeforeAll {
Import-Module -Name '$fixtureManifestPath' -Force
}

Describe 'Coverage target' {
It 'calls Get-Widget' {
(Get-Widget -Name 'Sprocket').Name | Should -Be 'Sprocket'
}
}
"@
}

AfterAll {
Remove-Module -Name 'FixtureHelpers' -Force -ErrorAction SilentlyContinue
}

Context 'with inner Pester <_>' -ForEach $script:innerPesterVersions {

BeforeAll {
$script:innerVersion = $_
}

It 'succeeds for a healthy suite' {
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:healthyPath

$result.Threw | Should -BeFalse
}

It 'fails the build when a test fails' {
# Regression: #52
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:failingTestPath

$result.Threw | Should -BeTrue
$result.ErrorMessage | Should -Match 'Pester tests failed'
}

It 'fails the build when a setup block throws' {
# Regression: #128 / #133 (FailedCount alone misses failed blocks)
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:beforeAllCrashPath

$result.Threw | Should -BeTrue
$result.ErrorMessage | Should -Match 'Pester tests failed'
}

It 'fails the build when a test file errors during discovery' {
# Regression: #128 / #133 (FailedCount alone misses failed containers)
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:discoveryCrashPath

$result.Threw | Should -BeTrue
$result.ErrorMessage | Should -Match 'Pester tests failed'
}

It 'writes test results to the requested output path' {
$testResultsPath = Join-Path -Path $script:outputPath -ChildPath "testResults-$script:innerVersion.xml"
$additionalParameters = @{
OutputPath = $testResultsPath
}
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:healthyPath -AdditionalParameters $additionalParameters

$result.Threw | Should -BeFalse
$testResultsPath | Should -Exist
}

It 'writes code coverage in the requested format to the requested path' {
# Regression: #62
$coverageOutputPath = Join-Path -Path $script:outputPath -ChildPath "coverage-$script:innerVersion.xml"
$additionalParameters = @{
CodeCoverage = $true
CodeCoverageFiles = @(Join-Path -Path $script:fixturePath -ChildPath 'Public/*.ps1')
CodeCoverageOutputFile = $coverageOutputPath
CodeCoverageOutputFileFormat = 'JaCoCo'
}
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:coveragePath -AdditionalParameters $additionalParameters

$result.Threw | Should -BeFalse
$coverageOutputPath | Should -Exist
[xml]$coverageReport = Get-Content -Path $coverageOutputPath -Raw
$coverageReport.report | Should -Not -BeNullOrEmpty
}
}

# BeforeDiscovery variables are not visible during the run phase, so the discovered version
# list is handed to the run phase through -ForEach.
Context 'Regressions independent of the inner Pester version' -ForEach @(
@{ AvailableVersions = $script:innerPesterVersions }
) {

BeforeAll {
$script:newestInnerVersion = $AvailableVersions | Select-Object -Last 1
$script:oldestInnerVersion = $AvailableVersions | Select-Object -First 1
}

It 'does not error when ModuleName is not provided' {
# Regression: the finally block called Remove-Module with an empty -Name, which
# raised a parameter-binding error that -ErrorAction SilentlyContinue cannot
# suppress.
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:newestInnerVersion -Path $script:healthyPath

$result.Threw | Should -BeFalse
$result.ErrorMessage | Should -BeNullOrEmpty
}

It 'honors the Pester version that is already loaded' -Skip:($script:innerPesterVersions.Count -lt 2) {
# Regression: an unconditional Import-Module Pester -MinimumVersion 5.0.0 loaded the
# newest installed Pester on top of an already-loaded older one, which crashes with a
# Pester.dll version conflict when 5.x and 6.x are installed side by side.
$result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:oldestInnerVersion -Path $script:healthyPath

$result.Threw | Should -BeFalse
$result.LoadedPesterVersions | Should -Be @($script:oldestInnerVersion)
}
}
}
Loading