diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a30e165..9afe42c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,48 +70,67 @@ 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 with: name: build.requires.psd1 - - name: Download Build Output - uses: actions/download-artifact@v8 - with: - name: ModuleBuilder - path: output/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 + 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 + 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/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/Source/Private/SetModuleContent.ps1 b/Source/Private/SetModuleContent.ps1 index 376eac6..fc92966 100644 --- a/Source/Private/SetModuleContent.ps1 +++ b/Source/Private/SetModuleContent.ps1 @@ -1,7 +1,7 @@ function SetModuleContent { <# .SYNOPSIS - A wrapper for Set-Content that handles arrays of file paths + A wrapper for Set-Content that copies lists of files .DESCRIPTION The implementation here is strongly dependent on Build-Module doing the right thing Build-Module can optionally pass a PREFIX or SUFFIX, but otherwise only passes files @@ -15,7 +15,7 @@ function SetModuleContent { [CmdletBinding()] param( # Where to write the joined output - [Parameter(Position=0, Mandatory)] + [Parameter(Position = 0, Mandatory)] [string]$OutputPath, # Input files, the scripts that will be copied to the output path @@ -28,12 +28,13 @@ function SetModuleContent { # The working directory (allows relative paths for other values) [string]$WorkingDirectory = $pwd, - # The encoding defaults to UTF8 (or UTF8NoBom on Core) + # The encoding defaults to UTF8 (or UTF8Bom on Core) [Parameter(DontShow)] [string]$Encoding = $(if($IsCoreCLR) { "UTF8Bom" } else { "UTF8" }) ) begin { Write-Debug "SetModuleContent WorkingDirectory $WorkingDirectory" + Write-Debug "Encoding $Encoding" Push-Location $WorkingDirectory -StackName SetModuleContent $ContentStarted = $false # There has been no content yet diff --git a/Source/Public/Build-Module.ps1 b/Source/Public/Build-Module.ps1 index 800184e..c671db7 100644 --- a/Source/Public/Build-Module.ps1 +++ b/Source/Public/Build-Module.ps1 @@ -306,7 +306,7 @@ function Build-Module { Join-Path -Path $ModuleInfo.ModuleBase -ChildPath $_ | Convert-Path -ErrorAction SilentlyContinue } - $ModuleInfo.Generators | Invoke-ScriptGenerator -Path $RootModule -Overwrite + $ModuleInfo.Generators | Invoke-ScriptGenerator -Path $RootModule -Overwrite -Encoding $ModuleInfo.Encoding # This is mostly for testing ... diff --git a/Source/Public/ConvertTo-Script.ps1 b/Source/Public/ConvertTo-Script.ps1 index c267e26..ce69239 100644 --- a/Source/Public/ConvertTo-Script.ps1 +++ b/Source/Public/ConvertTo-Script.ps1 @@ -53,7 +53,12 @@ function ConvertTo-Script { # This is used to find the module manifest, # But the the script will be saved in the same location [Parameter(Mandatory, ValueFromPipelineByPropertyName)] - [string]$Path + [string]$Path, + + # File encoding for output RootModule (defaults to UTF8) + # Converted to System.Text.Encoding for PowerShell 6 (and something else for PowerShell 5) + [ValidateSet("UTF8", "UTF8Bom", "UTF8NoBom", "UTF7", "ASCII", "Unicode", "UTF32")] + [string]$Encoding = $(if ($IsCoreCLR) { "UTF8Bom" } else { "UTF8" }) ) begin { Write-Debug " ENTER: ConvertTo-Script BEGIN $Path $FunctionName" @@ -81,6 +86,8 @@ function ConvertTo-Script { return [AstVisitAction]::Continue } } + + $SetContentCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Set-Content', [System.Management.Automation.CommandTypes]::Cmdlet) Write-Debug " EXIT: ConvertTo-Script BEGIN" } process { @@ -131,7 +138,7 @@ function ConvertTo-Script { Get-Item $Path ) | CompressToBase64 -ExpandScriptName ImportBase64Module "$FunctionName @PSBoundParameters" - ) | Set-Content "$FunctionName.ps1" + ) | & $SetContentCmd -Path "$FunctionName.ps1" -Encoding $Encoding Update-ScriptFileInfo "$FunctionName.ps1" -Version $Manifest.ModuleVersion -Author $Manifest.Author -CompanyName $Manifest.CompanyName -Copyright $Manifest.Copyright -Tags $Manifest.PrivateData.PSData.Tags -ProjectUri $Manifest.PrivateData.PSData.ProjectUri -LicenseUri $Manifest.PrivateData.PSData.LicenseUri -IconUri $Manifest.PrivateData.PSData.IconUri -ReleaseNotes $Manifest.PrivateData.PSData.ReleaseNotes diff --git a/Source/Public/Invoke-ScriptGenerator.ps1 b/Source/Public/Invoke-ScriptGenerator.ps1 index dc5d0d2..dcea7b1 100644 --- a/Source/Public/Invoke-ScriptGenerator.ps1 +++ b/Source/Public/Invoke-ScriptGenerator.ps1 @@ -68,15 +68,21 @@ function Invoke-ScriptGenerator { # If set, will overwrite the Source with the generated content. # Use with care, as this will modify the source file! - [switch]$Overwrite + [switch]$Overwrite, + + # The encoding defaults to UTF8 (or UTF8Bom on Core) + [Parameter()] + [string]$Encoding = $(if($IsCoreCLR) { "UTF8Bom" } else { "UTF8" }) ) begin { $AstParam = @{} + $PSBoundParameters $null = $AstParam.Remove("Overwrite") $null = $AstParam.Remove("Generator") $null = $AstParam.Remove("Parameters") + $null = $AstParam.Remove("Encoding") $ParseResults = ConvertToAst @AstParam [StringBuilder]$Builder = $ParseResults.Ast.Extent.Text + $SetContentCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Set-Content', [System.Management.Automation.CommandTypes]::Cmdlet) } process { if (-not $PSBoundParameters.ContainsKey("Generator") -and $Parameters.ContainsKey("Generator")) { @@ -114,9 +120,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'" @@ -135,14 +143,14 @@ function Invoke-ScriptGenerator { $ParseResults = ConvertToAst -Code $Builder.ToString() -Path $ParseResults.Path # In case a Generator tries to use the actual files, update the content if ($Overwrite -and $ParseResults.Path -and $ParseResults.Path -ne "scriptblock") { - Set-Content $ParseResults.Path $Builder + & $SetContentCmd -Path $ParseResults.Path -Value $Builder -Encoding $Encoding } } } end { Write-Debug "Overwrite: $Overwrite and it's a file: $(([bool]$ParseResults.Path) -and $ParseResults.Path -ne "scriptblock") (Content is $($Builder.Length) long)" if ($Overwrite -and $ParseResults.Path -and $ParseResults.Path -ne "scriptblock") { - Set-Content $ParseResults.Path $Builder + & $SetContentCmd -Path $ParseResults.Path -Value $Builder -Encoding $Encoding } else { $Builder.ToString() } 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/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/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/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/ConvertTo-SourceLineNumber.Tests.ps1 b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 index 8f432d9..1c6075d 100644 --- a/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 +++ b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 @@ -1,19 +1,22 @@ -Describe "ConvertTo-SourceLineNumber" { +Describe "ConvertTo-SourceLineNumber" { + BeforeDiscovery { + ${global:\} = [io.path]::DirectorySeparatorChar + $TestCases = @( + @{ outputLine = 40; sourceFile = ".${\}Private${\}TestUnExportedAliases.ps1"; sourceLine = 13 } + @{ outputLine = 48; sourceFile = ".${\}Public${\}Get-Source.ps1"; sourceLine = 5 } + @{ outputLine = 56; sourceFile = ".${\}Public${\}Set-Source.ps1"; sourceLine = 3 } + ) + } # use the integration test code BeforeAll { + $DebugPreference = "Continue" Build-Module $PSScriptRoot/../Integration/Source1/build.psd1 -Passthru + $DebugPreference = "SilentlyContinue" Push-Location $PSScriptRoot -StackName ConvertTo-SourceLineNumber $global:Convert_LineNumber_ModulePath = Convert-Path "./../Integration/Result1/Source1/1.0.0/Source1.psm1" $global:Convert_LineNumber_ModuleSource = Convert-Path "./../Integration/Source1" $global:Convert_LineNumber_ModuleContent = Get-Content $global:Convert_LineNumber_ModulePath - ${global:\} = [io.path]::DirectorySeparatorChar - - $global:TestCases = @( - @{ outputLine = 40; sourceFile = ".${\}Private${\}TestUnExportedAliases.ps1"; sourceLine = 13 } - @{ outputLine = 48; sourceFile = ".${\}Public${\}Get-Source.ps1"; sourceLine = 5 } - @{ outputLine = 56; sourceFile = ".${\}Public${\}Set-Source.ps1"; sourceLine = 3 } - ) } AfterAll { Pop-Location -StackName ConvertTo-SourceLineNumber @@ -28,36 +31,38 @@ Describe "ConvertTo-SourceLineNumber" { $line = (Get-Content (Join-Path $Convert_LineNumber_ModuleSource $SourceLocation.SourceFile))[$SourceLocation.SourceLineNumber - 1] try { - $Convert_LineNumber_ModuleContent[$outputLine -1] | Should -Be $line + $Convert_LineNumber_ModuleContent[$outputLine - 1] | Should -Be $line } catch { throw "Failed to match module line $outputLine to $($SourceLocation.SourceFile) line $($SourceLocation.SourceLineNumber).`nExpected $Line`nBut got $($Convert_LineNumber_ModuleContent[$outputLine -1])" } } 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 + $line = (Select-String -Path $Convert_LineNumber_ModulePath 'function Set-Source {').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 + $SourceFile = Join-Path $Convert_LineNumber_ModuleSource (Join-Path 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 + $outputLine = (Select-String -Path $Convert_LineNumber_ModulePath "sto͞o′pĭd").LineNumber + $sourceLine = (Select-String -Path $SourceFile "sto͞o′pĭd").LineNumber - $SourceLocation = "At Set-Source, ${Convert_LineNumber_ModulePath}: line $outputLine" | Convert-LineNumber + Get-Content $Convert_LineNumber_ModulePath | Out-Host - # This test is assuming you built the code on Windows. Should Convert-LineNumber convert the path? + $sourceLine | Should -BeGreaterThan 0 -Because "the test string 'sto͞o′pĭd' is definitely found in '$SourceFile'" + $outputLine | Should -BeGreaterThan 0 -Because "the test string 'sto͞o′pĭd' should be found in the module '$Convert_LineNumber_ModulePath'" + + $SourceLocation = "At Set-Source, ${Convert_LineNumber_ModulePath}: line $outputLine" | ConvertTo-SourceLineNumber $SourceLocation.SourceFile | Should -Be ".${\}Public${\}Set-Source.ps1" $SourceLocation.SourceLineNumber | Should -Be $sourceLine } 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/Tests/Public/Move-UsingStatement.Tests.ps1 b/Tests/Public/Move-UsingStatement.Tests.ps1 new file mode 100644 index 0000000..26df63c --- /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 = Join-Path $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() -split "[\r\n]+" -match "^\s*using" | Should -Be ($ExpectedResult.Trim() -split "[\r\n]+" -match "^\s*using") + } + } + } + + 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..bb63241 --- /dev/null +++ b/Tests/Public/Update-AliasesToExport.Tests.ps1 @@ -0,0 +1,305 @@ +#requires -Module ModuleBuilder +Describe "Update-AliasesToExport" { + BeforeAll { + $ManifestPath = Join-Path $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) -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() + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + 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 + } + } + + 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 - } - } -} 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