From da9153fd1cc0c69b6cb5e3d55b71597910e5f148 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Fri, 15 May 2026 23:29:34 -0400 Subject: [PATCH 1/5] Explicitly add a Windows PowerShell job to the build matrix. There are people still using this there. --- .github/workflows/build.yml | 60 +++++++++++++++--------- Source/Public/Invoke-ScriptGenerator.ps1 | 8 ++-- Tests/Integration/Parameters.Tests.ps1 | 11 ++--- Tests/Private/InitializeBuild.Tests.ps1 | 8 ++-- Tests/Public/Add-Parameter.Tests.ps1 | 12 ++--- Tests/Public/Merge-ScriptBlock.Tests.ps1 | 32 ++++++------- build.build.ps1 | 2 +- 7 files changed, 76 insertions(+), 57 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a30e165..d678fee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,6 +70,10 @@ jobs: fail-fast: false matrix: os: [ windows-latest, ubuntu-latest, macos-latest ] + shell: [ pwsh ] + include: + - os: windows-latest + shell: powershell steps: - name: Download build.requires.psd1 uses: actions/download-artifact@v8 @@ -79,39 +83,53 @@ jobs: uses: actions/download-artifact@v8 with: name: ModuleBuilder - path: output/ModuleBuilder # /home/runner/work/ModuleBuilder/ModuleBuilder/output/ModuleBuilder + path: Modules/ModuleBuilder # /home/runner/work/ModuleBuilder/ModuleBuilder/output/ModuleBuilder - name: Download Pester Tests uses: actions/download-artifact@v8 with: name: PesterTests path: PesterTests - - name: Install Output Modules + # Avoid installing ModuleBuilder with ModuleFast, so there's only one copy + - name: Remove ModuleBuilder from build.requires shell: pwsh run: | # PowerShell - # https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-powershell#powershell-module-locations - $ModuleDestination = if ($IsWindows) { - Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'PowerShell/Modules' - } else { - Join-Path $HOME '.local/share/powershell/Modules' - } - - Get-ChildItem -Directory output -OutVariable Modules - | Move-Item -Destination { Join-Path $ModuleDestination $_.Name } -Force - - Write-Host "Installing $($Modules -join ', ') to $ModuleDestination" - Get-ChildItem -Directory $ModuleDestination - Write-Host "PSModulePath:" - $Env:PSModulePath -split ([IO.Path]::PathSeparator) | Out-Host - - # Avoid installing ModuleBuilder with ModuleFast, so there's only one copy - @(Get-Content build.requires.psd1) - | Where { $_ -notmatch "ModuleBuilder"} - | Set-Content build.requires.psd1 + @(Get-Content build.requires.psd1).Where({ $_ -notmatch "ModuleBuilder"}) | Set-Content build.requires.psd1 - name: ⚡ Install Required Modules uses: JustinGrote/ModuleFast-action@v0.0.1 + env: + MODULEFAST_DESTINATION: ${{ github.workspace }}/Modules - name: Invoke-Pester + if: matrix.shell == 'powershell' + shell: powershell + env: + MODULEFAST_DESTINATION: ${{ github.workspace }}/Modules + run: | # PowerShell + $Env:PSModulePath = $Env:MODULEFAST_DESTINATION + [IO.Path]::PathSeparator + $Env:PSModulePath + + # For the cross-platform matrix we don't need to do coverage or anything complicated + $Result = Invoke-Pester . -PassThru + @( + "## Pester Tests for ${{ matrix.os }}" + "" + $Result.Duration.ToString() + "| Total | Passed | Failed |" + "|------:|-------:|-------:|" + "| $($Result.TotalCount) | $($Result.PassedCount) | $($Result.FailedCount) |" + "" + "| Duration | Total | Passed | Failed | Skipped | Name |" + "|---------:|------:|-------:|-------:|--------:|:-----|" + @($Result.Containers).ForEach{ + "| $($_.Duration) | $($_.TotalCount) | $($_.PassedCount) | $($_.FailedCount) | $($_.SkippedCount) | $($_.Name) |" + } + ) | Out-File -FilePath $env:GITHUB_STEP_SUMMARY + - name: Invoke-Pester + if: matrix.shell == 'pwsh' shell: pwsh + env: + MODULEFAST_DESTINATION: ${{ github.workspace }}/Modules run: | # PowerShell + $Env:PSModulePath = $Env:MODULEFAST_DESTINATION + [IO.Path]::PathSeparator + $Env:PSModulePath + # For the cross-platform matrix we don't need to do coverage or anything complicated $Result = Invoke-Pester . -PassThru @( diff --git a/Source/Public/Invoke-ScriptGenerator.ps1 b/Source/Public/Invoke-ScriptGenerator.ps1 index dc5d0d2..165f1bc 100644 --- a/Source/Public/Invoke-ScriptGenerator.ps1 +++ b/Source/Public/Invoke-ScriptGenerator.ps1 @@ -114,9 +114,11 @@ function Invoke-ScriptGenerator { } # Find that generator... - $GeneratorCmd = Get-Command -Name ${Generator} -ParameterType Ast -ErrorAction Ignore <# -CommandType Function #> - | Where-Object { $_.OutputType.Name -eq "TextReplacement" -or ($_.CommandType -eq "Alias" -and $_.Definition -like "PesterMock*" ) } - | Select-Object -First 1 + $GeneratorCmd = Get-Command -Name ${Generator} -ParameterType Ast -ErrorAction Ignore <# -CommandType Function #> | + Where-Object { + $_.OutputType.Name -eq "TextReplacement" -or ($_.CommandType -eq "Alias" -and $_.Definition -like "PesterMock*" ) + } | + Select-Object -First 1 if (-not $GeneratorCmd) { Write-Error "Generator missconfiguration. Unable to find Generator = '$Generator'" diff --git a/Tests/Integration/Parameters.Tests.ps1 b/Tests/Integration/Parameters.Tests.ps1 index 2cec9c9..8ffdfc6 100644 --- a/Tests/Integration/Parameters.Tests.ps1 +++ b/Tests/Integration/Parameters.Tests.ps1 @@ -11,15 +11,14 @@ Describe "Parameters" -Tag Integration { New-Item $PSScriptRoot/Result3/Parameters/3.0.0/DeleteMe.md -ItemType File -Force Write-Host "Module Under Test:" - Get-Command Build-Module - | Get-Module -Name { $_.Source } - | Get-Item - | Out-Host + Get-Command Build-Module | + Get-Module -Name { $_.Source } | + Get-Item | + Out-Host } It "Passthru is read from the build manifest" { - Build-Module (Convert-FolderSeparator "$PSScriptRoot/Parameters/build.psd1") -Verbose -OutVariable Output - | Out-Host + Build-Module (Convert-FolderSeparator "$PSScriptRoot/Parameters/build.psd1") -Verbose -OutVariable Output | Out-Host $Output | Should -Not -BeNullOrEmpty $Output.Path | Convert-FolderSeparator | Should -Be (Convert-FolderSeparator "$PSScriptRoot/Result3/Parameters/3.0.0/Parameters.psd1") diff --git a/Tests/Private/InitializeBuild.Tests.ps1 b/Tests/Private/InitializeBuild.Tests.ps1 index 5db014b..b03d024 100644 --- a/Tests/Private/InitializeBuild.Tests.ps1 +++ b/Tests/Private/InitializeBuild.Tests.ps1 @@ -52,11 +52,11 @@ Describe "InitializeBuild" { $Result.Result.Name | Should -Be "MyModule" $Result.Result.SourceDirectories | Should -Be @("Classes", "Private", "Public") - (Convert-FolderSeparator $Result.Result.ModuleBase) - | Should -Be (Convert-FolderSeparator "TestDrive:\Source") + (Convert-FolderSeparator $Result.Result.ModuleBase) | + Should -Be (Convert-FolderSeparator "TestDrive:\Source") - (Convert-FolderSeparator $Result.Result.SourcePath) - | Should -Be (Convert-FolderSeparator "TestDrive:\Source\MyModule.psd1") + (Convert-FolderSeparator $Result.Result.SourcePath) | + Should -Be (Convert-FolderSeparator "TestDrive:\Source\MyModule.psd1") } It "Returns default values from the Build Command" { diff --git a/Tests/Public/Add-Parameter.Tests.ps1 b/Tests/Public/Add-Parameter.Tests.ps1 index da3ac2b..13d7684 100644 --- a/Tests/Public/Add-Parameter.Tests.ps1 +++ b/Tests/Public/Add-Parameter.Tests.ps1 @@ -65,12 +65,12 @@ Describe "Add-Parameter" { $showDate | Should -Not -BeNullOrEmpty - $showDate.Body.ParamBlock.Parameters.Name.VariablePath.UserPath - | Should -Be @('Format', 'ForegroundColor', 'BackgroundColor') + $showDate.Body.ParamBlock.Parameters.Name.VariablePath.UserPath | + Should -Be @('Format', 'ForegroundColor', 'BackgroundColor') $showUserName | Should -Not -BeNullOrEmpty - $showUserName.Body.ParamBlock.Parameters.Name.VariablePath.UserPath - | Should -Be @('ForegroundColor', 'BackgroundColor') + $showUserName.Body.ParamBlock.Parameters.Name.VariablePath.UserPath | + Should -Be @('ForegroundColor', 'BackgroundColor') # Get-Date Should not be modified, since it does not match the FunctionName filter $getDate = $Ast.Find({ @@ -79,8 +79,8 @@ Describe "Add-Parameter" { $node.Name -eq 'Get-Date' }, $true) $getDate | Should -Not -BeNullOrEmpty - $getDate.Body.ParamBlock.Parameters.Name.VariablePath.UserPath - | Should -Be @('Format') + $getDate.Body.ParamBlock.Parameters.Name.VariablePath.UserPath | + Should -Be @('Format') } } } diff --git a/Tests/Public/Merge-ScriptBlock.Tests.ps1 b/Tests/Public/Merge-ScriptBlock.Tests.ps1 index d15d11f..6e2941f 100644 --- a/Tests/Public/Merge-ScriptBlock.Tests.ps1 +++ b/Tests/Public/Merge-ScriptBlock.Tests.ps1 @@ -60,24 +60,24 @@ Describe "Merge-ScriptBlock" { $showDate | Should -Not -BeNullOrEmpty - $showDate.Body.EndBlock -split "`n" - | ForEach-Object { $_.Trim() } - | Select-Object -Skip 1 - | Select-Object -First 3 - | Should -Be @( - "`$ForegroundColor.ToVt() + `$BackgroundColor.ToVt(`$true) + (" - "Get-Date -Format `$Format" - ") + `"``e[0m`"" - ) + $showDate.Body.EndBlock -split "`n" | + ForEach-Object { $_.Trim() } | + Select-Object -Skip 1 | + Select-Object -First 3 | + Should -Be @( + "`$ForegroundColor.ToVt() + `$BackgroundColor.ToVt(`$true) + (" + "Get-Date -Format `$Format" + ") + `"``e[0m`"" + ) $showUserName | Should -Not -BeNullOrEmpty - $showUserName.Body.EndBlock -split "`n" - | ForEach-Object { $_.Trim() } - | Select-Object -Skip 1 - | Select-Object -First 3 - | Should -Be @( - "`$ForegroundColor.ToVt() + `$BackgroundColor.ToVt(`$true) + (" - "[Environment]::UserName" + $showUserName.Body.EndBlock -split "`n" | + ForEach-Object { $_.Trim() } | + Select-Object -Skip 1 | + Select-Object -First 3 | + Should -Be @( + "`$ForegroundColor.ToVt() + `$BackgroundColor.ToVt(`$true) + (" + "[Environment]::UserName" ") + `"``e[0m`"" ) } diff --git a/build.build.ps1 b/build.build.ps1 index 053c3ec..53e6e49 100644 --- a/build.build.ps1 +++ b/build.build.ps1 @@ -19,7 +19,7 @@ Write-Information "$($PSStyle.Foreground.BrightMagenta)build.build.ps1$($PSStyle ## Self-contained build script - can be invoked directly or via Invoke-Build if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') { - . (Convert-Path ../../[tT]asks/scripts/Invoke-Build.ps1) -File $MyInvocation.MyCommand.Path @PSBoundParameters -Result Result + & (Convert-Path ../../[tT]asks/scripts/Invoke-Build.ps1) -File $MyInvocation.MyCommand.Path @PSBoundParameters -Result Result if ($Result.Error) { $Error[-1].ScriptStackTrace | Out-Host From 666ec3e4da93e3efc6bff0662c8c77cf45875bd7 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Sat, 16 May 2026 17:13:37 -0400 Subject: [PATCH 2/5] Fix Windows PowerShell regressions in tests. --- Tests/Private/CompressToBase64.Tests.ps1 | 8 ++++---- Tests/Private/ConvertToAst.Tests.ps1 | 2 +- .../Public/ConvertTo-SourceLineNumber.Tests.ps1 | 17 +++++++---------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Tests/Private/CompressToBase64.Tests.ps1 b/Tests/Private/CompressToBase64.Tests.ps1 index 898d81f..b11bcc0 100644 --- a/Tests/Private/CompressToBase64.Tests.ps1 +++ b/Tests/Private/CompressToBase64.Tests.ps1 @@ -4,7 +4,7 @@ Describe "CompressToBase64" { Context "It compresses and encodes a file for embedding into a script" { BeforeAll { $Base64 = InModuleScope ModuleBuilder { - CompressToBase64 $PSCommandPath + CompressToBase64 (Join-Path $PSScriptRoot CompressToBase64.Tests.ps1) } } @@ -22,14 +22,14 @@ Describe "CompressToBase64" { $OutputStream.Seek(0, "Begin") $Source = [System.IO.StreamReader]::new($OutputStream, $true).ReadToEnd() - $Source | Should -Be (Get-Content $PSCommandPath -Raw) + $Source | Should -Be (Get-Content (Join-Path $PSScriptRoot CompressToBase64.Tests.ps1) -Raw) } } Context "It wraps the Base64 encoded content in the specified command" { BeforeAll { $Base64 = InModuleScope ModuleBuilder { - CompressToBase64 $PSCommandPath -ExpandScriptName ImportBase64Module + CompressToBase64 (Join-Path $PSScriptRoot CompressToBase64.Tests.ps1) -ExpandScriptName ImportBase64Module } } @@ -49,7 +49,7 @@ Describe "CompressToBase64" { Context "It wraps the Base64 encoded content in the specified scriptblock" { BeforeAll { $Base64 = InModuleScope ModuleBuilder { - Get-ChildItem $PSCommandPath | CompressToBase64 -ExpandScript { ImportBase64Module } + Get-ChildItem (Join-Path $PSScriptRoot CompressToBase64.Tests.ps1) | CompressToBase64 -ExpandScript { ImportBase64Module } } } diff --git a/Tests/Private/ConvertToAst.Tests.ps1 b/Tests/Private/ConvertToAst.Tests.ps1 index a71589c..92b668b 100644 --- a/Tests/Private/ConvertToAst.Tests.ps1 +++ b/Tests/Private/ConvertToAst.Tests.ps1 @@ -4,7 +4,7 @@ Describe "ConvertToAst" { Context "It returns a ParseResult for file paths" { BeforeAll { $ParseResult = InModuleScope ModuleBuilder { - ConvertToAst -Code $PSCommandPath + ConvertToAst -Code (Join-Path $PSScriptRoot ConvertToAst.Tests.ps1) } } diff --git a/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 index 8f432d9..7e1eb44 100644 --- a/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 +++ b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 @@ -35,29 +35,26 @@ Describe "ConvertTo-SourceLineNumber" { } It "Should throw if the SourceFile doesn't exist" { - { Convert-LineNumber -SourceFile TestDrive:/NoSuchFile -SourceLineNumber 10 } | - Should -Throw "'TestDrive:/NoSuchFile' does not exist" + { ConvertTo-SourceLineNumber -SourceFile TestDrive:${\}NoSuchFile -SourceLineNumber 10 } | + Should -Throw "'TestDrive:${\}NoSuchFile' does not exist" } It 'Should work with an error PositionMessage' { $line = Select-String -Path $Convert_LineNumber_ModulePath 'function Set-Source {' | ForEach-Object LineNumber - $SourceLocation = "At ${Convert_LineNumber_ModulePath}:$line char:17" | Convert-LineNumber - # This test is assuming you built the code on Windows. Should Convert-LineNumber convert the path? + $SourceLocation = "At ${Convert_LineNumber_ModulePath}:$line char:17" | ConvertTo-SourceLineNumber $SourceLocation.SourceFile | Should -Be ".${\}Public${\}Set-Source.ps1" $SourceLocation.SourceLineNumber | Should -Be 1 } It 'Should work with ScriptStackTrace messages' { - $SourceFile = Join-Path $Convert_LineNumber_ModuleSource Public/Set-Source.ps1 | Convert-Path - - $outputLine = Select-String -Path $Convert_LineNumber_ModulePath "sto͞o′pĭd" | % LineNumber - $sourceLine = Select-String -Path $SourceFile "sto͞o′pĭd" | % LineNumber + $SourceFile = Join-Path $Convert_LineNumber_ModuleSource (Join-Path Public Set-Source.ps1) | Convert-Path - $SourceLocation = "At Set-Source, ${Convert_LineNumber_ModulePath}: line $outputLine" | Convert-LineNumber + $outputLine = Select-String -Path $Convert_LineNumber_ModulePath "sto͞o′pĭd" | ForEach-Object LineNumber + $sourceLine = Select-String -Path $SourceFile "sto͞o′pĭd" | ForEach-Object LineNumber - # This test is assuming you built the code on Windows. Should Convert-LineNumber convert the path? + $SourceLocation = "At Set-Source, ${Convert_LineNumber_ModulePath}: line $outputLine" | ConvertTo-SourceLineNumber $SourceLocation.SourceFile | Should -Be ".${\}Public${\}Set-Source.ps1" $SourceLocation.SourceLineNumber | Should -Be $sourceLine } From 8ca33f94944f7485e5713cfabf236c618085cc86 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Mon, 18 May 2026 22:41:17 -0400 Subject: [PATCH 3/5] Add test covverage Put back the tests for: - Move-UsingStatement - Update-AliasesToExport --- Source/Public/Update-AliasesToExport.ps1 | 33 +- Tests/Public/Move-UsingStatement.Tests.ps1 | 116 +++++++ .../Public/Move-UsingStatement.Tests.ps1.old | 148 --------- Tests/Public/Update-AliasesToExport.Tests.ps1 | 308 ++++++++++++++++++ .../Update-AliasesToExport.Tests.ps1.old | 180 ---------- 5 files changed, 454 insertions(+), 331 deletions(-) create mode 100644 Tests/Public/Move-UsingStatement.Tests.ps1 delete mode 100644 Tests/Public/Move-UsingStatement.Tests.ps1.old create mode 100644 Tests/Public/Update-AliasesToExport.Tests.ps1 delete mode 100644 Tests/Public/Update-AliasesToExport.Tests.ps1.old diff --git a/Source/Public/Update-AliasesToExport.ps1 b/Source/Public/Update-AliasesToExport.ps1 index 0a11423..5532f2b 100644 --- a/Source/Public/Update-AliasesToExport.ps1 +++ b/Source/Public/Update-AliasesToExport.ps1 @@ -20,7 +20,14 @@ function Update-AliasesToExport { # The path to the module manifest that should contain the aliases [Parameter(Mandatory, ValueFromPipelineByPropertyName)] - [string]$ModuleManifest + [string]$ModuleManifest, + + # Controls what to set AliasesToExport to when no aliases are found by static analysis. + # DoNotSet: (default) leave the manifest unchanged. + # Wildcard: set AliasesToExport = '*'. + # EmptyArray: set AliasesToExport = @(). + [ValidateSet("DoNotSet", "Wildcard", "EmptyArray")] + [string]$WhenNoAliases = "DoNotSet" ) begin { # This is used only to parse the parameters to New|Set|Remove-Alias @@ -141,9 +148,29 @@ function Update-AliasesToExport { } } process { + $null = Get-Metadata -Path $ModuleManifest -PropertyName AliasesToExport -ErrorAction SilentlyContinue -ErrorVariable Failed + if ($Failed) { + Write-Warning "Can't update AliasesToExport in '$ModuleManifest' unless it's already set." + return + } + $Visitor = [AliasExportGenerator]::new() $ScriptModule.Visit($Visitor) - Update-Metadata -Path $ModuleManifest -PropertyName AliasesToExport -Value $Visitor.Aliases -WhatIf:$WhatIfPreference -Confirm:($ConfirmPreference -eq 'Low') + if ($Visitor.Aliases.Count -gt 0) { + $newValue = $Visitor.Aliases + } else { + switch ($WhenNoAliases) { + "DoNotSet" { + return + } + "Wildcard" { + $newValue = '*' + } + "EmptyArray" { + $newValue = @() + } + } + } + Update-Metadata -Path $ModuleManifest -PropertyName AliasesToExport -Value $newValue -WhatIf:$WhatIfPreference -Confirm:($ConfirmPreference -eq 'Low') } } - diff --git a/Tests/Public/Move-UsingStatement.Tests.ps1 b/Tests/Public/Move-UsingStatement.Tests.ps1 new file mode 100644 index 0000000..c284252 --- /dev/null +++ b/Tests/Public/Move-UsingStatement.Tests.ps1 @@ -0,0 +1,116 @@ +#requires -Module ModuleBuilder +Describe "Move-UsingStatement" { + Context "Moving Using Statements to the beginning of the file" { + BeforeDiscovery { + $TestCases = @( + @{ + TestCaseName = 'Moves all using statements in `n terminated files to the top' + PSM1File = "function x {`n}`n" + + "using namespace System.IO`n`n" + + "function y {`n}`n" + + "using namespace System.Drawing" + ErrorBefore = 2 + ErrorAfter = 0 + }, + @{ + TestCaseName = 'Moves all using statements in `r`n terminated files to the top' + PSM1File = "function x {`r`n}`r`n" + + "USING namespace System.IO`r`n`r`n" + + "function y {`r`n}`r`n" + + "USING namespace System.Drawing" + ErrorBefore = 2 + ErrorAfter = 0 + }, + @{ + TestCaseName = 'Prevents duplicate using statements' + PSM1File = "using namespace System.IO`r`n" + + "function x {`r`n}`r`n`r`n" + + "using namespace System.IO`r`n" + + "function y {`r`n}`r`n" + + "USING namespace System.IO" + ExpectedResult = "using namespace System.IO`r`n" + + "# using namespace System.IO`r`n" + + "function x {`r`n}`r`n`r`n" + + "# using namespace System.IO`r`n" + + "function y {`r`n}`r`n" + + "# USING namespace System.IO" + ErrorBefore = 2 + ErrorAfter = 0 + }, + @{ + TestCaseName = 'Does not change the content when there are no out-of-place using statements' + PSM1File = "using namespace System.IO`r`n`r`n" + + "using namespace System.Drawing`r`n" + + "function x {`r`n}`r`n" + + "function y {`r`n}`r`n" + ErrorBefore = 0 + ErrorAfter = 0 + }, + @{ + TestCaseName = 'Moves using statements even if types are used' + PSM1File = "function x {`r`n}`r`n" + + "using namespace System.IO`r`n`r`n" + + "function y {`r`n}`r`n" + + "using namespace System.Collections.Generic`r`n" + + "function z { [Dictionary[String,PSObject]]::new() }" + ErrorBefore = 2 + ErrorAfter = 0 + }, + @{ + TestCaseName = 'Moves using statements even when there are (other) parse errors' + PSM1File = "using namespace System.IO`r`n`r`n" + + "function x {`r`n}`r`n" + + "using namespace System.Drawing`r`n" + + "function y {`r`n}`r`n}" + ErrorBefore = 2 + ErrorAfter = 1 + } + ) + } + + It '' -TestCases $TestCases { + param($TestCaseName, $PSM1File, $ErrorBefore, $ErrorAfter, $ExpectedResult) + + $testModuleFile = "$TestDrive\MyModule.psm1" + Set-Content $testModuleFile -Value $PSM1File -Encoding UTF8 -NoNewline + + # Verify parse errors exist before applying the generator + $ErrorFound = $null + $null = [System.Management.Automation.Language.Parser]::ParseFile( + $testModuleFile, + [ref]$null, + [ref]$ErrorFound + ) + $ErrorFound.Count | Should -Be $ErrorBefore + + # Apply the generator and get the resulting text + $result = Invoke-ScriptGenerator -Path $testModuleFile -Generator Move-UsingStatement -Parameters @{} + + # Verify parse errors after applying the generator + $ErrorFound = $null + $null = [System.Management.Automation.Language.Parser]::ParseInput( + $result, + 'testfile', + [ref]$null, + [ref]$ErrorFound + ) + $ErrorFound.Count | Should -Be $ErrorAfter + + if ($ExpectedResult) { + $result.Trim() | Should -Be $ExpectedResult -Because "there should be no duplicate using statements in:`n$result" + } + } + } + + Context "When Move-UsingStatement should do nothing" { + It 'Should not change the output when there are no using statement errors' { + $PSM1File = "using namespace System.IO; function x {}" + $testModuleFile = "$TestDrive\MyModule.psm1" + Set-Content $testModuleFile -Value $PSM1File -Encoding UTF8 -NoNewline + + $result = Invoke-ScriptGenerator -Path $testModuleFile -Generator Move-UsingStatement -Parameters @{} + + $result.Trim() | Should -Be $PSM1File.Trim() + } + } +} diff --git a/Tests/Public/Move-UsingStatement.Tests.ps1.old b/Tests/Public/Move-UsingStatement.Tests.ps1.old deleted file mode 100644 index 6216599..0000000 --- a/Tests/Public/Move-UsingStatement.Tests.ps1.old +++ /dev/null @@ -1,148 +0,0 @@ -#requires -Module ModuleBuilder -Describe "Move-UsingStatement" { - BeforeAll { - $CommandInfo = InModuleScope ModuleBuilder { Get-Command Move-UsingStatement } - } - - Context "Necessary Parameters" { - - It 'has a mandatory InputObject parameter' { - $AST = $CommandInfo.Parameters['InputObject'] - $AST | Should -Not -BeNullOrEmpty - $AST.ParameterType | Should -Be ([System.Management.Automation.Language.Ast]) - $AST.Attributes.Where{ $_ -is [Parameter] }.Mandatory | Should -Be $true - } - } - - Context "Moving Using Statements to the beginning of the file" { - BeforeAll { - $MoveUsingStatementsCmd = InModuleScope ModuleBuilder { - $null = Mock Write-Warning { } - { param($RootModule) - ConvertToAst $RootModule | MoveUsingStatements - } - } - - $TestCases = @( - @{ - TestCaseName = 'Moves all using statements in `n terminated files to the top' - PSM1File = "function x {`n}`n" + - "using namespace System.IO`n`n" + #UsingMustBeAtStartOfScript - "function y {`n}`n" + - "using namespace System.Drawing" #UsingMustBeAtStartOfScript - ErrorBefore = 2 - ErrorAfter = 0 - }, - @{ - TestCaseName = 'Moves all using statements in`r`n terminated files to the top' - PSM1File = "function x {`r`n}`r`n" + - "USING namespace System.IO`r`n`r`n" + #UsingMustBeAtStartOfScript - "function y {`r`n}`r`n" + - "USING namespace System.Drawing" #UsingMustBeAtStartOfScript - ErrorBefore = 2 - ErrorAfter = 0 - }, - @{ - TestCaseName = 'Prevents duplicate using statements' - PSM1File = "using namespace System.IO`r`n" + #UsingMustBeAtStartOfScript - "function x {`r`n}`r`n`r`n" + - "using namespace System.IO`r`n" + #UsingMustBeAtStartOfScript - "function y {`r`n}`r`n" + - "USING namespace System.IO" #UsingMustBeAtStartOfScript - ExpectedResult = "using namespace System.IO`r`n" + - "#using namespace System.IO`r`n" + - "function x {`r`n}`r`n`r`n" + - "#using namespace System.IO`r`n" + - "function y {`r`n}`r`n" + - "#USING namespace System.IO" - ErrorBefore = 2 - ErrorAfter = 0 - }, - @{ - TestCaseName = 'Does not change the content again if there are no out-of-place using statements' - PSM1File = "using namespace System.IO`r`n`r`n" + - "using namespace System.Drawing`r`n" + - "function x {`r`n}`r`n" + - "function y {`r`n}`r`n" - ErrorBefore = 0 - ErrorAfter = 0 - }, - @{ - TestCaseName = 'Moves using statements even if types are used' - PSM1File = "function x {`r`n}`r`n" + - "using namespace System.IO`r`n`r`n" + #UsingMustBeAtStartOfScript - "function y {`r`n}`r`n" + - "using namespace System.Collections.Generic" + #UsingMustBeAtStartOfScript - "function z { [Dictionary[String,PSObject]]::new() }" #TypeNotFound - ErrorBefore = 3 - ErrorAfter = 0 - }, - @{ - TestCaseName = 'Moves using statements even when there are (other) parse errors' - PSM1File = "using namespace System.IO`r`n`r`n" + - "function x {`r`n}`r`n" + - "using namespace System.Drawing`r`n" + # UsingMustBeAtStartOfScript - "function y {`r`n}`r`n}" # Extra } at the end - ErrorBefore = 2 - ErrorAfter = 1 - } - ) - } - - It '' -TestCases $TestCases { - param($TestCaseName, $PSM1File, $ErrorBefore, $ErrorAfter, $ExpectedResult) - - $testModuleFile = "$TestDrive/MyModule.psm1" - Set-Content $testModuleFile -value $PSM1File -Encoding UTF8 - # Before - $ErrorFound = $null - $null = [System.Management.Automation.Language.Parser]::ParseFile( - $testModuleFile, - [ref]$null, - [ref]$ErrorFound - ) - $ErrorFound.Count | Should -Be $ErrorBefore - - # After - &$MoveUsingStatementCmd -RootModule $testModuleFile - - $null = [System.Management.Automation.Language.Parser]::ParseFile( - $testModuleFile, - [ref]$null, - [ref]$ErrorFound - ) - $ErrorFound.Count | Should -Be $ErrorAfter - if ($ExpectedResult) { - $ActualResult = Get-Content $testModuleFile -Raw - $ActualResult.Trim() | Should -Be $ExpectedResult -Because "there should be no duplicate using statements in:`n$ActualResult" - } - } - } - - Context "When MoveUsingStatements should do nothing" { - BeforeAll { - $MoveUsingStatementsCmd = InModuleScope ModuleBuilder { - $null = Mock Write-Warning {} - $null = Mock Set-Content {} - $null = Mock Write-Debug {} -ParameterFilter { $Message -eq "No using statement errors found." } - - { param($RootModule) - ConvertToAst $RootModule | MoveUsingStatements - } - } - } - - It 'Should not do anything when there are no using statement errors' { - $testModuleFile = "$TestDrive\MyModule.psm1" - $PSM1File = "using namespace System.IO; function x {}" - Set-Content $testModuleFile -value $PSM1File -Encoding UTF8 - - &$MoveUsingStatementCmd -RootModule $testModuleFile -Debug - - (Get-Content -Raw $testModuleFile).Trim() | Should -Be $PSM1File - - Assert-MockCalled -CommandName Set-Content -Times 0 -ModuleName ModuleBuilder - Assert-MockCalled -CommandName Write-Debug -Times 1 -ModuleName ModuleBuilder - } - } -} diff --git a/Tests/Public/Update-AliasesToExport.Tests.ps1 b/Tests/Public/Update-AliasesToExport.Tests.ps1 new file mode 100644 index 0000000..d6fe60c --- /dev/null +++ b/Tests/Public/Update-AliasesToExport.Tests.ps1 @@ -0,0 +1,308 @@ +#requires -Module ModuleBuilder +Describe "Update-AliasesToExport" { + BeforeAll { + $ManifestPath = "$TestDrive\TestModule.psd1" + } + + Context "Parsing [Alias()] attributes on functions" { + BeforeEach { + New-ModuleManifest -Path $ManifestPath -AliasesToExport @() + } + + It "Returns a collection of aliases from the [Alias()] attribute" { + Invoke-ScriptGenerator -Code { + function Test-Alias { + [Alias("Foo", "Bar", "Alias")] + param() + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@("Foo", "Bar", "Alias") | Sort-Object) + } + + It "Parses only top-level functions (skips nested function aliases)" { + Invoke-ScriptGenerator -Code { + function Test-Alias { + [Alias("TA", "TAlias")] + param() + } + + function TestAlias { + [Alias("T")] + param() + + # This nested function's alias should NOT be exported + function Test-Negative { + [Alias("TN")] + param() + } + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -HaveCount 3 + $aliases | Should -BeIn @("TA", "TAlias", "T") + "TN" | Should -Not -BeIn $aliases + } + } + + Context "Parsing New-Alias" { + BeforeEach { + New-ModuleManifest -Path $ManifestPath -AliasesToExport @() + } + + It "Parses alias names regardless of parameter order" { + Invoke-ScriptGenerator -Code { + New-Alias -N 'Alias1' -Va 'Write-Verbose' + New-Alias -Value 'Write-Verbose' -Name 'Alias2' + New-Alias Alias3 Write-Verbose + New-Alias -Value 'Write-Verbose' 'Alias4' + New-Alias 'Alias5' -Value 'Write-Verbose' + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@('Alias1', 'Alias2', 'Alias3', 'Alias4', 'Alias5') | Sort-Object) + } + + It "Ignores aliases defined in nested function scope" { + Invoke-ScriptGenerator -Code { + New-Alias -Name 'Alias1' -Value 'Write-Verbose' + New-Alias -Value 'Write-Verbose' -Name 'Alias2' + New-Alias 'Alias3' 'Write-Verbose' + function Get-Something { + param() + New-Alias -Name Alias4 -Value 'Write-Verbose' + New-Alias -Name Alias5 -Va 'Write-Verbose' + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@('Alias1', 'Alias2', 'Alias3') | Sort-Object) + } + + It "Ignores aliases that have global scope" { + Invoke-ScriptGenerator -Code { + New-Alias -Name Alias1 -Scope Global -Value Write-Verbose + New-Alias -Scope Global -Value 'Write-Verbose' -Name 'Alias2' + New-Alias -Sc Global 'Alias3' 'Write-Verbose' + New-Alias -Va 'Write-Verbose' 'Alias4' -S Global + New-Alias Alias5 -Value 'Write-Verbose' -Scope Global + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -BeNullOrEmpty + } + } + + Context "Parsing Set-Alias" { + BeforeEach { + New-ModuleManifest -Path $ManifestPath -AliasesToExport @() + } + + It "Parses alias names regardless of parameter order" { + Invoke-ScriptGenerator -Code { + Set-Alias -Name Alias1 -Value Write-Verbose + Set-Alias -Va 'Write-Verbose' -N 'Alias2' + Set-Alias Alias3 Write-Verbose + Set-Alias -Va 'Write-Verbose' 'Alias4' + Set-Alias 'Alias5' -Value 'Write-Verbose' + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@('Alias1', 'Alias2', 'Alias3', 'Alias4', 'Alias5') | Sort-Object) + } + + It "Ignores aliases defined in nested function scope" { + Invoke-ScriptGenerator -Code { + Set-Alias -Name 'Alias1' -Value 'Write-Verbose' + Set-Alias -Value 'Write-Verbose' -Name 'Alias2' + Set-Alias 'Alias3' 'Write-Verbose' + function Get-Something { + param() + Set-Alias -Name 'Alias4' -Value 'Write-Verbose' + Set-Alias -Name 'Alias5' -Value 'Write-Verbose' + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@('Alias1', 'Alias2', 'Alias3') | Sort-Object) + } + + It "Ignores aliases that have global scope" { + Invoke-ScriptGenerator -Code { + Set-Alias -N 'Alias1' -Scope Global -Value 'Write-Verbose' + Set-Alias -Scope Global -Value 'Write-Verbose' -Name 'Alias2' + Set-Alias -Sc Global Alias3 Write-Verbose + Set-Alias -Va 'Write-Verbose' 'Alias4' -Sc Global + Set-Alias 'Alias5' -Value 'Write-Verbose' -Scope Global + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -BeNullOrEmpty + } + + # It "Detects variable Name as dynamic alias generation and sets AliasesToExport = '*'" { + # Invoke-ScriptGenerator -Code { + # $taskAlias = "my-task" + # Set-Alias -Name $taskAlias -Value 'Write-Verbose' + # } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } -WarningVariable warnings 3>$null + + # $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + # $aliases | Should -Be '*' + # $warnings | Should -Not -BeNullOrEmpty + # } + + # It "Detects dynamic alias generation inside ForEach-Object and sets AliasesToExport = '*'" { + # Invoke-ScriptGenerator -Code { + # @('a', 'b') | ForEach-Object { + # $taskAlias = "task-$_" + # Set-Alias -Name $taskAlias -Value 'Write-Verbose' + # } + # } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } -WarningVariable warnings 3>$null + + # $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + # $aliases | Should -Be '*' + # $warnings | Should -Not -BeNullOrEmpty + # } + + It "Does NOT flag Set-Alias with variable Name inside a function definition as dynamic" { + Invoke-ScriptGenerator -Code { + Set-Alias 'TopAlias' 'Write-Verbose' + function Set-DynamicAlias { + param($name) + Set-Alias -Name $name -Value 'Write-Verbose' + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } -WarningVariable warnings 3>$null + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -Be 'TopAlias' + $warnings | Should -BeNullOrEmpty + } + } + + Context "Remove-Alias cancels Alias exports" { + BeforeEach { + New-ModuleManifest -Path $ManifestPath -AliasesToExport @() + } + + It "Parses Remove-Alias regardless of parameter order" { + Invoke-ScriptGenerator -Code { + New-Alias -Name Alias1 -Value Write-Verbose + Set-Alias -Value 'Write-Verbose' -Name 'Alias2' + New-Alias Alias3 Write-Verbose + Set-Alias -Value 'Write-Verbose' 'Alias4' + Set-Alias 'Alias5' -Value 'Write-Verbose' + Remove-Alias Alias1 + Remove-Alias -Name Alias2 + Remove-Alias -N Alias5 + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@('Alias3', 'Alias4') | Sort-Object) + } + + It "Ignores Remove-Alias in nested function scopes" { + Invoke-ScriptGenerator -Code { + Set-Alias -Name 'Alias1' -Value 'Write-Verbose' + New-Alias -Value 'Write-Verbose' -Name 'Alias2' + Set-Alias 'Alias3' 'Write-Verbose' + function Get-Something { + param() + Set-Alias -Name 'Alias4' -Value 'Write-Verbose' + Set-Alias -Name 'Alias5' -Value 'Write-Verbose' + Remove-Alias -Name Alias1 + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@('Alias1', 'Alias2', 'Alias3') | Sort-Object) + } + + It "Does not fail when removing an alias that was already global scope (never added)" { + Invoke-ScriptGenerator -Code { + Set-Alias -Name Alias1 -Scope Global -Value Write-Verbose + Remove-Alias -Name Alias1 + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -BeNullOrEmpty + } + } + + Context "When AliasesToExport is missing in the manifest" { + It "Writes a warning and does not throw" { + # Minimal manifest without AliasesToExport + New-ModuleManifest -Path $ManifestPath + (Get-Content $ManifestPath).ForEach{ + if ($_ -match 'AliasesToExport') { + '# ' + $_ + } else { + $_ + } + } | Set-Content $ManifestPath + + try { + Invoke-ScriptGenerator -Code { + function Test-Alias { + [Alias("TA")] param() + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } -WarningAction Stop + } catch { + $FAILURE = $_ + } + $FAILURE | Should -Match 'the preference variable "WarningPreference" or common parameter is set to Stop' + $FAILURE | Should -Match "Can't update AliasesToExport" + } + } + + Context "WhenNoAliases parameter" { + BeforeEach { + New-ModuleManifest -Path $ManifestPath -AliasesToExport @('ExistingAlias') + } + + It "Does not update the manifest by default (DoNotSet) when no aliases are found" { + Invoke-ScriptGenerator -Code { + function Get-Something { + param() + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -Be 'ExistingAlias' + } + + It "Sets AliasesToExport = '*' when WhenNoAliases = 'Wildcard' and no aliases found" { + Invoke-ScriptGenerator -Code { + function Get-Something { + param() + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath; WhenNoAliases = 'Wildcard' } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -Be '*' + } + + It "Sets AliasesToExport = @() when WhenNoAliases = 'EmptyArray' and no aliases found" { + Invoke-ScriptGenerator -Code { + function Get-Something { + param() + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath; WhenNoAliases = 'EmptyArray' } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -BeNullOrEmpty + } + + It "Always updates with found static aliases regardless of WhenNoAliases" { + Invoke-ScriptGenerator -Code { + function Test-Alias { + [Alias("TA")] param() + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath; WhenNoAliases = 'DoNotSet' } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -Be 'TA' + } + } +} diff --git a/Tests/Public/Update-AliasesToExport.Tests.ps1.old b/Tests/Public/Update-AliasesToExport.Tests.ps1.old deleted file mode 100644 index e9d2156..0000000 --- a/Tests/Public/Update-AliasesToExport.Tests.ps1.old +++ /dev/null @@ -1,180 +0,0 @@ -#requires -Module ModuleBuilder -Describe "GetCommandAlias" { - BeforeAll { - $CommandInfo = InModuleScope ModuleBuilder { Get-Command GetCommandAlias } - } - - Context "Mandatory Parameter" { - It 'has a mandatory AST parameter' { - $AST = $CommandInfo.Parameters['AST'] - $AST | Should -Not -BeNullOrEmpty - $AST.ParameterType | Should -Be ([System.Management.Automation.Language.Ast]) - $AST.Attributes.Where{ $_ -is [Parameter] }.Mandatory | Should -Be $true - } - - } - - Context "Parsing Alias Parameters" { - # It used to return a hashtable, but we no longer care what the alias points to - It "Returns a collection of aliases" { - $Result = &$CommandInfo -Ast { - function Test-Alias { - [Alias("Foo","Bar","Alias")] - param() - } - }.Ast - - $Result | Should -Be @("Foo", "Bar", "Alias") - } - - It "Parses only top-level functions, and returns them in order" { - $Result = &$CommandInfo -Ast { - function Test-Alias { - [Alias("TA", "TAlias")] - param() - } - - function TestAlias { - [Alias("T")] - param() - - # This should not return - function Test-Negative { - [Alias("TN")] - param() - } - } - }.Ast - - $Result | Should -Be "TA","TAlias", "T" - } - } - - Context "Parsing New-Alias" { - It "Parses alias names regardless of parameter order" { - $Result = &$CommandInfo -Ast { - New-Alias -N 'Alias1' -Va 'Write-Verbose' - New-Alias -Value 'Write-Verbose' -Name 'Alias2' - New-Alias Alias3 Write-Verbose - New-Alias -Value 'Write-Verbose' 'Alias4' - New-Alias 'Alias5' -Value 'Write-Verbose' - }.Ast - - $Result | Should -Be 'Alias1', 'Alias2', 'Alias3', 'Alias4', 'Alias5' - } - - It "Ignores aliases defined in nested function scope" { - $Result = &$CommandInfo -Ast { - New-Alias -Name 'Alias1' -Value 'Write-Verbose' - New-Alias -Value 'Write-Verbose' -Name 'Alias2' - New-Alias 'Alias3' 'Write-Verbose' - function Get-Something { - param() - - New-Alias -Name Alias4 -Value 'Write-Verbose' - New-Alias -Name Alias5 -Va 'Write-Verbose' - } - }.Ast - - $Result | Should -Be 'Alias1', 'Alias2', 'Alias3' - } - - It "Ignores aliases that already have global scope" { - $Result = &$CommandInfo -Ast { - New-Alias -Name Alias1 -Scope Global -Value Write-Verbose - New-Alias -Scope Global -Value 'Write-Verbose' -Name 'Alias2' - New-Alias -Sc Global 'Alias3' 'Write-Verbose' - New-Alias -Va 'Write-Verbose' 'Alias4' -S Global - New-Alias Alias5 -Value 'Write-Verbose' -Scope Global - }.Ast - - $Result | Should -BeNullOrEmpty - } - } - - Context "Parsing Set-Alias" { - It "Parses alias names regardless of parameter order" { - $Result = &$CommandInfo -Ast { - Set-Alias -Name Alias1 -Value Write-Verbose - Set-Alias -Va 'Write-Verbose' -N 'Alias2' - Set-Alias Alias3 Write-Verbose - Set-Alias -Va 'Write-Verbose' 'Alias4' - Set-Alias 'Alias5' -Value 'Write-Verbose' - }.Ast - - $Result | Should -Be 'Alias1', 'Alias2', 'Alias3', 'Alias4', 'Alias5' - } - - It "Ignores aliases defined in nested function scope" { - $Result = &$CommandInfo -Ast { - Set-Alias -Name 'Alias1' -Value 'Write-Verbose' - Set-Alias -Value 'Write-Verbose' -Name 'Alias2' - Set-Alias 'Alias3' 'Write-Verbose' - function Get-Something { - param() - - Set-Alias -Name 'Alias4' -Value 'Write-Verbose' - Set-Alias -Name 'Alias5' -Value 'Write-Verbose' - } - }.Ast - - $Result | Should -Be 'Alias1', 'Alias2', 'Alias3' - } - - It "Ignores aliases that already have global scope" { - $Result = &$CommandInfo -Ast { - Set-Alias -N 'Alias1' -Scope Global -Value 'Write-Verbose' - Set-Alias -Scope Global -Value 'Write-Verbose' -Name 'Alias2' - Set-Alias -Sc Global Alias3 Write-Verbose - Set-Alias -Va 'Write-Verbose' 'Alias4' -Sc Global - Set-Alias 'Alias5' -Value 'Write-Verbose' -Scope Global - }.Ast - - $Result | Should -BeNullOrEmpty - } - } - - - Context "Remove-Alias cancels Alias exports" { - It "Parses parameters regardless of name" { - $Result = &$CommandInfo -Ast { - New-Alias -Name Alias1 -Value Write-Verbose - Set-Alias -Value 'Write-Verbose' -Name 'Alias2' - New-Alias Alias3 Write-Verbose - Set-Alias -Value 'Write-Verbose' 'Alias4' - Set-Alias 'Alias5' -Value 'Write-Verbose' - Remove-Alias Alias1 - Remove-Alias -Name Alias2 - Remove-Alias -N Alias5 - }.Ast - - $Result | Should -Be 'Alias3', 'Alias4' - } - - It "Ignores removals in function scopes" { - $Result = &$CommandInfo -Ast { - Set-Alias -Name 'Alias1' -Value 'Write-Verbose' - New-Alias -Value 'Write-Verbose' -Name 'Alias2' - Set-Alias 'Alias3' 'Write-Verbose' - function Get-Something { - param() - - Set-Alias -Name 'Alias4' -Value 'Write-Verbose' - Set-Alias -Name 'Alias5' -Value 'Write-Verbose' - Remove-Alias -Name Alias1 - } - }.Ast - - $Result | Should -Be 'Alias1', 'Alias2', 'Alias3' - } - - It "Does not fail when removing aliases that were ignored because of global scope" { - $Result = &$CommandInfo -Ast { - Set-Alias -Name Alias1 -Scope Global -Value Write-Verbose - Remove-Alias -Name Alias1 - }.Ast - - $Result | Should -BeNullOrEmpty - } - } -} From 67775a3c41b5f05838743bf9461d84d8032a59ca Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Tue, 19 May 2026 00:06:06 -0400 Subject: [PATCH 4/5] Fix more WIndows PS problems --- Source/Private/CompressToBase64.ps1 | 21 ++++++++---- Tests/Public/Move-UsingStatement.Tests.ps1 | 4 +-- Tests/Public/Update-AliasesToExport.Tests.ps1 | 33 +++++++++---------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/Source/Private/CompressToBase64.ps1 b/Source/Private/CompressToBase64.ps1 index ff02d83..a68eb20 100644 --- a/Source/Private/CompressToBase64.ps1 +++ b/Source/Private/CompressToBase64.ps1 @@ -36,12 +36,21 @@ function CompressToBase64 { process { foreach ($File in $Path | Convert-Path) { $Source = [System.IO.MemoryStream][System.IO.File]::ReadAllBytes($File) - $OutputStream = [System.IO.Compression.DeflateStream]::new( - [System.IO.MemoryStream]::new(), - [System.IO.Compression.CompressionMode]::Compress) - $Source.CopyTo($OutputStream) - $OutputStream.Flush() - $ByteArray = $OutputStream.BaseStream.ToArray() + # Write-Debug "Read $($Source.Length) bytes from $File" + + $MemoryStream = [System.IO.MemoryStream]::new() + $DeflateStream = [System.IO.Compression.DeflateStream]::new( + $MemoryStream, + [System.IO.Compression.CompressionMode]::Compress, + $true) + $Source.CopyTo($DeflateStream) + # Framework 4.x (Windows PS) doesn't flush until we close the DeflateStream + $DeflateStream.Dispose() + $ByteArray = $MemoryStream.ToArray() + $MemoryStream.Dispose() + $Source.Dispose() + # Write-Debug "Compressed to $($ByteArray.Length) bytes" + if (!$ExpandScript) { [Convert]::ToBase64String($ByteArray) } else { diff --git a/Tests/Public/Move-UsingStatement.Tests.ps1 b/Tests/Public/Move-UsingStatement.Tests.ps1 index c284252..26df63c 100644 --- a/Tests/Public/Move-UsingStatement.Tests.ps1 +++ b/Tests/Public/Move-UsingStatement.Tests.ps1 @@ -71,7 +71,7 @@ Describe "Move-UsingStatement" { It '' -TestCases $TestCases { param($TestCaseName, $PSM1File, $ErrorBefore, $ErrorAfter, $ExpectedResult) - $testModuleFile = "$TestDrive\MyModule.psm1" + $testModuleFile = Join-Path $TestDrive "MyModule.psm1" Set-Content $testModuleFile -Value $PSM1File -Encoding UTF8 -NoNewline # Verify parse errors exist before applying the generator @@ -97,7 +97,7 @@ Describe "Move-UsingStatement" { $ErrorFound.Count | Should -Be $ErrorAfter if ($ExpectedResult) { - $result.Trim() | Should -Be $ExpectedResult -Because "there should be no duplicate using statements in:`n$result" + $result.Trim() -split "[\r\n]+" -match "^\s*using" | Should -Be ($ExpectedResult.Trim() -split "[\r\n]+" -match "^\s*using") } } } diff --git a/Tests/Public/Update-AliasesToExport.Tests.ps1 b/Tests/Public/Update-AliasesToExport.Tests.ps1 index d6fe60c..bb63241 100644 --- a/Tests/Public/Update-AliasesToExport.Tests.ps1 +++ b/Tests/Public/Update-AliasesToExport.Tests.ps1 @@ -1,7 +1,7 @@ #requires -Module ModuleBuilder Describe "Update-AliasesToExport" { BeforeAll { - $ManifestPath = "$TestDrive\TestModule.psd1" + $ManifestPath = Join-Path $TestDrive "TestModule.psd1" } Context "Parsing [Alias()] attributes on functions" { @@ -234,25 +234,22 @@ Describe "Update-AliasesToExport" { It "Writes a warning and does not throw" { # Minimal manifest without AliasesToExport New-ModuleManifest -Path $ManifestPath - (Get-Content $ManifestPath).ForEach{ - if ($_ -match 'AliasesToExport') { - '# ' + $_ - } else { - $_ + (Get-Content $ManifestPath) -replace "^(.*AliasesToExport.*)$", '# $1' | Set-Content $ManifestPath + + Mock Write-Warning -ModuleName ModuleBuilder + Mock Update-Metadata -ModuleName ModuleBuilder + + Invoke-ScriptGenerator -Code { + function Test-Alias { + [Alias("TA")] param() } - } | Set-Content $ManifestPath + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } - try { - Invoke-ScriptGenerator -Code { - function Test-Alias { - [Alias("TA")] param() - } - } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } -WarningAction Stop - } catch { - $FAILURE = $_ - } - $FAILURE | Should -Match 'the preference variable "WarningPreference" or common parameter is set to Stop' - $FAILURE | Should -Match "Can't update AliasesToExport" + Assert-MockCalled Write-Warning -ModuleName ModuleBuilder -Exactly 1 -Scope It + # It does not even try to update the metadata + Assert-MockCalled Update-Metadata -ModuleName ModuleBuilder -Exactly 0 -Scope It + # It does not, in fact, update the AliasesToExport + Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport -ErrorAction Ignore | Should -BeNullOrEmpty } } From f570a54b2543e893dc3b9d6235bba063f8c670f8 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Tue, 19 May 2026 00:53:56 -0400 Subject: [PATCH 5/5] Update build and fix ModuleFast --- .github/workflows/build.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d678fee..9afe42c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,11 +79,6 @@ jobs: uses: actions/download-artifact@v8 with: name: build.requires.psd1 - - name: Download Build Output - uses: actions/download-artifact@v8 - with: - name: ModuleBuilder - path: Modules/ModuleBuilder # /home/runner/work/ModuleBuilder/ModuleBuilder/output/ModuleBuilder - name: Download Pester Tests uses: actions/download-artifact@v8 with: @@ -95,9 +90,15 @@ jobs: run: | # PowerShell @(Get-Content build.requires.psd1).Where({ $_ -notmatch "ModuleBuilder"}) | Set-Content build.requires.psd1 - name: ⚡ Install Required Modules - uses: JustinGrote/ModuleFast-action@v0.0.1 + uses: JustinGrote/ModuleFast-action@v1.0.1 env: MODULEFAST_DESTINATION: ${{ github.workspace }}/Modules + # Copy over the build output AFTER Install-ModuleFast, because it's caching my build output :( + - name: Download Build Output + uses: actions/download-artifact@v8 + with: + name: ModuleBuilder + path: Modules/ModuleBuilder # /home/runner/work/ModuleBuilder/ModuleBuilder/output/ModuleBuilder - name: Invoke-Pester if: matrix.shell == 'powershell' shell: powershell