diff --git a/CHANGELOG.md b/CHANGELOG.md index 5da6b45..14a0705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed +- [**#172**](https://github.com/psake/PowerShellBuild/issues/172) + **Breaking:** the consumer Pester floor in `RequiredModules` is raised from + `5.6.1` to `6.0.0`, and `Test-PSBuildPester` refuses to run under anything + older. Pester 5.x is no longer supported. As with the psake floor, the lower + minimum asserted support nothing verified — CI runs Pester 6 and nothing + exercised a 5.x consumer. Pester 6 keeps the v5 `Should -Be` assertions + (`Should.DisableV5` defaults to `$false`), so most suites need no changes; the + one behavioral change to watch is `Run.FailOnNullOrEmptyForEach`, which now + defaults to `$true`. Also fixes a guard that could never work: + `Test-PSBuildPester` declared a `5.0.0` minimum while calling + `New-PesterConfiguration` and `Run.SkipRemainingOnFailure`, neither of which + exists in 5.0.0. See the + [v0.8 → v1.0 migration guide](docs/migration-v0.8-to-v1.0.md). + - [**#166**](https://github.com/psake/PowerShellBuild/issues/166) **Breaking:** the consumer psake floor in `RequiredModules` is raised from `4.9.0` to `5.0.4`. psake 4.x is no longer supported. `RequiredModules` is diff --git a/PowerShellBuild/PowerShellBuild.psd1 b/PowerShellBuild/PowerShellBuild.psd1 index a0b539b..3bb6ba1 100644 --- a/PowerShellBuild/PowerShellBuild.psd1 +++ b/PowerShellBuild/PowerShellBuild.psd1 @@ -10,7 +10,7 @@ CompatiblePSEditions = @('Desktop', 'Core') RequiredModules = @( @{ModuleName = 'BuildHelpers'; ModuleVersion = '2.0.16' } - @{ModuleName = 'Pester'; ModuleVersion = '5.6.1' } + @{ModuleName = 'Pester'; ModuleVersion = '6.0.0' } @{ModuleName = 'psake'; ModuleVersion = '5.0.4' } ) FunctionsToExport = @( diff --git a/PowerShellBuild/Public/Test-PSBuildPester.ps1 b/PowerShellBuild/Public/Test-PSBuildPester.ps1 index 5a3b293..33dfca3 100644 --- a/PowerShellBuild/Public/Test-PSBuildPester.ps1 +++ b/PowerShellBuild/Public/Test-PSBuildPester.ps1 @@ -72,9 +72,9 @@ function Test-PSBuildPester { # 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 + $loadedPester = Import-Module -Name Pester -MinimumVersion 6.0.0 -ErrorAction Stop -PassThru } - if ($loadedPester.Version -lt [version]'5.0.0') { + if ($loadedPester.Version -lt [version]'6.0.0') { throw ($LocalizedData.PesterVersionNotSupported -f $loadedPester.Version) } diff --git a/PowerShellBuild/en-US/Messages.psd1 b/PowerShellBuild/en-US/Messages.psd1 index 48c8516..0feb84b 100644 --- a/PowerShellBuild/en-US/Messages.psd1 +++ b/PowerShellBuild/en-US/Messages.psd1 @@ -16,7 +16,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. +PesterVersionNotSupported=Pester version [{0}] is loaded, but Test-PSBuildPester requires Pester 6.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}] diff --git a/build.ps1 b/build.ps1 index 4237efb..8529de6 100644 --- a/build.ps1 +++ b/build.ps1 @@ -42,9 +42,6 @@ if ($Bootstrap.IsPresent) { } 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) diff --git a/docs/migration-v0.8-to-v1.0.md b/docs/migration-v0.8-to-v1.0.md index 192d430..44be184 100644 --- a/docs/migration-v0.8-to-v1.0.md +++ b/docs/migration-v0.8-to-v1.0.md @@ -43,6 +43,8 @@ One line per break; follow the link for details and migration steps. drops, and convert orphaned documents by hand. - [psake 4.x is no longer supported; the floor is now 5.0.4](#psake-4x-is-no-longer-supported-the-floor-is-now-504) — psake users must upgrade to 5.0.4+; Invoke-Build users are unaffected. +- [Pester 5.x is no longer supported; the floor is now 6.0.0](#pester-5x-is-no-longer-supported-the-floor-is-now-600) + — Pester 6 keeps the `Should -Be` syntax, so most suites need no changes. > More entries will follow as the remaining Phase 2 work lands. @@ -656,6 +658,59 @@ tests that had not been running. If your Pester tests call Decision and evidence in [#166](https://github.com/psake/PowerShellBuild/issues/166). +### Pester 5.x is no longer supported; the floor is now 6.0.0 + +`RequiredModules` requires **Pester 6.0.0 or newer**, previously 5.6.1, and +`Test-PSBuildPester` refuses to run under anything older. + +`RequiredModules` is enforced when the module is imported, so with only +Pester 5.x installed, `Import-Module PowerShellBuild` fails outright. + +**Migration:** + +```powershell +Install-Module -Name Pester -MinimumVersion 6.0.0 -Repository PSGallery -SkipPublisherCheck +``` + +`-SkipPublisherCheck` is needed on Windows PowerShell, where an older +Microsoft-signed Pester ships in the box. + +**Detection:** `Import-Module PowerShellBuild` fails naming `Pester` and +version `6.0.0`. If a Pester 5.x is already loaded in the session when +`Test-PSBuildPester` runs, it throws instead: `Pester version [5.9.1] is +loaded, but Test-PSBuildPester requires Pester 6.0.0 or newer.` + +**What upgrading Pester costs you.** Less than a major version bump usually +implies, because Pester 6 kept the v5 assertion syntax: + +- **`Should -Be` and the rest of the v5 assertions still work.** Pester 6's + `Should.DisableV5` configuration option defaults to `$false`, so existing + tests keep running unchanged. You are not required to adopt the new + `Should-Be` family. +- **An empty or `$null` `-ForEach` now fails.** `Run.FailOnNullOrEmptyForEach` + defaults to `$true` in Pester 6, where Pester 5 skipped silently. If a + data-driven test is fed an empty collection, discovery now errors instead of + quietly generating no tests. That is usually a bug being surfaced — a test + that never ran and never said so — but it can turn a green suite red. + Set `-AllowNullOrEmptyForEach` on the specific `It`/`Context` that can + legitimately be empty. +- Pester 6 supports Windows PowerShell 5.1 and PowerShell 7.4+, matching + PowerShellBuild's own support floor, so it adds no engine constraint. + +**Why the floor moved.** The same reason as the psake floor: the declared +minimum should be what is actually tested. CI runs Pester 6 and nothing else +verifies a 5.x consumer, so 5.6.1 asserted support nothing proved. + +Two related defects are fixed in the same change. `Test-PSBuildPester` +declared a `5.0.0` minimum it could never honor — Pester 5.0.0 does not export +`New-PesterConfiguration` and has no `Run.SkipRemainingOnFailure`, both of +which the function calls — so the guard admitted versions that failed later +with a confusing `CommandNotFoundException`. And the exact pin in +`requirements.psd1` moved to the current release. + +Decision and evidence in +[#172](https://github.com/psake/PowerShellBuild/issues/172). + ## Adding an entry (for PR contributors) Every breaking-change PR that lands in v1.0.0 must add an entry here for diff --git a/instructions/repository-specific.instructions.md b/instructions/repository-specific.instructions.md index ad048f7..388e2d8 100644 --- a/instructions/repository-specific.instructions.md +++ b/instructions/repository-specific.instructions.md @@ -40,7 +40,7 @@ PowerShellBuild/ │ ├── build.properties.ps1 # $PSBPreference (canonical config hashtable) │ ├── psakeFile.ps1 # Tasks consumers import │ └── IB.tasks.ps1 # Invoke-Build entry (aliased as PowerShellBuild.IB.Tasks) -├── tests/ # Pester 5+ tests +├── tests/ # Pester 6+ tests │ └── TestModule/ # Sample module exercised by the test suite ├── build.ps1 # Main build entry point for THIS repo ├── build.settings.ps1 # Build settings for THIS repo's own psake build @@ -236,11 +236,11 @@ versions — and installed via **PSDepend** when `./build.ps1 -Bootstrap` runs: ## Testing -Tests live in `tests/` and run on **Pester 6.0.0** (pinned exactly in `requirements.psd1`; -see psake/PowerShellBuild#17 for the targeting decision). Write assertions with the classic -`Should -Be` syntax — valid in both Pester 5 and 6 — rather than the Pester 6-only `Should-*` -assertion family, so tests remain backportable. The shipped module's own Pester compatibility -floor (5.0.0 in `Test-PSBuildPester`) is a separate contract and is unchanged. +Tests live in `tests/` and run on **Pester 6.1.0** (pinned in `requirements.psd1`; see +psake/PowerShellBuild#17 for the targeting decision). Write assertions with the classic +`Should -Be` syntax rather than the Pester 6-only `Should-*` assertion family — Pester 6 +still accepts it, and it keeps the diff against existing tests small. The shipped module's +own floor is **Pester 6.0.0** as of psake/PowerShellBuild#172, matching what CI tests. - Always build the module before running Pester directly — running against source can produce incorrect results. Prefer `./build.ps1 -Task Test` over a raw `Invoke-Pester` call. diff --git a/requirements.pester-matrix.psd1 b/requirements.pester-matrix.psd1 deleted file mode 100644 index cd58532..0000000 --- a/requirements.pester-matrix.psd1 +++ /dev/null @@ -1,18 +0,0 @@ -# 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 - } - } -} diff --git a/requirements.psd1 b/requirements.psd1 index bc837ae..ce98f09 100755 --- a/requirements.psd1 +++ b/requirements.psd1 @@ -4,7 +4,7 @@ } BuildHelpers = '2.0.16' Pester = @{ - Version = '6.0.0' + Version = '6.1.0' Parameters = @{ SkipPublisherCheck = $true } diff --git a/tests/Test-PSBuildPester.tests.ps1 b/tests/Test-PSBuildPester.tests.ps1 index 7a6c56e..43ccb4f 100644 --- a/tests/Test-PSBuildPester.tests.ps1 +++ b/tests/Test-PSBuildPester.tests.ps1 @@ -1,21 +1,23 @@ # 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 job runner itself lives in -# fixtures/FixtureHelpers.psm1, shared with the other test files that need a fresh session. +# invocation runs in a Start-Job subprocess, which keeps the inner run's Pester state out of the +# outer framework's. The job runner lives in fixtures/FixtureHelpers.psm1, shared with the other +# test files that need a fresh session. +# +# This was a two-major matrix until #172 raised the floor to Pester 6. It still discovers the +# version rather than hardcoding one, so the loop below is what would grow back if a second +# supported major ever returned. # # 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. + # Newest installed Pester of each supported major. Pester 6 is the only supported major + # since #172; an absent one simply produces no matrix legs, which fails loudly below rather + # than passing with nothing run. $script:innerPesterVersions = @( - foreach ($majorVersion in 5, 6) { + foreach ($majorVersion in 6) { $newestOfMajor = Get-Module -Name 'Pester' -ListAvailable | Where-Object { $_.Version.Major -eq $majorVersion } | Sort-Object -Property 'Version' -Descending | @@ -25,6 +27,75 @@ BeforeDiscovery { } } ) + if ($script:innerPesterVersions.Count -eq 0) { + throw 'No supported Pester major (6.x) is installed; Test-PSBuildPester cannot be verified.' + } +} + +Describe 'Pester version floor' { + + # The floor is stated in three places: the manifest's RequiredModules, and twice inside + # Test-PSBuildPester. They must agree, and for months they did not -- the manifest said + # 5.6.1 while the function said 5.0.0, a version that cannot run the function at all + # because Pester 5.0.0 has no New-PesterConfiguration (psake/PowerShellBuild#172). + # + # The function deliberately keeps a literal rather than reading the manifest at runtime. + # Reading it would introduce a lookup that returns nothing when the module is loaded from + # an unusual path, and a floor of $null makes "$version -lt $null" false -- the guard would + # vanish silently. This test enforces the agreement at the only moment anyone can act on + # it: when the drift is introduced. + + BeforeAll { + $script:moduleRoot = Split-Path -Path $PSScriptRoot -Parent + $script:sourceManifestPath = [IO.Path]::Combine( + $script:moduleRoot, 'PowerShellBuild', 'PowerShellBuild.psd1' + ) + $script:sourceFunctionPath = [IO.Path]::Combine( + $script:moduleRoot, 'PowerShellBuild', 'Public', 'Test-PSBuildPester.ps1' + ) + + $script:declaredFloor = ( + (Import-PowerShellDataFile -Path $script:sourceManifestPath).RequiredModules | + Where-Object { $_.ModuleName -eq 'Pester' } + ).ModuleVersion + + # Both forms the floor takes in the function: the -MinimumVersion argument on the + # import, and the [version] literal in the comparison. + $functionSource = Get-Content -Path $script:sourceFunctionPath -Raw + $script:guardVersion = @( + [regex]::Matches($functionSource, "-MinimumVersion\s+(?\d+\.\d+\.\d+)") + + [regex]::Matches($functionSource, "\[version\]'(?\d+\.\d+\.\d+)'") + ).ForEach({ $_.Groups['version'].Value }) + } + + It 'declares a Pester floor in the module manifest' { + $script:declaredFloor | Should -Not -BeNullOrEmpty + } + + It 'states the floor in Test-PSBuildPester at least twice' { + # Guards the test itself: if the function is refactored so neither pattern matches, + # an empty set would otherwise satisfy the comparison below and prove nothing. + $script:guardVersion.Count | Should -BeGreaterOrEqual 2 + } + + It 'uses the manifest floor everywhere the function states it' { + foreach ($version in $script:guardVersion) { + $version | Should -Be $script:declaredFloor + } + } + + It 'names the same floor in the message the guard throws' { + # Read as text rather than with Import-PowerShellDataFile: Messages.psd1 is a + # ConvertFrom-StringData document, not a hashtable literal. + $messagesPath = [IO.Path]::Combine( + $script:moduleRoot, 'PowerShellBuild', 'en-US', 'Messages.psd1' + ) + $messageLine = Get-Content -Path $messagesPath | + Where-Object { $_ -like 'PesterVersionNotSupported=*' } + + $messageLine | Should -Not -BeNullOrEmpty + $messageLine | Should -Match ([regex]::Escape($script:declaredFloor)) + } } Describe 'Test-PSBuildPester' { @@ -235,6 +306,13 @@ Describe 'Coverage target' { # 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. + # + # This skips in CI as of #172. It needs two installed Pester versions to tell + # "used the loaded one" apart from "imported the newest", and dropping the 5.x + # matrix left exactly one. It still runs on a developer machine with more than one + # Pester installed. Restoring it in CI would mean installing a second version that + # is never imported -- the machinery #172 deleted -- so it is left skipped + # deliberately rather than by oversight. $result = Invoke-TestPSBuildPesterInJob -ModulePath $script:builtModulePath -InnerPesterVersion $script:oldestInnerVersion -Path $script:healthyPath $result.Threw | Should -BeFalse