From ab9e1bb75691c28fc8f06e5386f36717f39baa79 Mon Sep 17 00:00:00 2001 From: nohwnd Date: Tue, 30 Jun 2026 16:12:16 +0200 Subject: [PATCH 1/2] Import Pester via its manifest in parallel workers Invoke-TestInParallel handed each worker $ExecutionContext.SessionState.Module.Path, which is the root module (Pester.psm1), not the manifest (Pester.psd1). Importing the bare .psm1 loads Pester without its manifest metadata, so the worker's Pester reports ModuleVersion 0.0.0.0. A test that imports a module whose manifest lists Pester in RequiredModules (e.g. @{ ModuleName = 'Pester'; ModuleVersion = '5.7.1' }) then fails under Run.Parallel, because the loaded 0.0.0.0 Pester does not satisfy the required version and PowerShell tries to load Pester from disk instead - throwing either "no valid module file was found in any module directory" or an assembly-already-loaded conflict. Resolve the manifest (.psd1) next to the module and import that, falling back to the root module path when no manifest is present, so workers load Pester with its real version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/functions/Pester.Parallel.ps1 | 12 +++++++- tst/Pester.RSpec.Parallel.ts.ps1 | 46 +++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/functions/Pester.Parallel.ps1 b/src/functions/Pester.Parallel.ps1 index 91f65a503..e2c16dc98 100644 --- a/src/functions/Pester.Parallel.ps1 +++ b/src/functions/Pester.Parallel.ps1 @@ -175,7 +175,17 @@ function Invoke-TestInParallel { }) # Path to the currently loaded Pester so each worker imports the exact same build. - $modulePath = $ExecutionContext.SessionState.Module.Path + # Use the module manifest (.psd1), not the root module (.psm1) that Module.Path points to: + # importing the bare .psm1 loads Pester without its manifest metadata, so its ModuleVersion + # becomes 0.0.0.0. That then fails to satisfy any module a test imports whose manifest lists + # Pester in RequiredModules (e.g. @{ ModuleName = 'Pester'; ModuleVersion = '5.7.1' }), + # because the loaded 0.0.0.0 is below the required version (#2816). + $pesterModuleInfo = $ExecutionContext.SessionState.Module + $modulePath = $pesterModuleInfo.Path + $manifestPath = & $SafeCommands['Join-Path'] $pesterModuleInfo.ModuleBase "$($pesterModuleInfo.Name).psd1" + if (& $SafeCommands['Test-Path'] -LiteralPath $manifestPath -PathType Leaf) { + $modulePath = $manifestPath + } # Cap concurrency at Run.ParallelThrottleLimit when set (> 0); otherwise use all processors. $requestedThrottle = [int]$Configuration.Run.ParallelThrottleLimit.Value diff --git a/tst/Pester.RSpec.Parallel.ts.ps1 b/tst/Pester.RSpec.Parallel.ts.ps1 index 10963d6a4..e9e5f4585 100644 --- a/tst/Pester.RSpec.Parallel.ts.ps1 +++ b/tst/Pester.RSpec.Parallel.ts.ps1 @@ -193,6 +193,52 @@ Describe 'D' { It 'sees data' { $Module | Should -Be 'hello'; $Data.k | Should - } } + b "Run.Parallel module loading" { + t "imports a module that lists Pester in RequiredModules (#2816)" { + # Each parallel worker imports Pester so test bodies can use it. The worker must import + # Pester *via its manifest* so the loaded module keeps its real ModuleVersion. Importing + # the bare root module instead would load Pester as 0.0.0.0, and any module a test imports + # whose manifest lists Pester in RequiredModules (e.g. @{ ModuleName = 'Pester'; + # ModuleVersion = '5.0.0' }) would then fail to resolve that requirement against the + # loaded 0.0.0.0 Pester - the bug reported in #2816. + $folder = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().Guid) + $null = New-Item -ItemType Directory -Path $folder -Force + try { + $moduleDir = Join-Path $folder 'RequiresPester' + $null = New-Item -ItemType Directory -Path $moduleDir -Force + Set-Content -Path (Join-Path $moduleDir 'RequiresPester.psm1') -Value 'function Get-RequiresPester { ''ok'' }' + Set-Content -Path (Join-Path $moduleDir 'RequiresPester.psd1') -Value @' +@{ + RootModule = 'RequiresPester.psm1' + ModuleVersion = '1.0.0' + GUID = 'b3c4d5e6-f7a8-4901-b2c3-d4e5f6a7b8c9' + RequiredModules = @( @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } ) + FunctionsToExport = @('Get-RequiresPester') +} +'@ + $manifest = Join-Path $moduleDir 'RequiresPester.psd1' + Set-Content -Path (Join-Path $folder 'Import.Tests.ps1') -Value @" +Describe 'Module import' { + It 'imports a module that requires Pester' { + { Import-Module '$manifest' -Force -ErrorAction Stop } | Should -Not -Throw + } +} +"@ + $c = [PesterConfiguration]::Default + $c.Run.Path = $folder + $c.Run.Parallel = $true + $c.Run.PassThru = $true + $c.Output.Verbosity = 'None' + + $r = Invoke-Pester -Configuration $c + + $r.PassedCount | Verify-Equal 1 + $r.FailedCount | Verify-Equal 0 + } + finally { Remove-Item -Path $folder -Recurse -Force } + } + } + b "Run.BeforeContainer" { t "runs the repo-root Pester.BeforeContainer.ps1 before each file in a sequential run" { $folder = New-BeforeContainerTestFolder From ea756be8102b8097e0a971ccafaf694f2118a37e Mon Sep 17 00:00:00 2001 From: nohwnd Date: Tue, 30 Jun 2026 19:22:32 +0200 Subject: [PATCH 2/2] Unload RequiresPester after the parallel RequiredModules test The regression test imports a RequiresPester module whose manifest lists Pester in RequiredModules. When Run.Parallel falls back to sequential (e.g. Windows PowerShell 5.1, which has no ForEach-Object -Parallel) that import runs in the current process, so the module leaks into the shared P-test session. The next *.ts.ps1 file's `Get-Module Pester | Remove-Module` then fails with "Unable to remove the module 'Pester' because it is required by 'RequiresPester'". Remove the module in the test's finally block before deleting its folder. Unloading first also releases the lock on its .psm1 so the folder removal cannot fail on Windows. The cleanup is a harmless no-op on the true-parallel path, where the import stays in the worker runspace. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tst/Pester.RSpec.Parallel.ts.ps1 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tst/Pester.RSpec.Parallel.ts.ps1 b/tst/Pester.RSpec.Parallel.ts.ps1 index e9e5f4585..a276433bd 100644 --- a/tst/Pester.RSpec.Parallel.ts.ps1 +++ b/tst/Pester.RSpec.Parallel.ts.ps1 @@ -235,7 +235,15 @@ Describe 'Module import' { $r.PassedCount | Verify-Equal 1 $r.FailedCount | Verify-Equal 0 } - finally { Remove-Item -Path $folder -Recurse -Force } + finally { + # Import.Tests.ps1 imports RequiresPester, which takes a dependency on Pester. When + # Run.Parallel falls back to sequential (e.g. Windows PowerShell 5.1) that import runs + # in this process, so the module leaks into the shared P-test session and the next + # *.ts.ps1 file's `Remove-Module Pester` fails with "required by 'RequiresPester'". + # Unload it first - this also releases the lock on its .psm1 so the folder can be removed. + Get-Module RequiresPester | Remove-Module -Force + Remove-Item -Path $folder -Recurse -Force + } } }