From 3194e7d0105adf84c9a8ead3587cf1e851c3531b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 22:53:26 +0000 Subject: [PATCH 1/2] test: Add PSBuildTestFixture shared fixture module Add the minimal fixture module that the integration-test sub-tasks of #17 (#98-#103) build on, per the design recorded on #97: - tests/fixtures/PSBuildTestFixture/: valid manifest, dot-sourcing psm1, two public functions with complete comment-based help (no .LINK URLs, so no test makes a network call), one private helper, and an excludeme.txt for Build.Exclude assertions. Inert data only: no build files, no requirements manifest, no nested test suite. - tests/fixtures/FixtureHelpers.psm1: Copy-PSBuildTestFixture copies the fixture into a destination (typically $TestDrive) so consuming tests never mutate the checked-in fixture. - tests/Fixtures.tests.ps1: smoke tests covering the copy helper, manifest validity, import, exports, behavior, and help completeness. The repository Pester run collects only top-level tests/*.tests.ps1 files, so nothing under tests/fixtures/ can be discovered as a test. Closes #97 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011semwa5HU1BUKKL4RoWjKa --- tests/Fixtures.tests.ps1 | 75 +++++++++++++++++++ tests/fixtures/FixtureHelpers.psm1 | 36 +++++++++ .../PSBuildTestFixture.psd1 | 22 ++++++ .../PSBuildTestFixture.psm1 | 12 +++ .../Private/Test-WidgetName.ps1 | 28 +++++++ .../PSBuildTestFixture/Public/Get-Widget.ps1 | 46 ++++++++++++ .../PSBuildTestFixture/Public/Set-Widget.ps1 | 44 +++++++++++ .../fixtures/PSBuildTestFixture/excludeme.txt | 3 + 8 files changed, 266 insertions(+) create mode 100644 tests/Fixtures.tests.ps1 create mode 100644 tests/fixtures/FixtureHelpers.psm1 create mode 100644 tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psd1 create mode 100644 tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psm1 create mode 100644 tests/fixtures/PSBuildTestFixture/Private/Test-WidgetName.ps1 create mode 100644 tests/fixtures/PSBuildTestFixture/Public/Get-Widget.ps1 create mode 100644 tests/fixtures/PSBuildTestFixture/Public/Set-Widget.ps1 create mode 100644 tests/fixtures/PSBuildTestFixture/excludeme.txt diff --git a/tests/Fixtures.tests.ps1 b/tests/Fixtures.tests.ps1 new file mode 100644 index 0000000..f09bcbe --- /dev/null +++ b/tests/Fixtures.tests.ps1 @@ -0,0 +1,75 @@ +BeforeAll { + Import-Module -Name ([IO.Path]::Combine($PSScriptRoot, 'fixtures', 'FixtureHelpers.psm1')) -Force +} + +AfterAll { + Remove-Module -Name 'FixtureHelpers' -Force -ErrorAction SilentlyContinue +} + +Describe 'PSBuildTestFixture' { + + Context 'Copy helper' { + + It 'Copies the fixture into the destination and returns the copy path' { + $fixturePath = Copy-PSBuildTestFixture -Destination $TestDrive + + $fixturePath | Should -Be (Join-Path -Path $TestDrive -ChildPath 'PSBuildTestFixture') + $fixturePath | Should -Exist + } + + It 'Copies the complete fixture layout' { + $fixturePath = Copy-PSBuildTestFixture -Destination (Join-Path -Path $TestDrive -ChildPath 'layout') + + Join-Path -Path $fixturePath -ChildPath 'PSBuildTestFixture.psd1' | Should -Exist + Join-Path -Path $fixturePath -ChildPath 'PSBuildTestFixture.psm1' | Should -Exist + Join-Path -Path $fixturePath -ChildPath 'Public/Get-Widget.ps1' | Should -Exist + Join-Path -Path $fixturePath -ChildPath 'Public/Set-Widget.ps1' | Should -Exist + Join-Path -Path $fixturePath -ChildPath 'Private/Test-WidgetName.ps1' | Should -Exist + Join-Path -Path $fixturePath -ChildPath 'excludeme.txt' | Should -Exist + } + } + + Context 'Fixture module' { + + BeforeAll { + $script:fixturePath = Copy-PSBuildTestFixture -Destination (Join-Path -Path $TestDrive -ChildPath 'module') + $script:fixtureManifestPath = Join-Path -Path $script:fixturePath -ChildPath 'PSBuildTestFixture.psd1' + } + + AfterAll { + Remove-Module -Name 'PSBuildTestFixture' -Force -ErrorAction SilentlyContinue + } + + It 'Has a valid module manifest' { + { Test-ModuleManifest -Path $script:fixtureManifestPath -ErrorAction Stop } | Should -Not -Throw + } + + It 'Imports from a test drive copy' { + { Import-Module -Name $script:fixtureManifestPath -Force -ErrorAction Stop } | Should -Not -Throw + } + + It 'Exports exactly the two public functions' { + $exportedCommands = (Get-Module -Name 'PSBuildTestFixture').ExportedFunctions.Keys | Sort-Object + $exportedCommands | Should -Be @('Get-Widget', 'Set-Widget') + } + + It 'Does not export the private helper' { + (Get-Module -Name 'PSBuildTestFixture').ExportedFunctions.Keys | Should -Not -Contain 'Test-WidgetName' + } + + It 'Public functions work end to end' { + $widget = Get-Widget -Name 'Sprocket' -Quantity 5 + + $widget.Name | Should -Be 'Sprocket' + $widget.Quantity | Should -Be 5 + } + + It 'Public functions have complete comment-based help' -ForEach @('Get-Widget', 'Set-Widget') { + $help = Get-Help -Name $_ -Full + + $help.Synopsis | Should -Not -BeNullOrEmpty + $help.Description | Should -Not -BeNullOrEmpty + $help.Examples.Example.Count | Should -BeGreaterOrEqual 1 + } + } +} diff --git a/tests/fixtures/FixtureHelpers.psm1 b/tests/fixtures/FixtureHelpers.psm1 new file mode 100644 index 0000000..068a65d --- /dev/null +++ b/tests/fixtures/FixtureHelpers.psm1 @@ -0,0 +1,36 @@ +function Copy-PSBuildTestFixture { + <# + .SYNOPSIS + Copy the PSBuildTestFixture module to a destination directory. + .DESCRIPTION + Copies the checked-in PSBuildTestFixture module into the supplied destination (typically + $TestDrive) and returns the path of the copy. Tests must always operate on a copy so that + no test run ever mutates the checked-in fixture and every test starts from a pristine + fixture state. + .PARAMETER Destination + Directory to copy the fixture into. The copy is created as a PSBuildTestFixture + subdirectory of this path. + .EXAMPLE + PS> $fixturePath = Copy-PSBuildTestFixture -Destination $TestDrive + + Copies the fixture module into the Pester test drive and returns the path of the copy. + .OUTPUTS + System.String + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $Destination + ) + + $fixtureSourcePath = Join-Path -Path $PSScriptRoot -ChildPath 'PSBuildTestFixture' + $fixtureCopyPath = Join-Path -Path $Destination -ChildPath 'PSBuildTestFixture' + + Copy-Item -Path $fixtureSourcePath -Destination $fixtureCopyPath -Recurse -Force + $fixtureCopyPath +} + +Export-ModuleMember -Function 'Copy-PSBuildTestFixture' diff --git a/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psd1 b/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psd1 new file mode 100644 index 0000000..587d7da --- /dev/null +++ b/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psd1 @@ -0,0 +1,22 @@ +@{ + RootModule = 'PSBuildTestFixture.psm1' + ModuleVersion = '0.1.0' + GUID = '1bc197fe-a274-4d34-965d-862f9a8fe7b7' + Author = 'psake contributors' + CompanyName = 'Community' + Copyright = '(c) psake contributors. All rights reserved.' + Description = 'Minimal fixture module consumed by the PowerShellBuild integration tests. Not published.' + PowerShellVersion = '5.1' + FunctionsToExport = @( + 'Get-Widget' + 'Set-Widget' + ) + CmdletsToExport = @() + VariablesToExport = @() + AliasesToExport = @() + PrivateData = @{ + PSData = @{ + Tags = @('PowerShellBuild', 'TestFixture') + } + } +} diff --git a/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psm1 b/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psm1 new file mode 100644 index 0000000..8f6c6cd --- /dev/null +++ b/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psm1 @@ -0,0 +1,12 @@ +# Dot source public and private functions +$private = @(Get-ChildItem -Path ([IO.Path]::Combine($PSScriptRoot, 'Private/*.ps1')) -Recurse) +$public = @(Get-ChildItem -Path ([IO.Path]::Combine($PSScriptRoot, 'Public/*.ps1')) -Recurse) +foreach ($import in $public + $private) { + try { + . $import.FullName + } catch { + throw "Unable to dot source [$($import.FullName)]" + } +} + +Export-ModuleMember -Function $public.Basename diff --git a/tests/fixtures/PSBuildTestFixture/Private/Test-WidgetName.ps1 b/tests/fixtures/PSBuildTestFixture/Private/Test-WidgetName.ps1 new file mode 100644 index 0000000..bb83518 --- /dev/null +++ b/tests/fixtures/PSBuildTestFixture/Private/Test-WidgetName.ps1 @@ -0,0 +1,28 @@ +function Test-WidgetName { + <# + .SYNOPSIS + Validate a widget name. + .DESCRIPTION + Returns $true when the widget name contains only letters, digits, and hyphens. This private + helper exists so the integration tests can assert that private functions are dot-sourced + into compiled output but never exported. + .PARAMETER Name + Widget name to validate. + .EXAMPLE + PS> Test-WidgetName -Name 'Sprocket' + + Returns $true. + .OUTPUTS + System.Boolean + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $Name + ) + + $Name -match '^[A-Za-z0-9-]+$' +} diff --git a/tests/fixtures/PSBuildTestFixture/Public/Get-Widget.ps1 b/tests/fixtures/PSBuildTestFixture/Public/Get-Widget.ps1 new file mode 100644 index 0000000..b6e14d9 --- /dev/null +++ b/tests/fixtures/PSBuildTestFixture/Public/Get-Widget.ps1 @@ -0,0 +1,46 @@ +function Get-Widget { + <# + .SYNOPSIS + Get a widget by name. + .DESCRIPTION + Returns a widget object with the requested name and quantity. This function exists only to + give the PowerShellBuild integration tests a public command with complete comment-based + help, typed parameters, and a private helper dependency. + .PARAMETER Name + Name of the widget to return. + .PARAMETER Quantity + Number of widget units to report on the returned object. Defaults to 1. + .EXAMPLE + PS> Get-Widget -Name 'Sprocket' + + Returns a widget object named Sprocket with a quantity of 1. + .EXAMPLE + PS> Get-Widget -Name 'Sprocket' -Quantity 5 + + Returns a widget object named Sprocket with a quantity of 5. + .OUTPUTS + System.Management.Automation.PSCustomObject + #> + [CmdletBinding()] + [OutputType([PSCustomObject])] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $Name, + + [Parameter()] + [ValidateRange(1, 1000)] + [int] + $Quantity = 1 + ) + + if (-not (Test-WidgetName -Name $Name)) { + throw "Invalid widget name [$Name]" + } + + [PSCustomObject]@{ + Name = $Name + Quantity = $Quantity + } +} diff --git a/tests/fixtures/PSBuildTestFixture/Public/Set-Widget.ps1 b/tests/fixtures/PSBuildTestFixture/Public/Set-Widget.ps1 new file mode 100644 index 0000000..65eb979 --- /dev/null +++ b/tests/fixtures/PSBuildTestFixture/Public/Set-Widget.ps1 @@ -0,0 +1,44 @@ +function Set-Widget { + <# + .SYNOPSIS + Set the quantity of a widget. + .DESCRIPTION + Returns a widget object with the supplied name and quantity, as if the widget had been + updated. This function exists only to give the PowerShellBuild integration tests a second + public command, including one that supports ShouldProcess. + .PARAMETER Name + Name of the widget to update. + .PARAMETER Quantity + New number of widget units. + .EXAMPLE + PS> Set-Widget -Name 'Sprocket' -Quantity 3 + + Sets the Sprocket widget quantity to 3 and returns the updated widget object. + .OUTPUTS + System.Management.Automation.PSCustomObject + #> + [CmdletBinding(SupportsShouldProcess)] + [OutputType([PSCustomObject])] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $Name, + + [Parameter(Mandatory)] + [ValidateRange(1, 1000)] + [int] + $Quantity + ) + + if (-not (Test-WidgetName -Name $Name)) { + throw "Invalid widget name [$Name]" + } + + if ($PSCmdlet.ShouldProcess($Name, 'Set widget quantity')) { + [PSCustomObject]@{ + Name = $Name + Quantity = $Quantity + } + } +} diff --git a/tests/fixtures/PSBuildTestFixture/excludeme.txt b/tests/fixtures/PSBuildTestFixture/excludeme.txt new file mode 100644 index 0000000..40c7d84 --- /dev/null +++ b/tests/fixtures/PSBuildTestFixture/excludeme.txt @@ -0,0 +1,3 @@ +=== EXCLUDE ME === +This file exists so integration tests can assert that $PSBPreference.Build.Exclude +patterns keep matching files out of the build output. From 9166a167c981f3725a4dd4e5b7511e6498b1a8fd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 22:59:16 +0000 Subject: [PATCH 2/2] test: Address review feedback and Windows PowerShell 5.1 help quirk - Copy-PSBuildTestFixture now removes any existing copy and copies the fixture contents into a freshly created directory, so recopying into the same destination replaces the copy instead of nesting a second PSBuildTestFixture directory, and not-yet-created destinations work. Adds a regression test for the recopy case. - The fixture psm1 dot-sourcing error now includes the underlying exception message, not only the file path. - The help smoke test counts examples through an array subexpression: on Windows PowerShell 5.1 a single help example is a bare PSObject whose .Count is $null, which failed the CI 5.1 leg for Set-Widget. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011semwa5HU1BUKKL4RoWjKa --- tests/Fixtures.tests.ps1 | 14 +++++++++++++- tests/fixtures/FixtureHelpers.psm1 | 11 ++++++++++- .../PSBuildTestFixture/PSBuildTestFixture.psm1 | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/tests/Fixtures.tests.ps1 b/tests/Fixtures.tests.ps1 index f09bcbe..84fbec6 100644 --- a/tests/Fixtures.tests.ps1 +++ b/tests/Fixtures.tests.ps1 @@ -27,6 +27,16 @@ Describe 'PSBuildTestFixture' { Join-Path -Path $fixturePath -ChildPath 'Private/Test-WidgetName.ps1' | Should -Exist Join-Path -Path $fixturePath -ChildPath 'excludeme.txt' | Should -Exist } + + It 'Recopying into the same destination replaces the copy without nesting' { + $destination = Join-Path -Path $TestDrive -ChildPath 'recopy' + $firstCopyPath = Copy-PSBuildTestFixture -Destination $destination + $secondCopyPath = Copy-PSBuildTestFixture -Destination $destination + + $secondCopyPath | Should -Be $firstCopyPath + Join-Path -Path $secondCopyPath -ChildPath 'PSBuildTestFixture.psd1' | Should -Exist + Join-Path -Path $secondCopyPath -ChildPath 'PSBuildTestFixture' | Should -Not -Exist + } } Context 'Fixture module' { @@ -69,7 +79,9 @@ Describe 'PSBuildTestFixture' { $help.Synopsis | Should -Not -BeNullOrEmpty $help.Description | Should -Not -BeNullOrEmpty - $help.Examples.Example.Count | Should -BeGreaterOrEqual 1 + # The array subexpression matters: on Windows PowerShell 5.1 a single help + # example is a bare PSObject whose .Count is $null. + @($help.Examples.Example).Count | Should -BeGreaterOrEqual 1 } } } diff --git a/tests/fixtures/FixtureHelpers.psm1 b/tests/fixtures/FixtureHelpers.psm1 index 068a65d..f43e398 100644 --- a/tests/fixtures/FixtureHelpers.psm1 +++ b/tests/fixtures/FixtureHelpers.psm1 @@ -29,7 +29,16 @@ function Copy-PSBuildTestFixture { $fixtureSourcePath = Join-Path -Path $PSScriptRoot -ChildPath 'PSBuildTestFixture' $fixtureCopyPath = Join-Path -Path $Destination -ChildPath 'PSBuildTestFixture' - Copy-Item -Path $fixtureSourcePath -Destination $fixtureCopyPath -Recurse -Force + # Remove any previous copy and recreate the directory before copying the fixture + # contents. Copying into an existing directory would otherwise nest a second + # PSBuildTestFixture directory inside it, and the destination (or its parents) + # may not exist yet. + if (Test-Path -Path $fixtureCopyPath) { + Remove-Item -Path $fixtureCopyPath -Recurse -Force + } + New-Item -Path $fixtureCopyPath -ItemType Directory -Force > $null + Copy-Item -Path (Join-Path -Path $fixtureSourcePath -ChildPath '*') -Destination $fixtureCopyPath -Recurse -Force + $fixtureCopyPath } diff --git a/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psm1 b/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psm1 index 8f6c6cd..b9bbbeb 100644 --- a/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psm1 +++ b/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psm1 @@ -5,7 +5,7 @@ foreach ($import in $public + $private) { try { . $import.FullName } catch { - throw "Unable to dot source [$($import.FullName)]" + throw "Unable to dot source [$($import.FullName)]: $($_.Exception.Message)" } }