From 74d07fcf3948abf4af1570cb35aace0205e33974 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Tue, 22 Jul 2025 16:47:29 -0700 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20=E2=9C=A8=20Add=20localization=20?= =?UTF-8?q?support=20and=20improve=20error=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introduced `Messages.psd1` for localized messages. * Updated functions to utilize localized messages for warnings and errors. * Improved formatting and readability in several functions. * Ensured consistent use of `[CmdletBinding()]` across all functions. --- PowerShellBuild/PowerShellBuild.psm1 | 10 +- .../Public/Build-PSBuildMAMLHelp.ps1 | 4 +- .../Public/Build-PSBuildMarkdown.ps1 | 6 +- .../Public/Build-PSBuildModule.ps1 | 115 +++++++++++++----- .../Public/Build-PSBuildUpdatableHelp.ps1 | 33 +++-- .../Public/Clear-PSBuildOutputFolder.ps1 | 10 +- PowerShellBuild/Public/Initialize-PSBuild.ps1 | 41 +++++-- .../Public/Publish-PSBuildModule.ps1 | 22 ++-- PowerShellBuild/Public/Test-PSBuildPester.ps1 | 50 ++++---- .../Public/Test-PSBuildScriptAnalysis.ps1 | 30 ++--- PowerShellBuild/en-US/Messages.psd1 | 26 ++++ 11 files changed, 231 insertions(+), 116 deletions(-) create mode 100644 PowerShellBuild/en-US/Messages.psd1 diff --git a/PowerShellBuild/PowerShellBuild.psm1 b/PowerShellBuild/PowerShellBuild.psm1 index 1726b41..e6b2e9d 100644 --- a/PowerShellBuild/PowerShellBuild.psm1 +++ b/PowerShellBuild/PowerShellBuild.psm1 @@ -1,6 +1,6 @@ # Dot source public functions $private = @(Get-ChildItem -Path ([IO.Path]::Combine($PSScriptRoot, 'Private/*.ps1')) -Recurse) -$public = @(Get-ChildItem -Path ([IO.Path]::Combine($PSScriptRoot, 'Public/*.ps1')) -Recurse) +$public = @(Get-ChildItem -Path ([IO.Path]::Combine($PSScriptRoot, 'Public/*.ps1')) -Recurse) foreach ($import in $public + $private) { try { . $import.FullName @@ -9,6 +9,14 @@ foreach ($import in $public + $private) { } } +$importLocalizedDataSplat = @{ + BindingVariable = 'LocalizedData' + FileName = 'Messages.psd1' + ErrorAction = 'SilentlyContinue' +} +Import-LocalizedData @importLocalizedDataSplat + + Export-ModuleMember -Function $public.Basename # $psakeTaskAlias = 'PowerShellBuild.psake.tasks' diff --git a/PowerShellBuild/Public/Build-PSBuildMAMLHelp.ps1 b/PowerShellBuild/Public/Build-PSBuildMAMLHelp.ps1 index d1fd5b1..f5ae020 100644 --- a/PowerShellBuild/Public/Build-PSBuildMAMLHelp.ps1 +++ b/PowerShellBuild/Public/Build-PSBuildMAMLHelp.ps1 @@ -14,7 +14,7 @@ function Build-PSBuildMAMLHelp { Uses PlatyPS to generate MAML XML help from markdown files in ./docs and saves the XML file to a directory under ./output/MyModule #> - [cmdletbinding()] + [CmdletBinding()] param( [parameter(Mandatory)] [string]$Path, @@ -23,7 +23,7 @@ function Build-PSBuildMAMLHelp { [string]$DestinationPath ) - $helpLocales = (Get-ChildItem -Path $Path -Directory).Name + $helpLoc2ales = (Get-ChildItem -Path $Path -Directory).Name # Generate the module's primary MAML help file foreach ($locale in $helpLocales) { diff --git a/PowerShellBuild/Public/Build-PSBuildMarkdown.ps1 b/PowerShellBuild/Public/Build-PSBuildMarkdown.ps1 index ec2a90a..9010543 100644 --- a/PowerShellBuild/Public/Build-PSBuildMarkdown.ps1 +++ b/PowerShellBuild/Public/Build-PSBuildMarkdown.ps1 @@ -25,7 +25,7 @@ function Build-PSBuildMarkdown { Analysis the comment-based help of the MyModule module and create markdown documents under ./docs/en-US. #> - [cmdletbinding()] + [CmdletBinding()] param( [parameter(Mandatory)] [string]$ModulePath, @@ -56,7 +56,7 @@ function Build-PSBuildMarkdown { try { if ($moduleInfo.ExportedCommands.Count -eq 0) { - Write-Warning 'No commands have been exported. Skipping markdown generation.' + Write-Warning $LocalizedData.NoCommandsExported return } @@ -93,7 +93,7 @@ function Build-PSBuildMarkdown { } New-MarkdownHelp @newMDParams > $null } catch { - Write-Error "Failed to generate markdown help. : $_" + Write-Error ($LocalizedData.FailedToGenerateMarkdownHelp -f $_) } finally { Remove-Module $moduleName } diff --git a/PowerShellBuild/Public/Build-PSBuildModule.ps1 b/PowerShellBuild/Public/Build-PSBuildModule.ps1 index e4d99e6..e8373d1 100644 --- a/PowerShellBuild/Public/Build-PSBuildModule.ps1 +++ b/PowerShellBuild/Public/Build-PSBuildModule.ps1 @@ -1,10 +1,12 @@ +# spell-checker:ignore modulename function Build-PSBuildModule { <# .SYNOPSIS Builds a PowerShell module based on source directory .DESCRIPTION - Builds a PowerShell module based on source directory and optionally concatenates - public/private functions from separete files into monolithic .PSM1 file. + Builds a PowerShell module based on source directory and optionally + concatenates public/private functions from separate files into + monolithic .PSM1 file. .PARAMETER Path The source module path. .PARAMETER DestinationPath @@ -12,7 +14,8 @@ function Build-PSBuildModule { .PARAMETER ModuleName The name of the module. .PARAMETER Compile - Switch to indicate if separete function files should be concatenated into monolithic .PSM1 file. + Switch to indicate if separate function files should be concatenated + into monolithic .PSM1 file. .PARAMETER CompileHeader String that will be at the top of your PSM1 file. .PARAMETER CompileFooter @@ -20,19 +23,23 @@ function Build-PSBuildModule { .PARAMETER CompileScriptHeader String that will be added to your PSM1 file before each script file. .PARAMETER CompileScriptFooter - String that will be added to your PSM1 file beforeafter each script file. + String that will be added to your PSM1 file after each script file. .PARAMETER ReadMePath - Path to project README. If present, this will become the "about_.help.txt" file in the build module. + Path to project README. If present, this will become the + "about_.help.txt" file in the build module. .PARAMETER CompileDirectories - List of directories containing .ps1 files that will also be compiled into the PSM1. + List of directories containing .ps1 files that will also be compiled + into the PSM1. .PARAMETER CopyDirectories List of directories that will copying "as-is" into the build module. .PARAMETER Exclude - Array of files (regular expressions) to exclude from copying into built module. + Array of files (regular expressions) to exclude from copying into built + module. .PARAMETER Culture - UI Culture. This is used to determine what culture directory to store "about_.help.txt" in. + UI Culture. This is used to determine what culture directory to store + "about_.help.txt" in. .EXAMPLE - PS> $buildParams = @{ + $buildParams = @{ Path = ./MyModule DestinationPath = ./Output/MoModule/0.1.0 ModuleName = MyModule @@ -40,11 +47,12 @@ function Build-PSBuildModule { Compile = $false Culture = (Get-UICulture).Name } - PS> Build-PSBuildModule @buildParams + Build-PSBuildModule @buildParams - Build module from source directory './MyModule' and save to '/Output/MoModule/0.1.0' + Build module from source directory './MyModule' and save to + '/Output/MoModule/0.1.0' #> - [cmdletbinding()] + [CmdletBinding()] param( [parameter(Mandatory)] [string]$Path, @@ -77,11 +85,22 @@ function Build-PSBuildModule { ) if (-not (Test-Path -LiteralPath $DestinationPath)) { - New-Item -Path $DestinationPath -ItemType Directory -Verbose:$VerbosePreference > $null + $newItemSplat = @{ + Path = $DestinationPath + ItemType = 'Directory' + Verbose = $VerbosePreference + } + New-Item @newItemSplat > $null } # Copy "non-processed files" - Get-ChildItem -Path $Path -Include '*.psm1', '*.psd1', '*.ps1xml' -Depth 1 | Copy-Item -Destination $DestinationPath -Force + $getChildItemSplat = @{ + Path = $Path + Include = '*.psm1', '*.psd1', '*.ps1xml' + Depth = 1 + } + Get-ChildItem @getChildItemSplat | + Copy-Item -Destination $DestinationPath -Force foreach ($dir in $CopyDirectories) { $copyPath = [IO.Path]::Combine($Path, $dir) Copy-Item -Path $copyPath -Destination $DestinationPath -Recurse -Force @@ -89,37 +108,57 @@ function Build-PSBuildModule { # Copy README as about_.help.txt if (-not [string]::IsNullOrEmpty($ReadMePath)) { - $culturePath = [IO.Path]::Combine($DestinationPath, $Culture) - $aboutModulePath = [IO.Path]::Combine($culturePath, "about_$($ModuleName).help.txt") - if(-not (Test-Path $culturePath -PathType Container)) { + $culturePath = [IO.Path]::Combine($DestinationPath, $Culture) + $aboutModulePath = [IO.Path]::Combine( + $culturePath, + "about_$($ModuleName).help.txt" + ) + if (-not (Test-Path $culturePath -PathType Container)) { New-Item $culturePath -Type Directory -Force > $null - Copy-Item -LiteralPath $ReadMePath -Destination $aboutModulePath -Force + $copyItemSplat = @{ + LiteralPath = $ReadMePath + Destination = $aboutModulePath + Force = $true + } + Copy-Item @copyItemSplat } } - # Copy source files to destination and optionally combine *.ps1 files into the PSM1 + # Copy source files to destination and optionally combine *.ps1 files + # into the PSM1 if ($Compile.IsPresent) { $rootModule = [IO.Path]::Combine($DestinationPath, "$ModuleName.psm1") # Grab the contents of the copied over PSM1 # This will be appended to the end of the finished PSM1 $psm1Contents = Get-Content -Path $rootModule -Raw - '' | Out-File -FilePath $rootModule -Encoding utf8 + '' | Out-File -FilePath $rootModule -Encoding 'utf8' if ($CompileHeader) { - $CompileHeader | Add-Content -Path $rootModule -Encoding utf8 + $CompileHeader | Add-Content -Path $rootModule -Encoding 'utf8' } $resolvedCompileDirectories = $CompileDirectories | ForEach-Object { [IO.Path]::Combine($Path, $_) } - $allScripts = Get-ChildItem -Path $resolvedCompileDirectories -Filter '*.ps1' -File -Recurse -ErrorAction SilentlyContinue + $getChildItemSplat = @{ + Path = $resolvedCompileDirectories + Filter = '*.ps1' + File = $true + Recurse = $true + ErrorAction = 'SilentlyContinue' + } + $allScripts = Get-ChildItem @getChildItemSplat $allScripts = $allScripts | Remove-ExcludedItem -Exclude $Exclude + $addContentSplat = @{ + Path = $rootModule + Encoding = 'utf8' + } $allScripts | ForEach-Object { $srcFile = Resolve-Path $_.FullName -Relative - Write-Verbose "Adding [$srcFile] to PSM1" + Write-Verbose ($LocalizedData.AddingFileToPsm1 -f $srcFile) if ($CompileScriptHeader) { Write-Output $CompileScriptHeader @@ -130,15 +169,14 @@ function Build-PSBuildModule { if ($CompileScriptFooter) { Write-Output $CompileScriptFooter } + } | Add-Content @addContentSplat - } | Add-Content -Path $rootModule -Encoding utf8 - - $psm1Contents | Add-Content -Path $rootModule -Encoding utf8 + $psm1Contents | Add-Content @addContentSplat if ($CompileFooter) { - $CompileFooter | Add-Content -Path $rootModule -Encoding utf8 + $CompileFooter | Add-Content @addContentSplat } - } else{ + } else { # Copy everything over, then remove stuff that should have been excluded # It's just easier this way $copyParams = @{ @@ -157,13 +195,26 @@ function Build-PSBuildModule { } } } - $toRemove | Remove-Item -Recurse -Force -ErrorAction Ignore + $toRemove | Remove-Item -Recurse -Force -ErrorAction 'Ignore' } # Export public functions in manifest if there are any public functions - $publicFunctions = Get-ChildItem $Path/Public/*.ps1 -Recurse -ErrorAction SilentlyContinue + $getChildItemSplat = @{ + Recurse = $true + ErrorAction = 'SilentlyContinue' + Path = "$Path/Public/*.ps1" + } + $publicFunctions = Get-ChildItem @getChildItemSplat if ($publicFunctions) { - $outputManifest = [IO.Path]::Combine($DestinationPath, "$ModuleName.psd1") - Update-Metadata -Path $OutputManifest -PropertyName FunctionsToExport -Value $publicFunctions.BaseName + $outputManifest = [IO.Path]::Combine( + $DestinationPath, + "$ModuleName.psd1" + ) + $updateMetadataSplat = @{ + Path = $OutputManifest + PropertyName = 'FunctionsToExport' + Value = $publicFunctions.BaseName + } + Update-Metadata @updateMetadataSplat } } diff --git a/PowerShellBuild/Public/Build-PSBuildUpdatableHelp.ps1 b/PowerShellBuild/Public/Build-PSBuildUpdatableHelp.ps1 index 9b8a404..de784eb 100644 --- a/PowerShellBuild/Public/Build-PSBuildUpdatableHelp.ps1 +++ b/PowerShellBuild/Public/Build-PSBuildUpdatableHelp.ps1 @@ -9,13 +9,14 @@ function Build-PSBuildUpdatableHelp { .PARAMETER OutputPath Path to create updatable help .cab file in. .PARAMETER Module - Name of the module to create a .cab file for. Defaults to the $ModuleName variable from the parent scope. + Name of the module to create a .cab file for. Defaults to the + $ModuleName variable from the parent scope. .EXAMPLE PS> Build-PSBuildUpdatableHelp -DocsPath ./docs -OutputPath ./Output/UpdatableHelp Create help .cab file based on PlatyPS markdown help. #> - [cmdletbinding()] + [CmdletBinding()] param( [parameter(Mandatory)] [string]$DocsPath, @@ -27,7 +28,7 @@ function Build-PSBuildUpdatableHelp { ) if ($null -ne $IsWindows -and -not $IsWindows) { - Write-Warning 'MakeCab.exe is only available on Windows. Cannot create help cab.' + Write-Warning $LocalizedData.MakeCabNotAvailable return } @@ -35,18 +36,32 @@ function Build-PSBuildUpdatableHelp { # Create updatable help output directory if (-not (Test-Path -LiteralPath $OutputPath)) { - New-Item $OutputPath -ItemType Directory -Verbose:$VerbosePreference > $null + $newItemSplat = @{ + ItemType = 'Directory' + Verbose = $VerbosePreference + Path = $OutputPath + } + New-Item @newItemSplat > $null } else { - Write-Verbose "Directory already exists [$OutputPath]." - Get-ChildItem $OutputPath | Remove-Item -Recurse -Force -Verbose:$VerbosePreference + Write-Verbose ($LocalizedData.DirectoryAlreadyExists -f $OutputPath) + $removeItemSplat = @{ + Recurse = $true + Force = $true + Verbose = $VerbosePreference + } + Get-ChildItem $OutputPath | Remove-Item @removeItemSplat } - # Generate updatable help files. Note: this will currently update the version number in the module's MD - # file in the metadata. + # Generate updatable help files. Note: this will currently update the + # version number in the module's MD file in the metadata. foreach ($locale in $helpLocales) { $cabParams = @{ CabFilesFolder = [IO.Path]::Combine($moduleOutDir, $locale) - LandingPagePath = [IO.Path]::Combine($DocsPath, $locale, "$Module.md") + LandingPagePath = [IO.Path]::Combine( + $DocsPath, + $locale, + "$Module.md" + ) OutputFolder = $OutputPath Verbose = $VerbosePreference } diff --git a/PowerShellBuild/Public/Clear-PSBuildOutputFolder.ps1 b/PowerShellBuild/Public/Clear-PSBuildOutputFolder.ps1 index a8001af..fd6149a 100644 --- a/PowerShellBuild/Public/Clear-PSBuildOutputFolder.ps1 +++ b/PowerShellBuild/Public/Clear-PSBuildOutputFolder.ps1 @@ -16,11 +16,11 @@ function Clear-PSBuildOutputFolder { # Maybe a bit paranoid but this task nuked \ on my laptop. Good thing I was not running as admin. [parameter(Mandatory)] [ValidateScript({ - if ($_.Length -le 3) { - throw "`$Path [$_] must be longer than 3 characters." - } - $true - })] + if ($_.Length -le 3) { + throw ($LocalizedData.PathLongerThan3Chars -f $_) + } + $true + })] [string]$Path ) diff --git a/PowerShellBuild/Public/Initialize-PSBuild.ps1 b/PowerShellBuild/Public/Initialize-PSBuild.ps1 index 0d0b803..c45dfb7 100644 --- a/PowerShellBuild/Public/Initialize-PSBuild.ps1 +++ b/PowerShellBuild/Public/Initialize-PSBuild.ps1 @@ -7,13 +7,14 @@ function Initialize-PSBuild { .PARAMETER BuildEnvironment Contains the PowerShellBuild settings (known as $PSBPreference). .PARAMETER UseBuildHelpers - Use BuildHelpers module to populate common environment variables based on current build system context. + Use BuildHelpers module to populate common environment variables based + on current build system context. .EXAMPLE PS> Initialize-PSBuild -UseBuildHelpers Populate build system environment variables. #> - [cmdletbinding()] + [CmdletBinding()] param( [Parameter(Mandatory)] [Hashtable] @@ -22,10 +23,24 @@ function Initialize-PSBuild { [switch]$UseBuildHelpers ) - if ($BuildEnvironment.Build.OutDir.StartsWith($env:BHProjectPath, [StringComparison]::OrdinalIgnoreCase)) { - $BuildEnvironment.Build.ModuleOutDir = [IO.Path]::Combine($BuildEnvironment.Build.OutDir, $env:BHProjectName, $BuildEnvironment.General.ModuleVersion) + if ( + $BuildEnvironment.Build.OutDir.StartsWith( + $env:BHProjectPath, + [StringComparison]::OrdinalIgnoreCase + ) + ) { + $BuildEnvironment.Build.ModuleOutDir = [IO.Path]::Combine( + $BuildEnvironment.Build.OutDir, + $env:BHProjectName, + $BuildEnvironment.General.ModuleVersion + ) } else { - $BuildEnvironment.Build.ModuleOutDir = [IO.Path]::Combine($env:BHProjectPath, $BuildEnvironment.Build.OutDir, $env:BHProjectName, $BuildEnvironment.General.ModuleVersion) + $BuildEnvironment.Build.ModuleOutDir = [IO.Path]::Combine( + $env:BHProjectPath, + $BuildEnvironment.Build.OutDir, + $env:BHProjectName, + $BuildEnvironment.General.ModuleVersion + ) } $params = @{ @@ -33,19 +48,19 @@ function Initialize-PSBuild { } Set-BuildEnvironment @params -Force - Write-Host 'Build System Details:' -ForegroundColor Yellow - $psVersion = $PSVersionTable.PSVersion.ToString() - $buildModuleName = $MyInvocation.MyCommand.Module.Name + Write-Host $LocalizedData.BuildSystemDetails -ForegroundColor 'Yellow' + $psVersion = $PSVersionTable.PSVersion.ToString() + $buildModuleName = $MyInvocation.MyCommand.Module.Name $buildModuleVersion = $MyInvocation.MyCommand.Module.Version - "Build Module: $buildModuleName`:$buildModuleVersion" - "PowerShell Version: $psVersion" + $LocalizedData.BuildModule -f $buildModuleName, $buildModuleVersion + $LocalizedData.PowerShellVersion -f $psVersion if ($UseBuildHelpers.IsPresent) { $nl = [System.Environment]::NewLine - Write-Host "$nl`Environment variables:" -ForegroundColor Yellow + Write-Host ($LocalizedData.EnvironmentVariables -f $nl) -ForegroundColor 'Yellow' (Get-Item ENV:BH*).Foreach({ - '{0,-20}{1}' -f $_.name, $_.value - }) + '{0,-20}{1}' -f $_.name, $_.value + }) } } diff --git a/PowerShellBuild/Public/Publish-PSBuildModule.ps1 b/PowerShellBuild/Public/Publish-PSBuildModule.ps1 index 3603c29..dd4708d 100644 --- a/PowerShellBuild/Public/Publish-PSBuildModule.ps1 +++ b/PowerShellBuild/Public/Publish-PSBuildModule.ps1 @@ -27,18 +27,18 @@ function Publish-PSBuildModule { Publish version 0.1.0 of the module at path .\Output\0.1.0\MyModule to the PSGallery repository using an API key and a PowerShell credential. #> - [cmdletbinding(DefaultParameterSetName = 'ApiKey')] + [CmdletBinding(DefaultParameterSetName = 'ApiKey')] param( [parameter(Mandatory)] [ValidateScript({ - if (-not (Test-Path -Path $_ )) { - throw 'Folder does not exist' - } - if (-not (Test-Path -Path $_ -PathType Container)) { - throw 'The Path argument must be a folder. File paths are not allowed.' - } - $true - })] + if (-not (Test-Path -Path $_ )) { + throw ($LocalizedData.PathDoesNotExist -f $_) + } + if (-not (Test-Path -Path $_ -PathType Container)) { + throw $LocalizedData.PathArgumentMustBeAFolder + } + $true + })] [System.IO.FileInfo]$Path, [parameter(Mandatory)] @@ -50,10 +50,10 @@ function Publish-PSBuildModule { [Alias('ApiKey')] [string]$NuGetApiKey, - [pscredential]$Credential + [PSCredential]$Credential ) - Write-Verbose "Publishing version [$Version] to repository [$Repository]..." + Write-Verbose ($LocalizedData.PublishingVersionToRepository -f $Version, $Repository) $publishParams = @{ Path = $Path diff --git a/PowerShellBuild/Public/Test-PSBuildPester.ps1 b/PowerShellBuild/Public/Test-PSBuildPester.ps1 index 9297971..3dbd4a9 100644 --- a/PowerShellBuild/Public/Test-PSBuildPester.ps1 +++ b/PowerShellBuild/Public/Test-PSBuildPester.ps1 @@ -31,11 +31,11 @@ function Test-PSBuildPester { .PARAMETER OutputVerbosity The verbosity of output, options are None, Normal, Detailed and Diagnostic. Default is Detailed. .EXAMPLE - PS> Test-PSBuildPester -Path ./tests -ModuleName Mymodule -OutputPath ./out/testResults.xml + PS> Test-PSBuildPester -Path ./tests -ModuleName MyModule -OutputPath ./out/testResults.xml Run Pester tests in ./tests and save results to ./out/testResults.xml #> - [cmdletbinding()] + [CmdletBinding()] param( [parameter(Mandatory)] [string]$Path, @@ -74,7 +74,7 @@ function Test-PSBuildPester { try { if ($ImportModule) { if (-not (Test-Path $ModuleManifest)) { - Write-Error "Unable to find module manifest [$ModuleManifest]. Can't import module" + Write-Error ($LocalizedData.UnableToFindModuleManifest -f $ModuleManifest) } else { # Remove any previously imported project modules and import from the output dir Get-Module $ModuleName | Remove-Module -Force -ErrorAction SilentlyContinue @@ -86,11 +86,11 @@ function Test-PSBuildPester { Import-Module Pester -MinimumVersion 5.0.0 $configuration = [PesterConfiguration]::Default - $configuration.Output.Verbosity = $OutputVerbosity - $configuration.Run.PassThru = $true + $configuration.Output.Verbosity = $OutputVerbosity + $configuration.Run.PassThru = $true $configuration.Run.SkipRemainingOnFailure = $SkipRemainingOnFailure - $configuration.TestResult.Enabled = -not [string]::IsNullOrEmpty($OutputPath) - $configuration.TestResult.OutputPath = $OutputPath + $configuration.TestResult.Enabled = -not [string]::IsNullOrEmpty($OutputPath) + $configuration.TestResult.OutputPath = $OutputPath $configuration.TestResult.OutputFormat = $OutputFormat if ($CodeCoverage.IsPresent) { @@ -98,43 +98,43 @@ function Test-PSBuildPester { if ($CodeCoverageFiles.Count -gt 0) { $configuration.CodeCoverage.Path = $CodeCoverageFiles } - $configuration.CodeCoverage.OutputPath = $CodeCoverageOutputFile + $configuration.CodeCoverage.OutputPath = $CodeCoverageOutputFile $configuration.CodeCoverage.OutputFormat = $CodeCoverageOutputFileFormat } $testResult = Invoke-Pester -Configuration $configuration -Verbose:$VerbosePreference if ($testResult.FailedCount -gt 0) { - throw 'One or more Pester tests failed' + throw $LocalizedData.PesterTestsFailed } if ($CodeCoverage.IsPresent) { - Write-Host "`nCode Coverage:`n" -ForegroundColor Yellow + Write-Host ("`n{0}:`n" -f $LocalizedData.CodeCoverage) -ForegroundColor Yellow if (Test-Path $CodeCoverageOutputFile) { $textInfo = (Get-Culture).TextInfo [xml]$testCoverage = Get-Content $CodeCoverageOutputFile $ccReport = $testCoverage.report.counter.ForEach({ - $total = [int]$_.missed + [int]$_.covered - $perc = [Math]::Truncate([int]$_.covered / $total) - [pscustomobject]@{ - name = $textInfo.ToTitleCase($_.Type.ToLower()) - percent = $perc - } - }) + $total = [int]$_.missed + [int]$_.covered + $percent = [Math]::Truncate([int]$_.covered / $total) + [PSCustomObject]@{ + name = $textInfo.ToTitleCase($_.Type.ToLower()) + percent = $percent + } + }) $ccFailMsgs = @() $ccReport.ForEach({ - 'Type: [{0}]: {1:p}' -f $_.name, $_.percent - if ($_.percent -lt $CodeCoverageThreshold) { - $ccFailMsgs += ('Code coverage: [{0}] is [{1:p}], which is less than the threshold of [{2:p}]' -f $_.name, $_.percent, $CodeCoverageThreshold) - } - }) + '{0}: [{1}]: {2:p}' -f $LocalizedData.Type, $_.name, $_.percent + if ($_.percent -lt $CodeCoverageThreshold) { + $ccFailMsgs += ($LocalizedData.CodeCoverageLessThanThreshold -f $_.name, $_.percent, $CodeCoverageThreshold) + } + }) Write-Host "`n" $ccFailMsgs.Foreach({ - Write-Error $_ - }) + Write-Error $_ + }) } else { - Write-Error "Code coverage file [$CodeCoverageOutputFile] not found." + Write-Error ($LocalizedData.CodeCoverageCodeCoverageFileNotFound -f $CodeCoverageOutputFile) } } } finally { diff --git a/PowerShellBuild/Public/Test-PSBuildScriptAnalysis.ps1 b/PowerShellBuild/Public/Test-PSBuildScriptAnalysis.ps1 index c917d1c..9d47628 100644 --- a/PowerShellBuild/Public/Test-PSBuildScriptAnalysis.ps1 +++ b/PowerShellBuild/Public/Test-PSBuildScriptAnalysis.ps1 @@ -5,17 +5,17 @@ function Test-PSBuildScriptAnalysis { .DESCRIPTION Run PSScriptAnalyzer tests against a module. .PARAMETER Path - Path to PowerShell module directory to run ScriptAnalyser on. + Path to PowerShell module directory to run ScriptAnalyzer on. .PARAMETER SeverityThreshold - Fail ScriptAnalyser test if any issues are found with this threshold or higher. + Fail ScriptAnalyzer test if any issues are found with this threshold or higher. .PARAMETER SettingsPath - Path to ScriptAnalyser settings to use. + Path to ScriptAnalyzer settings to use. .EXAMPLE - PS> Test-PSBuildScriptAnalysis -Path ./Output/Mymodule/0.1.0 -SeverityThreshold Error + PS> Test-PSBuildScriptAnalysis -Path ./Output/MyModule/0.1.0 -SeverityThreshold Error - Run ScriptAnalyzer on built module in ./Output/Mymodule/0.1.0. Throw error if any errors are found. + Run ScriptAnalyzer on built module in ./Output/MyModule/0.1.0. Throw error if any errors are found. #> - [cmdletbinding()] + [CmdletBinding()] param( [parameter(Mandatory)] [string]$Path, @@ -26,15 +26,15 @@ function Test-PSBuildScriptAnalysis { [string]$SettingsPath ) - Write-Verbose "SeverityThreshold set to: $SeverityThreshold" + Write-Verbose ($LocalizedData.SeverityThresholdSetTo -f $SeverityThreshold) $analysisResult = Invoke-ScriptAnalyzer -Path $Path -Settings $SettingsPath -Recurse -Verbose:$VerbosePreference - $errors = ($analysisResult.where({$_Severity -eq 'Error'})).Count - $warnings = ($analysisResult.where({$_Severity -eq 'Warning'})).Count - $infos = ($analysisResult.where({$_Severity -eq 'Information'})).Count + $errors = ($analysisResult.where({ $_Severity -eq 'Error' })).Count + $warnings = ($analysisResult.where({ $_Severity -eq 'Warning' })).Count + $infos = ($analysisResult.where({ $_Severity -eq 'Information' })).Count if ($analysisResult) { - Write-Host 'PSScriptAnalyzer results:' -ForegroundColor Yellow + Write-Host $LocalizedData.PSScriptAnalyzerResults -ForegroundColor Yellow $analysisResult | Format-Table -AutoSize } @@ -44,22 +44,22 @@ function Test-PSBuildScriptAnalysis { } 'Error' { if ($errors -gt 0) { - throw 'One or more ScriptAnalyzer errors were found!' + throw $LocalizedData.ScriptAnalyzerErrors } } 'Warning' { if ($errors -gt 0 -or $warnings -gt 0) { - throw 'One or more ScriptAnalyzer warnings were found!' + throw $LocalizedData.ScriptAnalyzerWarnings } } 'Information' { if ($errors -gt 0 -or $warnings -gt 0 -or $infos -gt 0) { - throw 'One or more ScriptAnalyzer warnings were found!' + throw $LocalizedData.ScriptAnalyzerWarnings } } default { if ($analysisResult.Count -ne 0) { - throw 'One or more ScriptAnalyzer issues were found!' + throw $LocalizedData.ScriptAnalyzerIssues } } } diff --git a/PowerShellBuild/en-US/Messages.psd1 b/PowerShellBuild/en-US/Messages.psd1 new file mode 100644 index 0000000..0d45520 --- /dev/null +++ b/PowerShellBuild/en-US/Messages.psd1 @@ -0,0 +1,26 @@ +ConvertFrom-StringData @' +NoCommandsExported=No commands have been exported. Skipping markdown generation. +FailedToGenerateMarkdownHelp=Failed to generate markdown help. : {0} +AddingFileToPsm1=Adding [{0}] to PSM1 +MakeCabNotAvailable=MakeCab.exe is not available. Cannot create help cab. +DirectoryAlreadyExists=Directory already exists [{0}]. +PathLongerThan3Chars=`$Path [{0}] must be longer than 3 characters. +BuildSystemDetails=Build System Details: +BuildModule=Build Module: {0}`:{1} +PowerShellVersion=PowerShell Version: {0} +EnvironmentVariables={0}`Environment variables: +PublishingVersionToRepository=Publishing version [{0}] to repository [{1}]... +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 +CodeCoverage=Code Coverage +Type=Type +CodeCoverageLessThanThreshold=Code coverage: [{0}] is [{1:p}], which is less than the threshold of [{2:p}] +CodeCoverageCodeCoverageFileNotFound=Code coverage file [{0}] not found. +SeverityThresholdSetTo=SeverityThreshold set to: {0} +PSScriptAnalyzerResults=PSScriptAnalyzer results: +ScriptAnalyzerErrors=One or more ScriptAnalyzer errors were found! +ScriptAnalyzerWarnings=One or more ScriptAnalyzer warnings were found! +ScriptAnalyzerIssues=One or more ScriptAnalyzer issues were found! +'@ From 4ccbef4596118098919d048a41bccfbcc4866ec7 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Tue, 22 Jul 2025 16:48:09 -0700 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20=E2=9C=A8=20Add=20localization=20?= =?UTF-8?q?support=20to=20CHANGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Documented the addition of localization support in the Unreleased section. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92685cf..53a93ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Add new dependencies variables to allow end user to modify which tasks are run. +- Add localization support. ## [0.7.2] 2025-05-21 From 79cced782504fdfb6a909e3f4e037e851cbb5211 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Thu, 24 Jul 2025 17:45:13 -0700 Subject: [PATCH 03/13] =?UTF-8?q?fix:=20=F0=9F=90=9B=20Correct=20variable?= =?UTF-8?q?=20name=20typo=20in=20Build-PSBuildMAMLHelp=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed the typo in the variable name from `$helpLoc2ales` to `$helpLocales`.\ * Added search exclusion patterns in `.vscode/settings.json`. * Reformatted command arguments in `.vscode/tasks.json` for better readability. * Introduced a new task `Bootstrap` in `.vscode/tasks.json` for initialization. * Updated `PowerShellBuild` version in `requirements.psd1` to `0.7.2`. * Enhanced test assertions to provide clearer output and ensure all expected files exist. * Comment out incorrect output path assignment for GitHub Actions. * Added debug statements to log the output path and contents of the `TestModule` after build. * Removed redundant count test for PSD1 and dot-sourced functions with specific file look ups. * Add LocalizedData to PSM incase of load failure --- .github/workflows/test.yml | 8 ++++- .vscode/settings.json | 10 +++++- .vscode/tasks.json | 25 ++++++++++--- PowerShellBuild/PowerShellBuild.psm1 | 29 +++++++++++++++ .../Public/Build-PSBuildMAMLHelp.ps1 | 2 +- tests/TestModule/requirements.psd1 | 14 ++++---- tests/build.tests.ps1 | 35 ++++++++++++------- 7 files changed, 95 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ee4ecfb..d4dd83b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,4 +16,10 @@ jobs: - uses: actions/checkout@v4 - name: Test shell: pwsh - run: ./build.ps1 -Task Test -Bootstrap + env: + DEBUG: ${{ runner.debug == '1' }} + run: | + if($env:DEBUG -eq 'true' -or $env:DEBUG -eq '1') { + $DebugPreference = 'Continue' + } + ./build.ps1 -Task Test -Bootstrap diff --git a/.vscode/settings.json b/.vscode/settings.json index 227163e..bce2856 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,13 @@ "powershell.codeFormatting.addWhitespaceAroundPipe": true, "powershell.codeFormatting.useCorrectCasing": true, "powershell.codeFormatting.newLineAfterOpenBrace": true, - "powershell.codeFormatting.alignPropertyValuePairs": true + "powershell.codeFormatting.alignPropertyValuePairs": true, + "search.useIgnoreFiles": true, + "search.exclude": { + "**/node_modules": true, + "**/bower_components": true, + "**/*.code-search": true, + "**/.ruby-lsp": true, + "Output/**": true + } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 106a76c..ca72255 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,13 +2,17 @@ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", - // Start PowerShell (pwsh on *nix) "windows": { "options": { "shell": { "executable": "powershell.exe", - "args": [ "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command" ] + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command" + ] } } }, @@ -16,7 +20,10 @@ "options": { "shell": { "executable": "/usr/bin/pwsh", - "args": [ "-NoProfile", "-Command" ] + "args": [ + "-NoProfile", + "-Command" + ] } } }, @@ -24,11 +31,13 @@ "options": { "shell": { "executable": "/usr/local/bin/pwsh", - "args": [ "-NoProfile", "-Command" ] + "args": [ + "-NoProfile", + "-Command" + ] } } }, - "tasks": [ { "label": "Clean", @@ -69,6 +78,12 @@ "label": "Publish", "type": "shell", "command": "${cwd}/build.ps1 -Task Publish -Verbose" + }, + { + "label": "Bootstrap", + "type": "shell", + "command": "${cwd}/build.ps1 -Task Init -Bootstrap -Verbose", + "problemMatcher": [] } ] } diff --git a/PowerShellBuild/PowerShellBuild.psm1 b/PowerShellBuild/PowerShellBuild.psm1 index e6b2e9d..87a2200 100644 --- a/PowerShellBuild/PowerShellBuild.psm1 +++ b/PowerShellBuild/PowerShellBuild.psm1 @@ -9,6 +9,35 @@ foreach ($import in $public + $private) { } } +data LocalizedData { + # Load here in case Import-LocalizedData is not available + ConvertFrom-StringData @' +NoCommandsExported=No commands have been exported. Skipping markdown generation. +FailedToGenerateMarkdownHelp=Failed to generate markdown help. : {0} +AddingFileToPsm1=Adding [{0}] to PSM1 +MakeCabNotAvailable=MakeCab.exe is not available. Cannot create help cab. +DirectoryAlreadyExists=Directory already exists [{0}]. +PathLongerThan3Chars=`$Path [{0}] must be longer than 3 characters. +BuildSystemDetails=Build System Details: +BuildModule=Build Module: {0}`:{1} +PowerShellVersion=PowerShell Version: {0} +EnvironmentVariables={0}`Environment variables: +PublishingVersionToRepository=Publishing version [{0}] to repository [{1}]... +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 +CodeCoverage=Code Coverage +Type=Type +CodeCoverageLessThanThreshold=Code coverage: [{0}] is [{1:p}], which is less than the threshold of [{2:p}] +CodeCoverageCodeCoverageFileNotFound=Code coverage file [{0}] not found. +SeverityThresholdSetTo=SeverityThreshold set to: {0} +PSScriptAnalyzerResults=PSScriptAnalyzer results: +ScriptAnalyzerErrors=One or more ScriptAnalyzer errors were found! +ScriptAnalyzerWarnings=One or more ScriptAnalyzer warnings were found! +ScriptAnalyzerIssues=One or more ScriptAnalyzer issues were found! +'@ +} $importLocalizedDataSplat = @{ BindingVariable = 'LocalizedData' FileName = 'Messages.psd1' diff --git a/PowerShellBuild/Public/Build-PSBuildMAMLHelp.ps1 b/PowerShellBuild/Public/Build-PSBuildMAMLHelp.ps1 index f5ae020..20d42a6 100644 --- a/PowerShellBuild/Public/Build-PSBuildMAMLHelp.ps1 +++ b/PowerShellBuild/Public/Build-PSBuildMAMLHelp.ps1 @@ -23,7 +23,7 @@ function Build-PSBuildMAMLHelp { [string]$DestinationPath ) - $helpLoc2ales = (Get-ChildItem -Path $Path -Directory).Name + $helpLocales = (Get-ChildItem -Path $Path -Directory).Name # Generate the module's primary MAML help file foreach ($locale in $helpLocales) { diff --git a/tests/TestModule/requirements.psd1 b/tests/TestModule/requirements.psd1 index ff8889f..bbeef6c 100644 --- a/tests/TestModule/requirements.psd1 +++ b/tests/TestModule/requirements.psd1 @@ -1,23 +1,23 @@ @{ - PSDependOptions = @{ + PSDependOptions = @{ Target = 'CurrentUser' } - 'InvokeBuild' = @{ + 'InvokeBuild' = @{ Version = '5.5.1' } - 'Pester' = @{ - Version = '4.8.1' + 'Pester' = @{ + Version = '4.8.1' Parameters = @{ SkipPublisherCheck = $true } } - 'psake' = @{ + 'psake' = @{ Version = '4.8.0' } - 'BuildHelpers' = @{ + 'BuildHelpers' = @{ Version = '2.0.10' } 'PowerShellBuild' = @{ - Version = '0.5.0' + Version = '0.7.2' } } diff --git a/tests/build.tests.ps1 b/tests/build.tests.ps1 index 9bbbdc3..6022d47 100644 --- a/tests/build.tests.ps1 +++ b/tests/build.tests.ps1 @@ -6,9 +6,11 @@ Describe 'Build' { # For some reason, the TestModule build process create the output in the project root # and not relative to it's own build file. if ($env:GITHUB_ACTION) { + $script:testModuleSource = [IO.Path]::Combine($PSScriptRoot, 'TestModule') $script:testModuleOutputPath = [IO.Path]::Combine($env:BHProjectPath, 'Output', 'TestModule', '0.1.0') } else { - $script:testModuleOutputPath = [IO.Path]::Combine($env:BHProjectPath, 'tests', 'TestModule', 'Output', 'TestModule', '0.1.0') + $script:testModuleSource = [IO.Path]::Combine($PSScriptRoot, 'TestModule') + $script:testModuleOutputPath = [IO.Path]::Combine($script:testModuleSource, 'Output', 'TestModule', '0.1.0') } } @@ -19,11 +21,11 @@ Describe 'Build' { Write-Host "OutputPath: $script:testModuleOutputPath" # build is PS job so psake doesn't freak out because it's nested - Start-Job -ScriptBlock { - Set-Location $using:PSScriptRoot/TestModule + Start-Job -Scriptblock { + Set-Location -Path $using:testModuleSource $global:PSBuildCompile = $true ./build.ps1 -Task Build - } | Wait-Job + } -WorkingDirectory $script:testModuleSource | Wait-Job } AfterAll { @@ -71,11 +73,17 @@ Describe 'Build' { Context 'Dot-sourced module' { BeforeAll { # build is PS job so psake doesn't freak out because it's nested - Start-Job -ScriptBlock { - Set-Location $using:PSScriptRoot/TestModule + Start-Job -Scriptblock { + Set-Location -Path $using:testModuleSource $global:PSBuildCompile = $false ./build.ps1 -Task Build - } | Wait-Job + } -WorkingDirectory $script:testModuleSource | Wait-Job + Write-Debug "TestModule output path: $script:testModuleSource" + $items = Get-ChildItem -Path $script:testModuleSource -Recurse -File + Write-Debug ($items | Format-Table FullName | Out-String) + Write-Debug "TestModule output path: $script:testModuleOutputPath" + $items = Get-ChildItem -Path $script:testModuleOutputPath -Recurse -File + Write-Debug ($items | Format-Table FullName | Out-String) } AfterAll { @@ -86,12 +94,13 @@ Describe 'Build' { $script:testModuleOutputPath | Should -Exist } - It 'Has PSD1 and dot-sourced functions' { - (Get-ChildItem -Path $script:testModuleOutputPath).Count | Should -Be 6 - "$script:testModuleOutputPath/TestModule.psd1" | Should -Exist - "$script:testModuleOutputPath/TestModule.psm1" | Should -Exist - "$script:testModuleOutputPath/Public" | Should -Exist - "$script:testModuleOutputPath/Private" | Should -Exist + It '<_> should exist' -ForEach @( + "TestModule.psd1", + "TestModule.psm1", + "Public", + "Private" + ) { + Join-Path -Path $script:testModuleOutputPath -ChildPath $_ | Should -Exist } It 'Does not contain excluded stuff' { From 0bd1800dd91c899ae3a760e86ad56b7eb7cc0982 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Mon, 31 Mar 2025 11:11:54 -0700 Subject: [PATCH 04/13] =?UTF-8?q?fix(Build-PSBuildMarkdown):=20=E2=9C=A8?= =?UTF-8?q?=20Correct=20CmdletBinding=20attribute=20casing=20and=20improve?= =?UTF-8?q?=20parameter=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated `[cmdletbinding()]` to `[CmdletBinding()]` for consistency. * Enhanced readability by aligning parameter assignments in `$newMDParams`. * Added new words to `cspell.json` for better spell checking. --- cspell.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cspell.json b/cspell.json index a8959d9..23d56cb 100644 --- a/cspell.json +++ b/cspell.json @@ -11,7 +11,9 @@ ], "words": [], "ignoreWords": [ + "BHPS", "psake", + "pscredential", "MAML" ], "import": [] From ef35c0bae2be4c0a40a946ede7d1ee81d7b646b1 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Mon, 31 Mar 2025 11:12:34 -0700 Subject: [PATCH 05/13] =?UTF-8?q?chore:=20=E2=9C=A8=20Update=20script=20an?= =?UTF-8?q?alysis=20settings=20and=20improve=20task=20definitions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Migrate `ScriptAnalyzerSettings.psd1` to `tests` directory and update paths. * Enhance task definitions in `psakeFile.ps1` for better readability and consistency. * Add new exclusion rule for `PSUseApprovedVerbs` in the script analysis settings. --- .vscode/settings.json | 1 + PowerShellBuild/ScriptAnalyzerSettings.psd1 | 13 ----------- psakeFile.ps1 | 26 ++++++++++----------- tests/ScriptAnalyzerSettings.psd1 | 14 ++++++++++- 4 files changed, 27 insertions(+), 27 deletions(-) delete mode 100644 PowerShellBuild/ScriptAnalyzerSettings.psd1 diff --git a/.vscode/settings.json b/.vscode/settings.json index bce2856..b8375aa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "powershell.codeFormatting.useCorrectCasing": true, "powershell.codeFormatting.newLineAfterOpenBrace": true, "powershell.codeFormatting.alignPropertyValuePairs": true, + "powershell.scriptAnalysis.settingsPath": "tests/ScriptAnalyzerSettings.psd1", "search.useIgnoreFiles": true, "search.exclude": { "**/node_modules": true, diff --git a/PowerShellBuild/ScriptAnalyzerSettings.psd1 b/PowerShellBuild/ScriptAnalyzerSettings.psd1 deleted file mode 100644 index f794cdb..0000000 --- a/PowerShellBuild/ScriptAnalyzerSettings.psd1 +++ /dev/null @@ -1,13 +0,0 @@ -@{ - ExcludeRules = @( - 'PSAvoidUsingWriteHost', - 'PSUseToExportFieldsInManifest' - 'PSUseDeclaredVarsMoreThanAssignments' - ) - Rules = @{ - # Don't trip on the task alias. It's by design - PSAvoidUsingCmdletAliases = @{ - Whitelist = @('task') - } - } -} diff --git a/psakeFile.ps1 b/psakeFile.ps1 index 22a24c4..cb183ac 100644 --- a/psakeFile.ps1 +++ b/psakeFile.ps1 @@ -1,24 +1,24 @@ -properties { +Properties { $settings = . ([IO.Path]::Combine($PSScriptRoot, 'build.settings.ps1')) if ($galleryApiKey) { $settings.PSGalleryApiKey = $galleryApiKey.GetNetworkCredential().password } } -task default -depends Test +Task default -depends Test -task Init { +Task Init { "STATUS: Testing with PowerShell $($settings.PSVersion)" 'Build System Details:' Get-Item ENV:BH* } -description 'Initialize build environment' -task Test -Depends Init, Analyze, Pester -description 'Run test suite' +Task Test -depends Init, Analyze, Pester -description 'Run test suite' -task Analyze -depends Build { - $analysis = Invoke-ScriptAnalyzer -Path $settings.ModuleOutDir -Recurse -Verbose:$false -Settings ([IO.Path]::Combine($env:BHModulePath, 'ScriptAnalyzerSettings.psd1')) - $errors = $analysis | Where-Object {$_.Severity -eq 'Error'} - $warnings = $analysis | Where-Object {$_.Severity -eq 'Warning'} +Task Analyze -depends Build { + $analysis = Invoke-ScriptAnalyzer -Path $settings.ModuleOutDir -Recurse -Verbose:$false -Settings ([IO.Path]::Combine('tests', 'ScriptAnalyzerSettings.psd1')) + $errors = $analysis | Where-Object { $_.Severity -eq 'Error' } + $warnings = $analysis | Where-Object { $_.Severity -eq 'Warning' } if (@($errors).Count -gt 0) { Write-Error -Message 'One or more Script Analyzer errors were found. Build cannot continue!' $errors | Format-Table -AutoSize @@ -30,11 +30,11 @@ task Analyze -depends Build { } } -description 'Run PSScriptAnalyzer' -task Pester -depends Build { +Task Pester -depends Build { Remove-Module $settings.ProjectName -ErrorAction SilentlyContinue -Verbose:$false $testResultsXml = [IO.Path]::Combine($settings.OutputDir, 'testResults.xml') - $testResults = Invoke-Pester -Path $settings.Tests -Output Detailed -PassThru + $testResults = Invoke-Pester -Path $settings.Tests -Output Detailed -PassThru if ($testResults.FailedCount -gt 0) { $testResults | Format-List @@ -42,13 +42,13 @@ task Pester -depends Build { } } -description 'Run Pester tests' -task Clean -depends Init { +Task Clean -depends Init { if (Test-Path -Path $settings.ModuleOutDir) { Remove-Item -Path $settings.ModuleOutDir -Recurse -Force -Verbose:$false } } -task Build -depends Init, Clean { +Task Build -depends Init, Clean { New-Item -Path $settings.ModuleOutDir -ItemType Directory -Force > $null Copy-Item -Path "$($settings.SUT)/*" -Destination $settings.ModuleOutDir -Recurse @@ -59,7 +59,7 @@ task Build -depends Init, Clean { # & .\Build\Convert-PSAke.ps1 $psakePath | Out-File -Encoding UTF8 $ibPath } -task Publish -depends Test { +Task Publish -depends Test { " Publishing version [$($settings.Manifest.ModuleVersion)] to PSGallery..." if ($settings.PSGalleryApiKey) { Publish-Module -Path $settings.ModuleOutDir -NuGetApiKey $settings.PSGalleryApiKey diff --git a/tests/ScriptAnalyzerSettings.psd1 b/tests/ScriptAnalyzerSettings.psd1 index 8fe45b9..2c11de5 100644 --- a/tests/ScriptAnalyzerSettings.psd1 +++ b/tests/ScriptAnalyzerSettings.psd1 @@ -1,3 +1,15 @@ @{ - + ExcludeRules = @( + 'PSAvoidUsingWriteHost', + 'PSUseToExportFieldsInManifest', + 'PSUseDeclaredVarsMoreThanAssignments', + # This throws a warning on Build verb, which is valid as of PSv6 + 'PSUseApprovedVerbs' + ) + Rules = @{ + # Don't trip on the task alias. It's by design + PSAvoidUsingCmdletAliases = @{ + Whitelist = @('task') + } + } } From 0a4a30370581d7eb9cdac54cda2b0b236d5efbba Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Mon, 31 Mar 2025 11:13:11 -0700 Subject: [PATCH 06/13] =?UTF-8?q?fix(Publish-PSBuildModule,=20Test-PSBuild?= =?UTF-8?q?Pester):=20=E2=9C=A8=20Improve=20parameter=20handling=20and=20s?= =?UTF-8?q?uppress=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added suppression attributes for unused parameters in `Publish-PSBuildModule` and `Test-PSBuildPester`. * Enhanced `CmdletBinding` attribute casing for consistency. * Refined parameter validation logic for better clarity and maintainability. --- PowerShellBuild/Public/Publish-PSBuildModule.ps1 | 9 +++++++-- PowerShellBuild/Public/Test-PSBuildPester.ps1 | 9 +++++++-- README.md | 4 ++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/PowerShellBuild/Public/Publish-PSBuildModule.ps1 b/PowerShellBuild/Public/Publish-PSBuildModule.ps1 index dd4708d..a282bbc 100644 --- a/PowerShellBuild/Public/Publish-PSBuildModule.ps1 +++ b/PowerShellBuild/Public/Publish-PSBuildModule.ps1 @@ -1,4 +1,9 @@ function Publish-PSBuildModule { + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + '', + Justification = 'Both Credential and NuGetApiKey are used just not via explicit variable call.' + )] <# .SYNOPSIS Publishes a module to the defined PowerShell repository. @@ -32,10 +37,10 @@ function Publish-PSBuildModule { [parameter(Mandatory)] [ValidateScript({ if (-not (Test-Path -Path $_ )) { - throw ($LocalizedData.PathDoesNotExist -f $_) + throw 'Folder does not exist' } if (-not (Test-Path -Path $_ -PathType Container)) { - throw $LocalizedData.PathArgumentMustBeAFolder + throw 'The Path argument must be a folder. File paths are not allowed.' } $true })] diff --git a/PowerShellBuild/Public/Test-PSBuildPester.ps1 b/PowerShellBuild/Public/Test-PSBuildPester.ps1 index 3dbd4a9..2bf762f 100644 --- a/PowerShellBuild/Public/Test-PSBuildPester.ps1 +++ b/PowerShellBuild/Public/Test-PSBuildPester.ps1 @@ -1,4 +1,9 @@ function Test-PSBuildPester { + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'CodeCoverageThreshold', + Justification = 'Used inside a foreach method call.' + )] <# .SYNOPSIS Execute Pester tests for module. @@ -124,9 +129,9 @@ function Test-PSBuildPester { $ccFailMsgs = @() $ccReport.ForEach({ - '{0}: [{1}]: {2:p}' -f $LocalizedData.Type, $_.name, $_.percent + 'Type: [{0}]: {1:p}' -f $_.name, $_.percent if ($_.percent -lt $CodeCoverageThreshold) { - $ccFailMsgs += ($LocalizedData.CodeCoverageLessThanThreshold -f $_.name, $_.percent, $CodeCoverageThreshold) + $ccFailMsgs += ('Code coverage: [{0}] is [{1:p}], which is less than the threshold of [{2:p}]' -f $_.name, $_.percent, $CodeCoverageThreshold) } }) Write-Host "`n" diff --git a/README.md b/README.md index 8725468..526f1dc 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,11 @@ Install-Module -Name psake -RequiredVersion 4.8.0 -Repository PSGallery > [how to dot source tasks using PowerShell aliases](https://github.com/nightroman/Invoke-Build/blob/master/Tasks/Import/README.md#example-2-import-from-a-module-with-tasks) > example. +

Logo

+ ## Status - Work in progress @@ -209,3 +211,5 @@ $PSBPreference.Test.CodeCoverage.Enabled = $false [psgallery]: https://www.powershellgallery.com/packages/PowerShellBuild [license-badge]: https://img.shields.io/github/license/psake/PowerShellBuild.svg [license]: https://raw.githubusercontent.com/psake/PowerShellBuild/main/LICENSE + + From a82a9724b65f850e51869fd523a4d5318b1f74d5 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Mon, 31 Mar 2025 11:14:06 -0700 Subject: [PATCH 07/13] =?UTF-8?q?fix(CHANGELOG,=20PowerShellBuild.psd1):?= =?UTF-8?q?=20=F0=9F=90=9B=20Bump=20version=20to=200.7.1=20and=20update=20?= =?UTF-8?q?changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove extra item from `New-MarkdownHelp` splat to prevent failure with `$PSBPreference.Docs.Overwrite = $true` * Clean up failing Script Analyzer settings and relocate the configuration file. --- CHANGELOG.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53a93ba..267d22f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +### Added + - Add new dependencies variables to allow end user to modify which tasks are run. - Add localization support. +### Fixes + +- Fix a bug in `Build-PSBuildMarkdown` where a hashtable item was added twice. + ## [0.7.2] 2025-05-21 ### Added @@ -27,11 +33,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `$PSBPreference.Test.OutputVerbosity` can be set to **None**, **Normal**, **Detailed**, and **Diagnostic**. The default value is **Detailed**. -## [0.7.1] 2025-04-01 +## [0.7.1] 2025-03-31 Bugfix -### Fixes +### Fixed -- Fix a bug in `Build-PSBuildMarkdown` where a hashtable item was added twice. +- Remove extra item from `New-MarkdownHelp` splat that would result in a failure + when using `$PSBPreference.Docs.Overwrite = $true` +- Clean up some failing Script Analyzer settings, including moving the file. ## [0.7.0] 2025-03-31 From 665eb7b92d815a86e1ee2dfd1f9b3a4ddf6a515d Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Mon, 31 Mar 2025 12:26:45 -0700 Subject: [PATCH 08/13] =?UTF-8?q?feat(vscode,=20tests):=20=E2=9C=A8=20Add?= =?UTF-8?q?=20new=20PowerShell=20debug=20configurations=20and=20enhance=20?= =?UTF-8?q?psake=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introduced two new launch configurations for debugging PowerShell scripts in VSCode. * Updated `psakeFile.ps1` to correct property casing and added functionality to control documentation overwrite behavior. * Enhanced tests to verify the documentation overwrite feature. --- .vscode/launch.json | 26 +++++++++++++++++++++++++- tests/TestModule/psakeFile.ps1 | 19 ++++++++++++------- tests/build.tests.ps1 | 30 ++++++++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c1b568a..b0e497f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,30 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Run Build and Debug (Temp Console)", + "type": "PowerShell", + "request": "launch", + "script": "${cwd}/build.ps1", + "args": [ + "-Task Test", + "-Verbose" + ], + "cwd": "${workspaceFolder}", + "createTemporaryIntegratedConsole": true + }, + { + "name": "Run Build and Debug", + "type": "PowerShell", + "request": "launch", + "script": "${cwd}/build.ps1", + "args": [ + "-Task Test", + "-Verbose" + ], + "cwd": "${workspaceFolder}", + "createTemporaryIntegratedConsole": true + }, { "name": "PowerShell: Interactive Session", "type": "PowerShell", @@ -11,4 +35,4 @@ "cwd": "" } ] -} \ No newline at end of file +} diff --git a/tests/TestModule/psakeFile.ps1 b/tests/TestModule/psakeFile.ps1 index 68e4dbb..6a28237 100644 --- a/tests/TestModule/psakeFile.ps1 +++ b/tests/TestModule/psakeFile.ps1 @@ -1,6 +1,6 @@ Import-Module ../../Output/PowerShellBuild -Force -properties { +Properties { # Pester can build the module using both scenarios if (Test-Path -Path 'Variable:\PSBuildCompile') { $PSBPreference.Build.CompileModule = $global:PSBuildCompile @@ -15,21 +15,26 @@ properties { $PSBPreference.Build.Exclude = ('excludeme.txt', 'excludemealso*', 'dontcopy') # If compiling, insert headers/footers for entire PSM1 and for each inserted function - $PSBPreference.Build.CompileHeader = '# Module Header' + [Environment]::NewLine - $PSBPreference.Build.CompileFooter = '# Module Footer' + $PSBPreference.Build.CompileHeader = '# Module Header' + [Environment]::NewLine + $PSBPreference.Build.CompileFooter = '# Module Footer' $PSBPreference.Build.CompileScriptHeader = '# Function header' $PSBPreference.Build.CompileScriptFooter = '# Function footer' + [Environment]::NewLine # So Pester InModuleScope works - $PSBPreference.Test.ImportModule = $true - $PSBPreference.Test.CodeCoverage.Enabled = $true + $PSBPreference.Test.ImportModule = $true + $PSBPreference.Test.CodeCoverage.Enabled = $true $PSBPreference.Test.CodeCoverage.Threshold = 0.0 $PSBPreference.Test.CodeCoverage.OutputFile = 'cc.xml' # Override the default output directory $PSBPreference.Build.OutDir = 'Output' + + # Don't overwrite the docs + $PSBPreference.Docs.Overwrite = $false } -task default -depends Build +Task default -depends Build + +Task Build -FromModule PowerShellBuild -minimumVersion 0.5.0 + -task Build -FromModule PowerShellBuild -minimumVersion 0.5.0 diff --git a/tests/build.tests.ps1 b/tests/build.tests.ps1 index 6022d47..da4e917 100644 --- a/tests/build.tests.ps1 +++ b/tests/build.tests.ps1 @@ -21,7 +21,7 @@ Describe 'Build' { Write-Host "OutputPath: $script:testModuleOutputPath" # build is PS job so psake doesn't freak out because it's nested - Start-Job -Scriptblock { + Start-Job -ScriptBlock { Set-Location -Path $using:testModuleSource $global:PSBuildCompile = $true ./build.ps1 -Task Build @@ -68,12 +68,38 @@ Describe 'Build' { It 'Has MAML help XML' { "$script:testModuleOutputPath/en-US/TestModule-help.xml" | Should -Exist } + + It 'Can Overwrite the Docs' { + # Replace with a different string to test the overwrite + $docPath = "$PSScriptRoot/TestModule/docs/en-US/Get-HelloWorld.md" + $original = Get-Content $docPath -Raw + $new = $original -replace 'Hello World', 'Hello Universe' + Set-Content $docPath -Value $new -Force + # Test that the file was updated + Get-Content $docPath -Raw | Should -BeExactly $new + + # Update the psake file + $psakeFile = "$PSScriptRoot/TestModule/psakeFile.ps1" + $psakeFileContent = Get-Content $psakeFile -Raw + $psakeFileContent = $psakeFileContent -replace '\$PSBPreference.Docs.Overwrite = \$false', '$PSBPreference.Docs.Overwrite = $true' + Set-Content $psakeFile -Value $psakeFileContent -Force + + # build is PS job so psake doesn't freak out because it's nested + Start-Job -ScriptBlock { + Set-Location $using:PSScriptRoot/TestModule + $global:PSBuildCompile = $true + ./build.ps1 -Task Build + } | Wait-Job + + # Test that the file reset as expected + Get-Content $docPath -Raw | Should -BeExactly $original + } } Context 'Dot-sourced module' { BeforeAll { # build is PS job so psake doesn't freak out because it's nested - Start-Job -Scriptblock { + Start-Job -ScriptBlock { Set-Location -Path $using:testModuleSource $global:PSBuildCompile = $false ./build.ps1 -Task Build From f674ec8064c939cbabd5b7b9f1637310ed88bae0 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Mon, 31 Mar 2025 16:44:01 -0700 Subject: [PATCH 09/13] =?UTF-8?q?chore:=20=E2=9C=A8=20Fix=20up=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Build tests now run in TempDir * TestModule moved into fixtures folder. * Added test for docs --- .vscode/tasks.json | 2 +- .../Public/Publish-PSBuildModule.ps1 | 10 +- PowerShellBuild/Public/Test-PSBuildPester.ps1 | 10 +- psakeFile.ps1 | 2 +- requirements.psd1 | 2 +- tests/Help.tests.ps1 | 56 ++++----- tests/Manifest.tests.ps1 | 23 ++-- tests/TestModule/Tests/Help.tests.ps1 | 93 -------------- tests/build.tests.ps1 | 96 +++++++------- tests/{ => fixtures}/TestModule/.build.ps1 | 0 .../{ => fixtures}/TestModule/.gitattributes | 0 .../TestModule/.github/CONTRIBUTING.md | 0 .../TestModule/.github/ISSUE_TEMPLATE.md | 0 .../.github/PULL_REQUEST_TEMPLATE.md | 0 tests/{ => fixtures}/TestModule/.gitignore | 0 .../TestModule/.vscode/extensions.json | 0 .../TestModule/.vscode/settings.json | 0 .../TestModule/.vscode/tasks.json | 0 tests/{ => fixtures}/TestModule/CHANGELOG.md | 0 .../TestModule/CODE_OF_CONDUCT.md | 0 tests/{ => fixtures}/TestModule/LICENSE | 0 tests/{ => fixtures}/TestModule/README.md | 0 .../TestModule/Private/GetHelloWorld.ps1 | 0 .../TestModule/Private/excludemealso.ps1 | 0 .../TestModule/Public/Get-HelloWorld.ps1 | 0 .../TestModule/TestModule/TestModule.psd1 | 0 .../TestModule/TestModule/TestModule.psm1 | 0 .../TestModule/dontcopy/garbage.txt | 0 .../TestModule/TestModule/excludeme.txt | 0 .../TestModule/stuff/copymealways.txt | 0 .../fixtures/TestModule/Tests/Help.tests.ps1 | 117 ++++++++++++++++++ .../TestModule/Tests/Manifest.tests.ps1 | 0 .../TestModule/Tests/Meta.tests.ps1 | 0 .../TestModule/Tests/MetaFixers.psm1 | 0 .../Tests/ScriptAnalyzerSettings.psd1 | 0 .../Tests/a_InModuleScope.tests.ps1 | 0 .../TestModule/azure-pipelines.yml | 0 tests/{ => fixtures}/TestModule/build.ps1 | 0 tests/{ => fixtures}/TestModule/mkdocs.yml | 0 tests/{ => fixtures}/TestModule/psakeFile.ps1 | 9 ++ .../TestModule/requirements.psd1 | 0 41 files changed, 231 insertions(+), 189 deletions(-) delete mode 100644 tests/TestModule/Tests/Help.tests.ps1 rename tests/{ => fixtures}/TestModule/.build.ps1 (100%) rename tests/{ => fixtures}/TestModule/.gitattributes (100%) rename tests/{ => fixtures}/TestModule/.github/CONTRIBUTING.md (100%) rename tests/{ => fixtures}/TestModule/.github/ISSUE_TEMPLATE.md (100%) rename tests/{ => fixtures}/TestModule/.github/PULL_REQUEST_TEMPLATE.md (100%) rename tests/{ => fixtures}/TestModule/.gitignore (100%) rename tests/{ => fixtures}/TestModule/.vscode/extensions.json (100%) rename tests/{ => fixtures}/TestModule/.vscode/settings.json (100%) rename tests/{ => fixtures}/TestModule/.vscode/tasks.json (100%) rename tests/{ => fixtures}/TestModule/CHANGELOG.md (100%) rename tests/{ => fixtures}/TestModule/CODE_OF_CONDUCT.md (100%) rename tests/{ => fixtures}/TestModule/LICENSE (100%) rename tests/{ => fixtures}/TestModule/README.md (100%) rename tests/{ => fixtures}/TestModule/TestModule/Private/GetHelloWorld.ps1 (100%) rename tests/{ => fixtures}/TestModule/TestModule/Private/excludemealso.ps1 (100%) rename tests/{ => fixtures}/TestModule/TestModule/Public/Get-HelloWorld.ps1 (100%) rename tests/{ => fixtures}/TestModule/TestModule/TestModule.psd1 (100%) rename tests/{ => fixtures}/TestModule/TestModule/TestModule.psm1 (100%) rename tests/{ => fixtures}/TestModule/TestModule/dontcopy/garbage.txt (100%) rename tests/{ => fixtures}/TestModule/TestModule/excludeme.txt (100%) rename tests/{ => fixtures}/TestModule/TestModule/stuff/copymealways.txt (100%) create mode 100644 tests/fixtures/TestModule/Tests/Help.tests.ps1 rename tests/{ => fixtures}/TestModule/Tests/Manifest.tests.ps1 (100%) rename tests/{ => fixtures}/TestModule/Tests/Meta.tests.ps1 (100%) rename tests/{ => fixtures}/TestModule/Tests/MetaFixers.psm1 (100%) rename tests/{ => fixtures}/TestModule/Tests/ScriptAnalyzerSettings.psd1 (100%) rename tests/{ => fixtures}/TestModule/Tests/a_InModuleScope.tests.ps1 (100%) rename tests/{ => fixtures}/TestModule/azure-pipelines.yml (100%) rename tests/{ => fixtures}/TestModule/build.ps1 (100%) rename tests/{ => fixtures}/TestModule/mkdocs.yml (100%) rename tests/{ => fixtures}/TestModule/psakeFile.ps1 (99%) rename tests/{ => fixtures}/TestModule/requirements.psd1 (100%) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ca72255..69ccd58 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,7 +6,7 @@ "windows": { "options": { "shell": { - "executable": "powershell.exe", + "executable": "pwsh.exe", "args": [ "-NoProfile", "-ExecutionPolicy", diff --git a/PowerShellBuild/Public/Publish-PSBuildModule.ps1 b/PowerShellBuild/Public/Publish-PSBuildModule.ps1 index a282bbc..68414e5 100644 --- a/PowerShellBuild/Public/Publish-PSBuildModule.ps1 +++ b/PowerShellBuild/Public/Publish-PSBuildModule.ps1 @@ -1,9 +1,4 @@ function Publish-PSBuildModule { - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', - '', - Justification = 'Both Credential and NuGetApiKey are used just not via explicit variable call.' - )] <# .SYNOPSIS Publishes a module to the defined PowerShell repository. @@ -32,6 +27,11 @@ function Publish-PSBuildModule { Publish version 0.1.0 of the module at path .\Output\0.1.0\MyModule to the PSGallery repository using an API key and a PowerShell credential. #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + '', + Justification = 'Both Credential and NuGetApiKey are used just not via explicit variable call.' + )] [CmdletBinding(DefaultParameterSetName = 'ApiKey')] param( [parameter(Mandatory)] diff --git a/PowerShellBuild/Public/Test-PSBuildPester.ps1 b/PowerShellBuild/Public/Test-PSBuildPester.ps1 index 2bf762f..531e8ed 100644 --- a/PowerShellBuild/Public/Test-PSBuildPester.ps1 +++ b/PowerShellBuild/Public/Test-PSBuildPester.ps1 @@ -1,9 +1,4 @@ function Test-PSBuildPester { - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', - 'CodeCoverageThreshold', - Justification = 'Used inside a foreach method call.' - )] <# .SYNOPSIS Execute Pester tests for module. @@ -41,6 +36,11 @@ function Test-PSBuildPester { Run Pester tests in ./tests and save results to ./out/testResults.xml #> [CmdletBinding()] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'CodeCoverageThreshold', + Justification = 'Used inside a foreach method call.' + )] param( [parameter(Mandatory)] [string]$Path, diff --git a/psakeFile.ps1 b/psakeFile.ps1 index cb183ac..4fd5b41 100644 --- a/psakeFile.ps1 +++ b/psakeFile.ps1 @@ -16,7 +16,7 @@ Task Init { Task Test -depends Init, Analyze, Pester -description 'Run test suite' Task Analyze -depends Build { - $analysis = Invoke-ScriptAnalyzer -Path $settings.ModuleOutDir -Recurse -Verbose:$false -Settings ([IO.Path]::Combine('tests', 'ScriptAnalyzerSettings.psd1')) + $analysis = Invoke-ScriptAnalyzer -Path $settings.ModuleOutDir -Recurse -Verbose:$false -Settings './tests/ScriptAnalyzerSettings.psd1' $errors = $analysis | Where-Object { $_.Severity -eq 'Error' } $warnings = $analysis | Where-Object { $_.Severity -eq 'Warning' } if (@($errors).Count -gt 0) { diff --git a/requirements.psd1 b/requirements.psd1 index 5e22bd3..190478b 100755 --- a/requirements.psd1 +++ b/requirements.psd1 @@ -4,7 +4,7 @@ } BuildHelpers = '2.0.16' Pester = @{ - MinimumVersion = '5.6.1' + MinimumVersion = '5.7.1' Parameters = @{ SkipPublisherCheck = $true } diff --git a/tests/Help.tests.ps1 b/tests/Help.tests.ps1 index 974e60a..2719468 100755 --- a/tests/Help.tests.ps1 +++ b/tests/Help.tests.ps1 @@ -1,17 +1,17 @@ # Taken with love from @juneb_get_help (https://raw.githubusercontent.com/juneb/PesterTDD/master/Module.Help.Tests.ps1) BeforeDiscovery { + function global:FilterOutCommonParams { param ($Params) - $commonParams = [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + - [System.Management.Automation.PSCmdlet]::CommonParameters - $params | Where-Object { $_.Name -notin $commonParams } | Sort-Object -Property Name -Unique + $commonParameters = [System.Management.Automation.PSCmdlet]::CommonParameters + [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + $params | Where-Object { $_.Name -notin $commonParameters } | Sort-Object -Property Name -Unique } - $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest - $outputDir = Join-Path -Path $env:BHProjectPath -ChildPath 'Output' - $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName - $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion + $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest + $outputDir = Join-Path -Path $env:BHProjectPath -ChildPath 'Output' + $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName + $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion $outputModVerManifest = Join-Path -Path $outputModVerDir -ChildPath "$($env:BHProjectName).psd1" # Get module commands @@ -25,36 +25,32 @@ BeforeDiscovery { if ($PSVersionTable.PSVersion.Major -lt 6) { $params.CommandType[0] += 'Workflow' } - $commands = Get-Command @params + $script:commands = Get-Command @params ## When testing help, remember that help is cached at the beginning of each session. ## To test, restart session. } -AfterAll { - Remove-Item Function:/FilterOutCommonParams -} - Describe "Test help for <_.Name>" -ForEach $commands { BeforeDiscovery { # Get command help, parameters, and links - $command = $_ - $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue - $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters - $commandParameterNames = $commandParameters.Name - $helpLinks = $commandHelp.relatedLinks.navigationLink.uri + $command = $_ + $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue + $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters + $script:commandParameterNames = $commandParameters.Name + $script:helpLinks = $commandHelp.relatedLinks.navigationLink.uri } BeforeAll { # These vars are needed in both discovery and test phases so we need to duplicate them here - $command = $_ - $commandName = $_.Name - $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue - $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters - $commandParameterNames = $commandParameters.Name - $helpParameters = global:FilterOutCommonParams -Params $commandHelp.Parameters.Parameter - $helpParameterNames = $helpParameters.Name + $command = $_ + $script:commandName = $_.Name + $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue + $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters + $script:commandParameterNames = $commandParameters.Name + $helpParameters = global:FilterOutCommonParams -Params $commandHelp.Parameters.Parameter + $script:helpParameterNames = $helpParameters.Name } # If help is not found, synopsis in auto-generated help is the syntax diagram @@ -81,13 +77,13 @@ Describe "Test help for <_.Name>" -ForEach $commands { (Invoke-WebRequest -Uri $_ -UseBasicParsing).StatusCode | Should -Be '200' } - Context "Parameter <_.Name>" -Foreach $commandParameters { + Context "Parameter <_.Name>" -ForEach $commandParameters { BeforeAll { - $parameter = $_ - $parameterName = $parameter.Name - $parameterHelp = $commandHelp.parameters.parameter | Where-Object Name -eq $parameterName - $parameterHelpType = if ($parameterHelp.ParameterValue) { $parameterHelp.ParameterValue.Trim() } + $parameter = $_ + $parameterName = $parameter.Name + $parameterHelp = $commandHelp.parameters.parameter | Where-Object Name -EQ $parameterName + $script:parameterHelpType = if ($parameterHelp.ParameterValue) { $parameterHelp.ParameterValue.Trim() } } # Should be a description for every parameter @@ -107,7 +103,7 @@ Describe "Test help for <_.Name>" -ForEach $commands { } } - Context "Test <_> help parameter help for " -Foreach $helpParameterNames { + Context "Test <_> help parameter help for " -ForEach $helpParameterNames { # Shouldn't find extra parameters in help. It "finds help parameter in code: <_>" { diff --git a/tests/Manifest.tests.ps1 b/tests/Manifest.tests.ps1 index b659654..4dcc18d 100644 --- a/tests/Manifest.tests.ps1 +++ b/tests/Manifest.tests.ps1 @@ -1,14 +1,15 @@ BeforeAll { + Set-Location "$PSScriptRoot/.." Set-BuildEnvironment -Force - $moduleName = $env:BHProjectName - $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest - $outputDir = Join-Path -Path $ENV:BHProjectPath -ChildPath 'Output' - $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName - $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion + $moduleName = $env:BHProjectName + $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest + $outputDir = Join-Path -Path $ENV:BHProjectPath -ChildPath 'Output' + $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName + $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion $outputManifestPath = Join-Path -Path $outputModVerDir -Child "$($moduleName).psd1" - $manifestData = Test-ModuleManifest -Path $outputManifestPath -Verbose:$false -ErrorAction Stop -WarningAction SilentlyContinue + $manifestData = Test-ModuleManifest -Path $outputManifestPath -Verbose:$false -ErrorAction Stop -WarningAction SilentlyContinue - $changelogPath = Join-Path -Path $env:BHProjectPath -Child 'CHANGELOG.md' + $changelogPath = Join-Path -Path $env:BHProjectPath -Child 'CHANGELOG.md' $changelogVersion = Get-Content $changelogPath | ForEach-Object { if ($_ -match "^##\s\[(?(\d+\.){1,3}\d+)\]") { $changelogVersion = $matches.Version @@ -16,7 +17,7 @@ BeforeAll { } } - $script:manifest = $null + $script:manifest = $null } Describe 'Module manifest' { @@ -47,7 +48,7 @@ Describe 'Module manifest' { } It 'Has a valid guid' { - {[guid]::Parse($manifestData.Guid)} | Should -Not -Throw + { [guid]::Parse($manifestData.Guid) } | Should -Not -Throw } It 'Has a valid copyright' { @@ -55,7 +56,7 @@ Describe 'Module manifest' { } It 'Has a valid version in the changelog' { - $changelogVersion | Should -Not -BeNullOrEmpty + $changelogVersion | Should -Not -BeNullOrEmpty $changelogVersion -as [Version] | Should -Not -BeNullOrEmpty } @@ -76,7 +77,7 @@ Describe 'Git tagging' -Skip { } It 'Is tagged with a valid version' { - $gitTagVersion | Should -Not -BeNullOrEmpty + $gitTagVersion | Should -Not -BeNullOrEmpty $gitTagVersion -as [Version] | Should -Not -BeNullOrEmpty } diff --git a/tests/TestModule/Tests/Help.tests.ps1 b/tests/TestModule/Tests/Help.tests.ps1 deleted file mode 100644 index 7467ad8..0000000 --- a/tests/TestModule/Tests/Help.tests.ps1 +++ /dev/null @@ -1,93 +0,0 @@ -# Taken with love from @juneb_get_help (https://raw.githubusercontent.com/juneb/PesterTDD/master/Module.Help.Tests.ps1) - -Describe 'Help' { - $testCases = Get-Command -Module $env:BHProjectName -CommandType Cmdlet, Function | ForEach-Object { - @{ - Name = $_.Name - Command = $_ - } - } - - BeforeAll { - $commonParameters = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', - 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable', 'Confirm', 'Whatif' - } - - # No auto-generated help - Context 'Auto-generation' { - it 'Help for [] should not be auto-generated' -TestCases $testCases { - param($Name, $Command) - - $help = Get-Help $Name -ErrorAction SilentlyContinue - $help.Synopsis | Should -Not -BeLike '*`[``]*' - } - } - - - # Should have a description for every function - Context 'Help description' { - It 'Help for [] has a description' -TestCases $testCases { - param($Name, $Command) - - $help = Get-Help $Name -ErrorAction SilentlyContinue - $help.Description | Should -Not -BeNullOrEmpty - } - } - - # Should be at least one example per command - Context 'Examples' { - It 'Help for [] has example code' -TestCases $testCases { - param($Name, $Command) - - $help = Get-Help $Name -ErrorAction SilentlyContinue - ($help.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty - } - } - - # Parameter help - Context 'Parameter help' { - It '[] has help for every parameter' -TestCases $testCases { - param($Name, $Command) - - $help = Get-Help $Name -ErrorAction SilentlyContinue - $parameters = $Command.ParameterSets.Parameters | - Sort-Object -Property Name -Unique | - Where-Object { $_.Name -notin $commonParameters } - $parameterNames = $parameters.Name - - # Without the filter, WhatIf and Confirm parameters are still flagged in "finds help parameter in code" test - $helpParameters = $help.Parameters.Parameter | - Where-Object { $_.Name -notin $commonParameters } | - Sort-Object -Property Name -Unique - $helpParameterNames = $helpParameters.Name - - foreach ($parameter in $parameters) { - $parameterName = $parameter.Name - $parameterHelp = $help.parameters.parameter | Where-Object Name -eq $parameterName - $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty - - $codeMandatory = $parameter.IsMandatory.toString() - $parameterHelp.Required | Should -Be $codeMandatory - - $codeType = $parameter.ParameterType.Name - # To avoid calling Trim method on a null object. - $helpType = if ($parameterHelp.parameterValue) { $parameterHelp.parameterValue.Trim() } - $helpType | Should -Be $codeType - } - } - } - - # Links are valid - Context 'Links' { - It 'Help for [] has valid links' -TestCases $testCases { - param($Name, $Command) - - $help = Get-Help $Name -ErrorAction SilentlyContinue - $link = $help.relatedLinks.navigationLink.uri - foreach ($link in $links) { - $Results = Invoke-WebRequest -Uri $link -UseBasicParsing - $Results.StatusCode | Should -Be '200' - } - } - } -} diff --git a/tests/build.tests.ps1 b/tests/build.tests.ps1 index da4e917..ed21fa4 100644 --- a/tests/build.tests.ps1 +++ b/tests/build.tests.ps1 @@ -2,27 +2,31 @@ Describe 'Build' { BeforeAll { - # Hack for GH Actions - # For some reason, the TestModule build process create the output in the project root - # and not relative to it's own build file. - if ($env:GITHUB_ACTION) { - $script:testModuleSource = [IO.Path]::Combine($PSScriptRoot, 'TestModule') - $script:testModuleOutputPath = [IO.Path]::Combine($env:BHProjectPath, 'Output', 'TestModule', '0.1.0') - } else { - $script:testModuleSource = [IO.Path]::Combine($PSScriptRoot, 'TestModule') - $script:testModuleOutputPath = [IO.Path]::Combine($script:testModuleSource, 'Output', 'TestModule', '0.1.0') - } + $tempDir = Join-Path $TestDrive 'TestModule' + Copy-Item $PSScriptRoot/fixtures/TestModule $tempDir -Recurse + Set-Location $tempDir + + # Capture any of the jobs for cleanup later + [array]$script:jobs = @() + + $path = 'Output/TestModule/0.1.0' + $script:testModuleOutputPath = Join-Path . $path + } + + AfterAll { + Set-Location $PSScriptRoot + $jobs | Stop-Job -ErrorAction Ignore + $jobs | Remove-Job -ErrorAction Ignore } Context 'Compile module' { BeforeAll { - - Write-Host "PSScriptRoot: $PSScriptRoot" + Write-Host "PSScriptRoot: $tempDir" Write-Host "OutputPath: $script:testModuleOutputPath" # build is PS job so psake doesn't freak out because it's nested - Start-Job -ScriptBlock { - Set-Location -Path $using:testModuleSource + $script:jobs += Start-Job -Scriptblock { + Set-Location $using:tempDir $global:PSBuildCompile = $true ./build.ps1 -Task Build } -WorkingDirectory $script:testModuleSource | Wait-Job @@ -68,39 +72,13 @@ Describe 'Build' { It 'Has MAML help XML' { "$script:testModuleOutputPath/en-US/TestModule-help.xml" | Should -Exist } - - It 'Can Overwrite the Docs' { - # Replace with a different string to test the overwrite - $docPath = "$PSScriptRoot/TestModule/docs/en-US/Get-HelloWorld.md" - $original = Get-Content $docPath -Raw - $new = $original -replace 'Hello World', 'Hello Universe' - Set-Content $docPath -Value $new -Force - # Test that the file was updated - Get-Content $docPath -Raw | Should -BeExactly $new - - # Update the psake file - $psakeFile = "$PSScriptRoot/TestModule/psakeFile.ps1" - $psakeFileContent = Get-Content $psakeFile -Raw - $psakeFileContent = $psakeFileContent -replace '\$PSBPreference.Docs.Overwrite = \$false', '$PSBPreference.Docs.Overwrite = $true' - Set-Content $psakeFile -Value $psakeFileContent -Force - - # build is PS job so psake doesn't freak out because it's nested - Start-Job -ScriptBlock { - Set-Location $using:PSScriptRoot/TestModule - $global:PSBuildCompile = $true - ./build.ps1 -Task Build - } | Wait-Job - - # Test that the file reset as expected - Get-Content $docPath -Raw | Should -BeExactly $original - } } Context 'Dot-sourced module' { BeforeAll { # build is PS job so psake doesn't freak out because it's nested - Start-Job -ScriptBlock { - Set-Location -Path $using:testModuleSource + $script:jobs += Start-Job -Scriptblock { + Set-Location $using:tempDir $global:PSBuildCompile = $false ./build.ps1 -Task Build } -WorkingDirectory $script:testModuleSource | Wait-Job @@ -137,4 +115,38 @@ Describe 'Build' { "$script:testModuleOutputPath/en-US/TestModule-help.xml" | Should -Exist } } + Context 'Overwrite Docs' { + BeforeAll { + + Write-Host "PSScriptRoot: $tempDir" + Write-Host "OutputPath: $script:testModuleOutputPath" + + # Replace with a different string to test the overwrite + $script:docPath = "$tempDir/docs/en-US/Get-HelloWorld.md" + $script:original = Get-Content $docPath -Raw + $new = $original -replace 'Hello World', 'Hello Universe' + Set-Content $docPath -Value $new -Force + + # Update the psake file + $psakeFile = "$tempDir/psakeFile.ps1" + $psakeFileContent = Get-Content $psakeFile -Raw + $psakeFileContent = $psakeFileContent -replace '\$PSBPreference.Docs.Overwrite = \$false', '$PSBPreference.Docs.Overwrite = $true' + Set-Content $psakeFile -Value $psakeFileContent -Force + + # build is PS job so psake doesn't freak out because it's nested + $script:jobs += Start-Job -Scriptblock { + Set-Location $using:tempDir + $global:PSBuildCompile = $true + ./build.ps1 -Task Build + } | Wait-Job + } + + AfterAll { + Remove-Item $script:testModuleOutputPath -Recurse -Force + } + It 'Can Overwrite the Docs' { + # Test that the file reset as expected + Get-Content $script:docPath -Raw | Should -BeExactly $script:original + } + } } diff --git a/tests/TestModule/.build.ps1 b/tests/fixtures/TestModule/.build.ps1 similarity index 100% rename from tests/TestModule/.build.ps1 rename to tests/fixtures/TestModule/.build.ps1 diff --git a/tests/TestModule/.gitattributes b/tests/fixtures/TestModule/.gitattributes similarity index 100% rename from tests/TestModule/.gitattributes rename to tests/fixtures/TestModule/.gitattributes diff --git a/tests/TestModule/.github/CONTRIBUTING.md b/tests/fixtures/TestModule/.github/CONTRIBUTING.md similarity index 100% rename from tests/TestModule/.github/CONTRIBUTING.md rename to tests/fixtures/TestModule/.github/CONTRIBUTING.md diff --git a/tests/TestModule/.github/ISSUE_TEMPLATE.md b/tests/fixtures/TestModule/.github/ISSUE_TEMPLATE.md similarity index 100% rename from tests/TestModule/.github/ISSUE_TEMPLATE.md rename to tests/fixtures/TestModule/.github/ISSUE_TEMPLATE.md diff --git a/tests/TestModule/.github/PULL_REQUEST_TEMPLATE.md b/tests/fixtures/TestModule/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from tests/TestModule/.github/PULL_REQUEST_TEMPLATE.md rename to tests/fixtures/TestModule/.github/PULL_REQUEST_TEMPLATE.md diff --git a/tests/TestModule/.gitignore b/tests/fixtures/TestModule/.gitignore similarity index 100% rename from tests/TestModule/.gitignore rename to tests/fixtures/TestModule/.gitignore diff --git a/tests/TestModule/.vscode/extensions.json b/tests/fixtures/TestModule/.vscode/extensions.json similarity index 100% rename from tests/TestModule/.vscode/extensions.json rename to tests/fixtures/TestModule/.vscode/extensions.json diff --git a/tests/TestModule/.vscode/settings.json b/tests/fixtures/TestModule/.vscode/settings.json similarity index 100% rename from tests/TestModule/.vscode/settings.json rename to tests/fixtures/TestModule/.vscode/settings.json diff --git a/tests/TestModule/.vscode/tasks.json b/tests/fixtures/TestModule/.vscode/tasks.json similarity index 100% rename from tests/TestModule/.vscode/tasks.json rename to tests/fixtures/TestModule/.vscode/tasks.json diff --git a/tests/TestModule/CHANGELOG.md b/tests/fixtures/TestModule/CHANGELOG.md similarity index 100% rename from tests/TestModule/CHANGELOG.md rename to tests/fixtures/TestModule/CHANGELOG.md diff --git a/tests/TestModule/CODE_OF_CONDUCT.md b/tests/fixtures/TestModule/CODE_OF_CONDUCT.md similarity index 100% rename from tests/TestModule/CODE_OF_CONDUCT.md rename to tests/fixtures/TestModule/CODE_OF_CONDUCT.md diff --git a/tests/TestModule/LICENSE b/tests/fixtures/TestModule/LICENSE similarity index 100% rename from tests/TestModule/LICENSE rename to tests/fixtures/TestModule/LICENSE diff --git a/tests/TestModule/README.md b/tests/fixtures/TestModule/README.md similarity index 100% rename from tests/TestModule/README.md rename to tests/fixtures/TestModule/README.md diff --git a/tests/TestModule/TestModule/Private/GetHelloWorld.ps1 b/tests/fixtures/TestModule/TestModule/Private/GetHelloWorld.ps1 similarity index 100% rename from tests/TestModule/TestModule/Private/GetHelloWorld.ps1 rename to tests/fixtures/TestModule/TestModule/Private/GetHelloWorld.ps1 diff --git a/tests/TestModule/TestModule/Private/excludemealso.ps1 b/tests/fixtures/TestModule/TestModule/Private/excludemealso.ps1 similarity index 100% rename from tests/TestModule/TestModule/Private/excludemealso.ps1 rename to tests/fixtures/TestModule/TestModule/Private/excludemealso.ps1 diff --git a/tests/TestModule/TestModule/Public/Get-HelloWorld.ps1 b/tests/fixtures/TestModule/TestModule/Public/Get-HelloWorld.ps1 similarity index 100% rename from tests/TestModule/TestModule/Public/Get-HelloWorld.ps1 rename to tests/fixtures/TestModule/TestModule/Public/Get-HelloWorld.ps1 diff --git a/tests/TestModule/TestModule/TestModule.psd1 b/tests/fixtures/TestModule/TestModule/TestModule.psd1 similarity index 100% rename from tests/TestModule/TestModule/TestModule.psd1 rename to tests/fixtures/TestModule/TestModule/TestModule.psd1 diff --git a/tests/TestModule/TestModule/TestModule.psm1 b/tests/fixtures/TestModule/TestModule/TestModule.psm1 similarity index 100% rename from tests/TestModule/TestModule/TestModule.psm1 rename to tests/fixtures/TestModule/TestModule/TestModule.psm1 diff --git a/tests/TestModule/TestModule/dontcopy/garbage.txt b/tests/fixtures/TestModule/TestModule/dontcopy/garbage.txt similarity index 100% rename from tests/TestModule/TestModule/dontcopy/garbage.txt rename to tests/fixtures/TestModule/TestModule/dontcopy/garbage.txt diff --git a/tests/TestModule/TestModule/excludeme.txt b/tests/fixtures/TestModule/TestModule/excludeme.txt similarity index 100% rename from tests/TestModule/TestModule/excludeme.txt rename to tests/fixtures/TestModule/TestModule/excludeme.txt diff --git a/tests/TestModule/TestModule/stuff/copymealways.txt b/tests/fixtures/TestModule/TestModule/stuff/copymealways.txt similarity index 100% rename from tests/TestModule/TestModule/stuff/copymealways.txt rename to tests/fixtures/TestModule/TestModule/stuff/copymealways.txt diff --git a/tests/fixtures/TestModule/Tests/Help.tests.ps1 b/tests/fixtures/TestModule/Tests/Help.tests.ps1 new file mode 100644 index 0000000..857eb63 --- /dev/null +++ b/tests/fixtures/TestModule/Tests/Help.tests.ps1 @@ -0,0 +1,117 @@ +# Taken with love from @juneb_get_help (https://raw.githubusercontent.com/juneb/PesterTDD/master/Module.Help.Tests.ps1) + +BeforeDiscovery { + function global:FilterOutCommonParams { + param ($Params) + $commonParams = [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + + [System.Management.Automation.PSCmdlet]::CommonParameters + $params | Where-Object { $_.Name -notin $commonParams } | Sort-Object -Property Name -Unique + } + + $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest + $outputDir = Join-Path -Path $env:BHProjectPath -ChildPath 'Output' + $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName + $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion + $outputModVerManifest = Join-Path -Path $outputModVerDir -ChildPath "$($env:BHProjectName).psd1" + + # Get module commands + # Remove all versions of the module from the session. Pester can't handle multiple versions. + Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore + Import-Module -Name $outputModVerManifest -Verbose:$false -ErrorAction Stop + $params = @{ + Module = (Get-Module $env:BHProjectName) + CommandType = [System.Management.Automation.CommandTypes[]]'Cmdlet, Function' # Not alias + } + if ($PSVersionTable.PSVersion.Major -lt 6) { + $params.CommandType[0] += 'Workflow' + } + $commands = Get-Command @params + + ## When testing help, remember that help is cached at the beginning of each session. + ## To test, restart session. +} + +AfterAll { + Remove-Item Function:/FilterOutCommonParams +} + +Describe "Test help for <_.Name>" -ForEach $commands { + + BeforeDiscovery { + # Get command help, parameters, and links + $command = $_ + $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue + $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters + $commandParameterNames = $commandParameters.Name + $helpLinks = $commandHelp.relatedLinks.navigationLink.uri + } + + BeforeAll { + # These vars are needed in both discovery and test phases so we need to duplicate them here + $command = $_ + $commandName = $_.Name + $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue + $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters + $commandParameterNames = $commandParameters.Name + $helpParameters = global:FilterOutCommonParams -Params $commandHelp.Parameters.Parameter + $helpParameterNames = $helpParameters.Name + } + + # If help is not found, synopsis in auto-generated help is the syntax diagram + It 'Help is not auto-generated' { + $commandHelp.Synopsis | Should -Not -BeLike '*`[``]*' + } + + # Should be a description for every function + It "Has description" { + $commandHelp.Description | Should -Not -BeNullOrEmpty + } + + # Should be at least one example + It "Has example code" { + ($commandHelp.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty + } + + # Should be at least one example description + It "Has example help" { + ($commandHelp.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty + } + + It "Help link <_> is valid" -ForEach $helpLinks { + (Invoke-WebRequest -Uri $_ -UseBasicParsing).StatusCode | Should -Be '200' + } + + Context "Parameter <_.Name>" -ForEach $commandParameters { + + BeforeAll { + $parameter = $_ + $parameterName = $parameter.Name + $parameterHelp = $commandHelp.parameters.parameter | Where-Object Name -EQ $parameterName + $parameterHelpType = if ($parameterHelp.ParameterValue) { $parameterHelp.ParameterValue.Trim() } + } + + # Should be a description for every parameter + It "Has description" { + $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty + } + + # Required value in Help should match IsMandatory property of parameter + It "Has correct [mandatory] value" { + $codeMandatory = $_.IsMandatory.toString() + $parameterHelp.Required | Should -Be $codeMandatory + } + + # Parameter type in help should match code + It "Has correct parameter type" { + $parameterHelpType | Should -Be $parameter.ParameterType.Name + } + } + + Context "Test <_> help parameter help for " -ForEach $helpParameterNames { + + # Shouldn't find extra parameters in help. + It "finds help parameter in code: <_>" { + $_ -in $parameterNames | Should -Be $true + } + } +} diff --git a/tests/TestModule/Tests/Manifest.tests.ps1 b/tests/fixtures/TestModule/Tests/Manifest.tests.ps1 similarity index 100% rename from tests/TestModule/Tests/Manifest.tests.ps1 rename to tests/fixtures/TestModule/Tests/Manifest.tests.ps1 diff --git a/tests/TestModule/Tests/Meta.tests.ps1 b/tests/fixtures/TestModule/Tests/Meta.tests.ps1 similarity index 100% rename from tests/TestModule/Tests/Meta.tests.ps1 rename to tests/fixtures/TestModule/Tests/Meta.tests.ps1 diff --git a/tests/TestModule/Tests/MetaFixers.psm1 b/tests/fixtures/TestModule/Tests/MetaFixers.psm1 similarity index 100% rename from tests/TestModule/Tests/MetaFixers.psm1 rename to tests/fixtures/TestModule/Tests/MetaFixers.psm1 diff --git a/tests/TestModule/Tests/ScriptAnalyzerSettings.psd1 b/tests/fixtures/TestModule/Tests/ScriptAnalyzerSettings.psd1 similarity index 100% rename from tests/TestModule/Tests/ScriptAnalyzerSettings.psd1 rename to tests/fixtures/TestModule/Tests/ScriptAnalyzerSettings.psd1 diff --git a/tests/TestModule/Tests/a_InModuleScope.tests.ps1 b/tests/fixtures/TestModule/Tests/a_InModuleScope.tests.ps1 similarity index 100% rename from tests/TestModule/Tests/a_InModuleScope.tests.ps1 rename to tests/fixtures/TestModule/Tests/a_InModuleScope.tests.ps1 diff --git a/tests/TestModule/azure-pipelines.yml b/tests/fixtures/TestModule/azure-pipelines.yml similarity index 100% rename from tests/TestModule/azure-pipelines.yml rename to tests/fixtures/TestModule/azure-pipelines.yml diff --git a/tests/TestModule/build.ps1 b/tests/fixtures/TestModule/build.ps1 similarity index 100% rename from tests/TestModule/build.ps1 rename to tests/fixtures/TestModule/build.ps1 diff --git a/tests/TestModule/mkdocs.yml b/tests/fixtures/TestModule/mkdocs.yml similarity index 100% rename from tests/TestModule/mkdocs.yml rename to tests/fixtures/TestModule/mkdocs.yml diff --git a/tests/TestModule/psakeFile.ps1 b/tests/fixtures/TestModule/psakeFile.ps1 similarity index 99% rename from tests/TestModule/psakeFile.ps1 rename to tests/fixtures/TestModule/psakeFile.ps1 index 6a28237..5007a1e 100644 --- a/tests/TestModule/psakeFile.ps1 +++ b/tests/fixtures/TestModule/psakeFile.ps1 @@ -38,3 +38,12 @@ Task default -depends Build Task Build -FromModule PowerShellBuild -minimumVersion 0.5.0 + + + + + + + + + diff --git a/tests/TestModule/requirements.psd1 b/tests/fixtures/TestModule/requirements.psd1 similarity index 100% rename from tests/TestModule/requirements.psd1 rename to tests/fixtures/TestModule/requirements.psd1 From e329f2c87c25d91b33003da5a5307386f431c640 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Mon, 31 Mar 2025 16:48:14 -0700 Subject: [PATCH 10/13] Build Test: Fix relative path for output --- tests/build.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/build.tests.ps1 b/tests/build.tests.ps1 index ed21fa4..03de45e 100644 --- a/tests/build.tests.ps1 +++ b/tests/build.tests.ps1 @@ -10,7 +10,7 @@ Describe 'Build' { [array]$script:jobs = @() $path = 'Output/TestModule/0.1.0' - $script:testModuleOutputPath = Join-Path . $path + $script:testModuleOutputPath = Join-Path $tempDir $path } AfterAll { From d7758e3d255d58e1143d9f0adb946a15baa14e25 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Mon, 31 Mar 2025 16:51:16 -0700 Subject: [PATCH 11/13] Remove Set-Location from AfterAll --- tests/build.tests.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/build.tests.ps1 b/tests/build.tests.ps1 index 03de45e..549a779 100644 --- a/tests/build.tests.ps1 +++ b/tests/build.tests.ps1 @@ -14,7 +14,6 @@ Describe 'Build' { } AfterAll { - Set-Location $PSScriptRoot $jobs | Stop-Job -ErrorAction Ignore $jobs | Remove-Job -ErrorAction Ignore } From baad92e3d90b278758ad31ede138fe334a647541 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Mon, 31 Mar 2025 17:02:42 -0700 Subject: [PATCH 12/13] =?UTF-8?q?fix(tests):=20=F0=9F=90=9B=20Replace=20`S?= =?UTF-8?q?et-Location`=20with=20`Push-Location`=20and=20`Pop-Location`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Ensures proper stack management for directory changes in tests. * Improves cleanup in `AfterAll` by restoring the previous location. --- tests/build.tests.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/build.tests.ps1 b/tests/build.tests.ps1 index 549a779..f83ae22 100644 --- a/tests/build.tests.ps1 +++ b/tests/build.tests.ps1 @@ -4,7 +4,7 @@ Describe 'Build' { BeforeAll { $tempDir = Join-Path $TestDrive 'TestModule' Copy-Item $PSScriptRoot/fixtures/TestModule $tempDir -Recurse - Set-Location $tempDir + Push-Location $tempDir # Capture any of the jobs for cleanup later [array]$script:jobs = @() @@ -14,6 +14,7 @@ Describe 'Build' { } AfterAll { + Pop-Location $jobs | Stop-Job -ErrorAction Ignore $jobs | Remove-Job -ErrorAction Ignore } From d09c3ac396c28c1ec87030ee47b53f7a749a23af Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Sun, 27 Jul 2025 13:03:21 -0700 Subject: [PATCH 13/13] =?UTF-8?q?feat(tests):=20=E2=9C=A8=20Enhance=20buil?= =?UTF-8?q?d=20process=20and=20add=20DotSource=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introduced `BeforeDiscovery` block to ensure the build script is executed if the project path is not set. * Added `DotSource.psm1` to facilitate dot-sourcing of public functions. * Updated `psakeFile.ps1` to dynamically import the PowerShellBuild module and log its version. * Improved handling of temporary directories and paths for better compatibility with GitHub Actions. --- tests/build.tests.ps1 | 125 +++++++++++++++++------- tests/fixtures/DotSource.psm1 | 11 +++ tests/fixtures/TestModule/psakeFile.ps1 | 7 +- 3 files changed, 107 insertions(+), 36 deletions(-) create mode 100644 tests/fixtures/DotSource.psm1 diff --git a/tests/build.tests.ps1 b/tests/build.tests.ps1 index f83ae22..fc5de25 100644 --- a/tests/build.tests.ps1 +++ b/tests/build.tests.ps1 @@ -1,39 +1,67 @@ # spell-checker:ignore excludeme +BeforeDiscovery { + if ($null -eq $env:BHProjectPath) { + $path = Join-Path -Path $PSScriptRoot -ChildPath '..\build.ps1' + . $path -Task Build + } + $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest + $outputDir = Join-Path -Path $env:BHProjectPath -ChildPath 'Output' + $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName + $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion + $global:outputModVerManifest = Join-Path -Path $outputModVerDir -ChildPath "$($env:BHProjectName).psd1" + + # Get module commands + # Remove all versions of the module from the session. Pester can't handle multiple versions. + Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore + Import-Module -Name $outputModVerManifest -Verbose:$false -ErrorAction Stop +} Describe 'Build' { - BeforeAll { - $tempDir = Join-Path $TestDrive 'TestModule' - Copy-Item $PSScriptRoot/fixtures/TestModule $tempDir -Recurse - Push-Location $tempDir + <# + We prepare the tests by copying the TestModule to a temporary location + and setting the output path to a known location. + #> + + $script:testModuleSource = Join-Path $TestDrive 'TestModule' + Copy-Item $PSScriptRoot/fixtures/TestModule $script:testModuleSource -Recurse + Set-Location $script:testModuleSource + # Hack for GH Actions + # For some reason, the TestModule build process create the output in the project root + # and not relative to it's own build file. + if ($env:GITHUB_ACTION) { + $script:testModuleOutputPath = [IO.Path]::Combine($env:BHProjectPath, 'Output', 'TestModule', '0.1.0') + } else { + $script:testModuleOutputPath = [IO.Path]::Combine($script:testModuleSource, 'Output', 'TestModule', '0.1.0') + } # Capture any of the jobs for cleanup later [array]$script:jobs = @() - - $path = 'Output/TestModule/0.1.0' - $script:testModuleOutputPath = Join-Path $tempDir $path } AfterAll { - Pop-Location - $jobs | Stop-Job -ErrorAction Ignore - $jobs | Remove-Job -ErrorAction Ignore + Set-Location $PSScriptRoot } Context 'Compile module' { BeforeAll { - Write-Host "PSScriptRoot: $tempDir" + Write-Host "PSScriptRoot: $script:testModuleSource" Write-Host "OutputPath: $script:testModuleOutputPath" # build is PS job so psake doesn't freak out because it's nested - $script:jobs += Start-Job -Scriptblock { - Set-Location $using:tempDir + $script:jobs += Start-Job -ScriptBlock { + param($testModuleSource, $outputModVerManifest) + Set-Location -Path $using:testModuleSource + # We want to load the current build of PowerShellBuild so we use a + # global variable to store the output path. + $global:PSBOutput = $outputModVerManifest $global:PSBuildCompile = $true ./build.ps1 -Task Build - } -WorkingDirectory $script:testModuleSource | Wait-Job + } -WorkingDirectory $script:testModuleSource -ArgumentList $testModuleSource, $outputModVerManifest | Wait-Job } - AfterAll { Remove-Item $script:testModuleOutputPath -Recurse -Force + $jobs | Stop-Job -ErrorAction Ignore + $jobs | Remove-Job -ErrorAction Ignore } It 'Creates module' { @@ -76,22 +104,28 @@ Describe 'Build' { Context 'Dot-sourced module' { BeforeAll { + $copyItemSplat = @{ + Path = "$PSScriptRoot/fixtures/DotSource.psm1" + Destination = "$script:testModuleSource/TestModule/TestModule.psm1" + Force = $true + } + # Overwrite the existing PSM1 with the dot-sourced version + Copy-Item @copyItemSplat # build is PS job so psake doesn't freak out because it's nested - $script:jobs += Start-Job -Scriptblock { - Set-Location $using:tempDir + $script:jobs += Start-Job -ScriptBlock { + param($testModuleSource, $outputModVerManifest) + Set-Location -Path $testModuleSource + # We want to load the current build of PowerShellBuild so we use a + # global variable to store the output path. + $global:PSBOutput = $outputModVerManifest $global:PSBuildCompile = $false ./build.ps1 -Task Build - } -WorkingDirectory $script:testModuleSource | Wait-Job - Write-Debug "TestModule output path: $script:testModuleSource" - $items = Get-ChildItem -Path $script:testModuleSource -Recurse -File - Write-Debug ($items | Format-Table FullName | Out-String) - Write-Debug "TestModule output path: $script:testModuleOutputPath" - $items = Get-ChildItem -Path $script:testModuleOutputPath -Recurse -File - Write-Debug ($items | Format-Table FullName | Out-String) + } -WorkingDirectory $script:testModuleSource -ArgumentList $testModuleSource, $outputModVerManifest | Wait-Job } - AfterAll { Remove-Item $script:testModuleOutputPath -Recurse -Force + $jobs | Stop-Job -ErrorAction Ignore + $jobs | Remove-Job -ErrorAction Ignore } It 'Creates module' { @@ -112,38 +146,63 @@ Describe 'Build' { } It 'Has MAML help XML' { - "$script:testModuleOutputPath/en-US/TestModule-help.xml" | Should -Exist + [IO.Path]::Combine($script:testModuleOutputPath, "en-US", "TestModule-help.xml") | Should -Exist } } Context 'Overwrite Docs' { BeforeAll { - Write-Host "PSScriptRoot: $tempDir" + Write-Host "PSScriptRoot: $script:testModuleSource" Write-Host "OutputPath: $script:testModuleOutputPath" + $copyItemSplat = @{ + Path = "$PSScriptRoot/fixtures/DotSource.psm1" + Destination = "$script:testModuleSource/TestModule/TestModule.psm1" + Force = $true + } + # Overwrite the existing PSM1 with the dot-sourced version + Copy-Item @copyItemSplat + # Build once, and then we'll modify + $script:jobs += Start-Job -ScriptBlock { + param($testModuleSource, $outputModVerManifest) + Set-Location -Path $using:testModuleSource + # We want to load the current build of PowerShellBuild so we use a + # global variable to store the output path. + $global:PSBOutput = $global:outputModVerManifest + $global:PSBuildCompile = $false + ./build.ps1 -Task Build + } -WorkingDirectory $script:testModuleSource -ArgumentList $testModuleSource, $outputModVerManifest | Wait-Job + # Replace with a different string to test the overwrite - $script:docPath = "$tempDir/docs/en-US/Get-HelloWorld.md" + $script:docPath = [IO.Path]::Combine($script:testModuleSource, "docs", "en-US", "Get-HelloWorld.md") $script:original = Get-Content $docPath -Raw $new = $original -replace 'Hello World', 'Hello Universe' Set-Content $docPath -Value $new -Force # Update the psake file - $psakeFile = "$tempDir/psakeFile.ps1" + $psakeFile = [IO.Path]::Combine($script:testModuleSource, "psakeFile.ps1") $psakeFileContent = Get-Content $psakeFile -Raw $psakeFileContent = $psakeFileContent -replace '\$PSBPreference.Docs.Overwrite = \$false', '$PSBPreference.Docs.Overwrite = $true' Set-Content $psakeFile -Value $psakeFileContent -Force # build is PS job so psake doesn't freak out because it's nested - $script:jobs += Start-Job -Scriptblock { - Set-Location $using:tempDir - $global:PSBuildCompile = $true + $script:jobs += Start-Job -ScriptBlock { + param($testModuleSource, $outputModVerManifest) + Set-Location -Path $using:testModuleSource + # We want to load the current build of PowerShellBuild so we use a + # global variable to store the output path. + $global:PSBOutput = $global:outputModVerManifest + $global:PSBuildCompile = $false ./build.ps1 -Task Build - } | Wait-Job + } -WorkingDirectory $script:testModuleSource -ArgumentList $testModuleSource, $outputModVerManifest | Wait-Job } AfterAll { Remove-Item $script:testModuleOutputPath -Recurse -Force + $jobs | Stop-Job -ErrorAction Ignore + $jobs | Remove-Job -ErrorAction Ignore } + It 'Can Overwrite the Docs' { # Test that the file reset as expected Get-Content $script:docPath -Raw | Should -BeExactly $script:original diff --git a/tests/fixtures/DotSource.psm1 b/tests/fixtures/DotSource.psm1 new file mode 100644 index 0000000..4a6ddc9 --- /dev/null +++ b/tests/fixtures/DotSource.psm1 @@ -0,0 +1,11 @@ +# Dot source public 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/TestModule/psakeFile.ps1 b/tests/fixtures/TestModule/psakeFile.ps1 index 5007a1e..86bfa20 100644 --- a/tests/fixtures/TestModule/psakeFile.ps1 +++ b/tests/fixtures/TestModule/psakeFile.ps1 @@ -1,4 +1,5 @@ -Import-Module ../../Output/PowerShellBuild -Force +$psb = Import-Module $global:PSBOutput -Force -PassThru +Write-Host "Using PowerShellBuild version $($psb.Version)" Properties { # Pester can build the module using both scenarios @@ -33,9 +34,9 @@ Properties { $PSBPreference.Docs.Overwrite = $false } -Task default -depends Build +Task default -Depends Build -Task Build -FromModule PowerShellBuild -minimumVersion 0.5.0 +Task Build -FromModule PowerShellBuild -MinimumVersion 0.5.0