diff --git a/.gitignore b/.gitignore index 65affa9a7..b362f480e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ step-templates/*.sh step-templates/*.py /.vs !.vscode +*copy.ps1 diff --git a/step-templates/tests/Invoke-PesterTests.ps1 b/step-templates/tests/Invoke-PesterTests.ps1 index 30e81c545..f215274fb 100644 --- a/step-templates/tests/Invoke-PesterTests.ps1 +++ b/step-templates/tests/Invoke-PesterTests.ps1 @@ -1,12 +1,18 @@ +param( + [string] $Filter = "*" +) + $ErrorActionPreference = "Stop"; Set-StrictMode -Version "Latest"; $thisScript = $MyInvocation.MyCommand.Path; $thisFolder = [System.IO.Path]::GetDirectoryName($thisScript); $rootFolder = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($thisFolder, "..", "..")); # Adjust to always point to the root -$testFiles = Get-ChildItem -Path "$thisFolder" -Filter "*.tests.ps1" -Recurse +$sharedRunner = Join-Path $rootFolder "tools" "Invoke-SharedPesterTests.ps1" function Unpack-Scripts-Under-Test { + $testFiles = Get-ChildItem -Path "$thisFolder" -Filter "*.tests.ps1" -Recurse + foreach ($testFile in $testFiles) { $baseName = $testFile.BaseName -replace "\.ScriptBody.Tests$" $scriptFileName = "$baseName.ScriptBody.ps1" @@ -35,28 +41,10 @@ function Unpack-Scripts-Under-Test { } } -function Import-Pester { - # Attempt to use local Pester module, fallback to global if not found - try { - $packagesFolder = [System.IO.Path]::Combine($rootFolder, "packages") - $pester3Path = [System.IO.Path]::Combine($packagesFolder, "Pester\tools\Pester") - # Import the specific version of Pester 3.4.0 - Import-Module -Name $pester3Path -RequiredVersion 3.4.0 -ErrorAction Stop - } catch { - Write-Host "Using globally installed Pester module version 3.4.0." - # Specify the exact version of Pester 3.x you have installed - Import-Module -Name Pester -RequiredVersion 3.4.0 -ErrorAction Stop - } -} - -function Run-Tests { - # Find and run all Pester test files in the tests directory - foreach ($testFile in $testFiles) { - Write-Host "Running tests in: $($testFile.FullName)" - Invoke-Pester -Path $testFile.FullName - } -} - -Import-Pester -Unpack-Scripts-Under-Test -Run-Tests +& $sharedRunner ` + -TestRoot $thisFolder ` + -Filter $Filter ` + -BeforeRun ${function:Unpack-Scripts-Under-Test} ` + -UsePassThruFailureCheck ` + -PreferredPesterVersion "3.4.0" ` + -SuiteName "step-templates" diff --git a/step-templates/tests/sql-backup-database.ScriptBody.Tests.ps1 b/step-templates/tests/sql-backup-database.ScriptBody.Tests.ps1 index a13970704..214249736 100644 --- a/step-templates/tests/sql-backup-database.ScriptBody.Tests.ps1 +++ b/step-templates/tests/sql-backup-database.ScriptBody.Tests.ps1 @@ -1,7 +1,7 @@ $ErrorActionPreference = "Stop"; Set-StrictMode -Version "Latest"; -. "$PSScriptRoot\..\sql-backup-database.ScriptBody.ps1" +. (Join-Path $PSScriptRoot ".." "sql-backup-database.ScriptBody.ps1") function SetupTestEnvironment { param( @@ -79,7 +79,7 @@ function SetupTestEnvironment { Describe "ApplyRetentionPolicy Tests" { BeforeAll { - $script:BackupDirectory = "C:\Backups" + $script:BackupDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "OctopusDeployLibrary-SqlBackupTests" $script:DatabaseName = "ExampleDB" $script:StartDate = Get-Date $script:timestampFormat = "yyyy-MM-dd-HHmmss" diff --git a/step-templates/tests/windows-scheduled-task-create.ScriptBody.Tests.ps1 b/step-templates/tests/windows-scheduled-task-create.ScriptBody.Tests.ps1 index ebb8d29d7..205169f50 100644 --- a/step-templates/tests/windows-scheduled-task-create.ScriptBody.Tests.ps1 +++ b/step-templates/tests/windows-scheduled-task-create.ScriptBody.Tests.ps1 @@ -1,7 +1,7 @@ $ErrorActionPreference = "Stop"; Set-StrictMode -Version "Latest"; -. "$PSScriptRoot\..\windows-scheduled-task-create.ScriptBody.ps1" +. (Join-Path $PSScriptRoot ".." "windows-scheduled-task-create.ScriptBody.ps1") Describe "Create-ScheduledTask" { diff --git a/tools/Invoke-SharedPesterTests.ps1 b/tools/Invoke-SharedPesterTests.ps1 new file mode 100644 index 000000000..87f664dbd --- /dev/null +++ b/tools/Invoke-SharedPesterTests.ps1 @@ -0,0 +1,133 @@ +param( + [Parameter(Mandatory = $true)] + [string] $TestRoot, + [string] $Filter = "*", + [scriptblock] $BeforeRun, + [string[]] $ImportModules = @(), + [switch] $UsePassThruFailureCheck, + [string] $PreferredPesterVersion, + [string] $SuiteName = "tests" +) + +$ErrorActionPreference = "Stop"; +Set-StrictMode -Version "Latest"; + +$testRootPath = [System.IO.Path]::GetFullPath($TestRoot) +$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..")) +$originalSystemRoot = $env:SystemRoot +$originalTemp = $env:TEMP + +function Get-PesterModuleSpec { + $packagesFolder = Join-Path $repoRoot "packages" + $attempts = @() + $localPesterPaths = @() + + if ($PreferredPesterVersion) { + $localPesterPaths += (Join-Path $packagesFolder ("Pester.{0}" -f $PreferredPesterVersion) "tools" "Pester") + } + $localPesterPaths += (Join-Path $packagesFolder "Pester" "tools" "Pester") + + foreach ($localPesterPath in $localPesterPaths | Select-Object -Unique) { + $attempts += $localPesterPath + if (Test-Path -Path $localPesterPath) { + $localManifestPath = Join-Path $localPesterPath "Pester.psd1" + $modulePath = $localPesterPath + if (Test-Path -Path $localManifestPath) { + $modulePath = $localManifestPath + } + + $module = Test-ModuleManifest -Path $modulePath -ErrorAction Stop + if ($module.Version.Major -eq 3 -and ((-not $PreferredPesterVersion) -or $module.Version -eq [version]$PreferredPesterVersion)) { + return [pscustomobject]@{ + ModulePath = $module.Path + Version = $module.Version.ToString() + Source = "repository packages" + } + } + } + } + + $availablePesterModules = @(Get-Module -ListAvailable Pester | Sort-Object Version -Descending) + $globalPester = $null + + if ($PreferredPesterVersion) { + $globalPester = $availablePesterModules | Where-Object { $_.Version -eq [version]$PreferredPesterVersion } | Select-Object -First 1 + } + + if (-not $globalPester) { + $globalPester = $availablePesterModules | Where-Object { $_.Version.Major -eq 3 } | Select-Object -First 1 + } + + if ($globalPester) { + return [pscustomobject]@{ + ModulePath = $globalPester.Path + Version = $globalPester.Version.ToString() + Source = "installed modules" + } + } + + $preferredVersionMessage = if ($PreferredPesterVersion) { "preferred version $PreferredPesterVersion" } else { "a Pester 3.x version" } + $attemptedPathsMessage = if ($attempts.Count -gt 0) { " Tried package paths: $($attempts -join ', ')." } else { "" } + throw "Pester $preferredVersionMessage for suite '$SuiteName' was not found in the repository packages folder or installed modules.$attemptedPathsMessage" +} + +function Invoke-SelectedTests { + param( + [Parameter(Mandatory = $true)] + [System.IO.FileInfo[]] $TestFiles + ) + + foreach ($testFile in $TestFiles) { + Write-Host "Running tests in: $($testFile.FullName)" + if ($UsePassThruFailureCheck) { + $result = Invoke-Pester -Path $testFile.FullName -PassThru + if ($result.FailedCount -gt 0) { + throw "Tests failed in $($testFile.FullName)." + } + } else { + Invoke-Pester -Path $testFile.FullName + } + } +} + +try { + if (-not $env:SystemRoot) { + $env:SystemRoot = "C:\Windows" + } + if (-not $env:TEMP) { + $env:TEMP = [System.IO.Path]::GetTempPath() + } + + foreach ($modulePath in $ImportModules) { + Import-Module -Name $modulePath -ErrorAction Stop + } + + if ($BeforeRun) { + & $BeforeRun + } + + $testFiles = @(Get-ChildItem -Path $testRootPath -Filter "*.tests.ps1" -Recurse) + if (-not [string]::IsNullOrWhiteSpace($Filter) -and $Filter -ne "*") { + $testFiles = @($testFiles | Where-Object { $_.Name -like $Filter -or $_.FullName -like $Filter }) + } + + if ($testFiles.Count -eq 0) { + Write-Host "No matching test files found under $testRootPath for filter '$Filter'." + return + } + + if ($PSVersionTable.PSEdition -eq "Core" -and -not $IsWindows) { + $referenceAssembliesPath = Join-Path $PSHOME "ref" + if (-not (Test-Path -Path $referenceAssembliesPath)) { + throw "Pester 3.4.3 on macOS requires a compatible pwsh installation with reference assemblies under '$referenceAssembliesPath'. This runner is intentionally lean and does not patch Pester at runtime." + } + } + + $pesterModule = Get-PesterModuleSpec + Write-Host "Using Pester module version $($pesterModule.Version) from $($pesterModule.Source)." + Import-Module -Name $pesterModule.ModulePath -RequiredVersion $pesterModule.Version -ErrorAction Stop + Invoke-SelectedTests -TestFiles $testFiles +} finally { + $env:SystemRoot = $originalSystemRoot + $env:TEMP = $originalTemp +} diff --git a/tools/StepTemplatePacker/tests/ConvertTo-OctopusJson.Tests.ps1 b/tools/StepTemplatePacker/tests/ConvertTo-OctopusJson.Tests.ps1 index e39e5fb0c..6ce2c2166 100644 --- a/tools/StepTemplatePacker/tests/ConvertTo-OctopusJson.Tests.ps1 +++ b/tools/StepTemplatePacker/tests/ConvertTo-OctopusJson.Tests.ps1 @@ -1,5 +1,6 @@ $ErrorActionPreference = "Stop"; Set-StrictMode -Version "Latest"; +. (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "Test-JsonAssertions.ps1") Describe "ConvertTo-OctopusDeploy" { @@ -55,8 +56,7 @@ Describe "ConvertTo-OctopusDeploy" { It "InputObject is a populated array" { $input = @( $null, 100, "my string" ); $expected = "[`r`n null,`r`n 100,`r`n `"my string`"`r`n]"; - ConvertTo-OctopusJson -InputObject $input ` - | Should Be $expected; + ConvertTo-OctopusJson -InputObject $input | Should BeJsonEquivalent $expected } It "InputObject is an empty PSCustomObject" { @@ -90,10 +90,10 @@ Describe "ConvertTo-OctopusDeploy" { "myPsObject": { "childProperty": "childValue" } -} + } "@ - ConvertTo-OctopusJson -InputObject $input ` - | Should Be $expected; + $expected = $expected.Trim() + ConvertTo-OctopusJson -InputObject $input | Should BeJsonEquivalent $expected } It "InputObject is an unhandled type" { @@ -101,4 +101,4 @@ Describe "ConvertTo-OctopusDeploy" { | Should Throw "Unhandled input object type 'System.Guid'."; } -} \ No newline at end of file +} diff --git a/tools/StepTemplatePacker/tests/Invoke-PesterTests.ps1 b/tools/StepTemplatePacker/tests/Invoke-PesterTests.ps1 index f8fb09f51..034965a20 100644 --- a/tools/StepTemplatePacker/tests/Invoke-PesterTests.ps1 +++ b/tools/StepTemplatePacker/tests/Invoke-PesterTests.ps1 @@ -1,18 +1,19 @@ +param( + [string] $Filter = "*" +) + $ErrorActionPreference = "Stop"; Set-StrictMode -Version "Latest"; $thisScript = $MyInvocation.MyCommand.Path; $thisFolder = [System.IO.Path]::GetDirectoryName($thisScript); - -$packagesFolder = $thisFolder; -$packagesFolder = [System.IO.Path]::GetDirectoryName($packagesFolder); -$packagesFolder = [System.IO.Path]::GetDirectoryName($packagesFolder); -$packagesFolder = [System.IO.Path]::GetDirectoryName($packagesFolder); -$packagesFolder = [System.IO.Path]::Combine($packagesFolder, "packages"); - +$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $thisFolder ".." ".." "..")); $packer = [System.IO.Path]::GetDirectoryName($thisFolder); +$sharedRunner = Join-Path $repoRoot "tools" "Invoke-SharedPesterTests.ps1"; -Import-Module -Name $packer; -Import-Module -Name ([System.IO.Path]::Combine($packagesFolder, "Pester.3.4.3\tools\Pester")); - -Invoke-Pester; \ No newline at end of file +& $sharedRunner ` + -TestRoot $thisFolder ` + -Filter $Filter ` + -ImportModules @($packer) ` + -PreferredPesterVersion "3.4.3" ` + -SuiteName "StepTemplatePacker"; diff --git a/tools/StepTemplatePacker/tests/Set-OctopusStepTemplateProperty.Tests.ps1 b/tools/StepTemplatePacker/tests/Set-OctopusStepTemplateProperty.Tests.ps1 index a27f7ee54..e6940f749 100644 --- a/tools/StepTemplatePacker/tests/Set-OctopusStepTemplateProperty.Tests.ps1 +++ b/tools/StepTemplatePacker/tests/Set-OctopusStepTemplateProperty.Tests.ps1 @@ -1,5 +1,6 @@ $ErrorActionPreference = "Stop"; Set-StrictMode -Version "Latest"; +. (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "Test-JsonAssertions.ps1") Describe "Set-OctopusStepTemplateProperty" { @@ -9,8 +10,7 @@ Describe "Set-OctopusStepTemplateProperty" { -PropertyName "Octopus.Action.Script.Syntax" ` -Value "PowerShell"; $expected = "{`r`n `"Properties`": {`r`n `"Octopus.Action.Script.Syntax`": `"PowerShell`"`r`n }`r`n}"; - ConvertTo-OctopusJson -InputObject $stepJson ` - | Should Be $expected; + ConvertTo-OctopusJson -InputObject $stepJson | Should BeJsonEquivalent $expected } It "No properties exist" { @@ -19,8 +19,7 @@ Describe "Set-OctopusStepTemplateProperty" { -PropertyName "Octopus.Action.Script.Syntax" ` -Value "PowerShell"; $expected = "{`r`n `"Properties`": {`r`n `"Octopus.Action.Script.Syntax`": `"PowerShell`"`r`n }`r`n}"; - ConvertTo-OctopusJson -InputObject $stepJson ` - | Should Be $expected; + ConvertTo-OctopusJson -InputObject $stepJson | Should BeJsonEquivalent $expected } It "Specified property does not exist" { @@ -29,8 +28,7 @@ Describe "Set-OctopusStepTemplateProperty" { -PropertyName "Octopus.Action.Script.Syntax" ` -Value "PowerShell"; $expected = "{`r`n `"Properties`": {`r`n `"otherProperty`": `"`",`r`n `"Octopus.Action.Script.Syntax`": `"PowerShell`"`r`n }`r`n}"; - ConvertTo-OctopusJson -InputObject $stepJson ` - | Should Be $expected; + ConvertTo-OctopusJson -InputObject $stepJson | Should BeJsonEquivalent $expected } It "Property does not exist" { @@ -39,8 +37,7 @@ Describe "Set-OctopusStepTemplateProperty" { -PropertyName "Octopus.Action.Script.Syntax" ` -Value "PowerShell"; $expected = "{`r`n `"Properties`": {`r`n `"Octopus.Action.Script.Syntax`": `"PowerShell`"`r`n }`r`n}"; - ConvertTo-OctopusJson -InputObject $stepJson ` - | Should Be $expected; + ConvertTo-OctopusJson -InputObject $stepJson | Should BeJsonEquivalent $expected } It "Property exists with a null value" { @@ -49,8 +46,7 @@ Describe "Set-OctopusStepTemplateProperty" { -PropertyName "Octopus.Action.Script.Syntax" ` -Value "PowerShell"; $expected = "{`r`n `"Properties`": {`r`n `"Octopus.Action.Script.Syntax`": `"PowerShell`"`r`n }`r`n}"; - ConvertTo-OctopusJson -InputObject $stepJson ` - | Should Be $expected; + ConvertTo-OctopusJson -InputObject $stepJson | Should BeJsonEquivalent $expected } It "Property exists with an empty string value" { @@ -59,8 +55,7 @@ Describe "Set-OctopusStepTemplateProperty" { -PropertyName "Octopus.Action.Script.Syntax" ` -Value "PowerShell"; $expected = "{`r`n `"Properties`": {`r`n `"Octopus.Action.Script.Syntax`": `"PowerShell`"`r`n }`r`n}"; - ConvertTo-OctopusJson -InputObject $stepJson ` - | Should Be $expected; + ConvertTo-OctopusJson -InputObject $stepJson | Should BeJsonEquivalent $expected } It "Property exists with a string value" { @@ -69,9 +64,8 @@ Describe "Set-OctopusStepTemplateProperty" { -PropertyName "Octopus.Action.Script.Syntax" ` -Value "PowerShell"; $expected = "{`r`n `"Properties`": {`r`n `"Octopus.Action.Script.Syntax`": `"PowerShell`"`r`n }`r`n}"; - ConvertTo-OctopusJson -InputObject $stepJson ` - | Should Be $expected; + ConvertTo-OctopusJson -InputObject $stepJson | Should BeJsonEquivalent $expected } -} \ No newline at end of file +} diff --git a/tools/StepTemplatePacker/tests/Test-JsonAssertions.ps1 b/tools/StepTemplatePacker/tests/Test-JsonAssertions.ps1 new file mode 100644 index 000000000..20e4caafe --- /dev/null +++ b/tools/StepTemplatePacker/tests/Test-JsonAssertions.ps1 @@ -0,0 +1,20 @@ +function global:ConvertTo-CompressedJsonForAssertion { + param( + [Parameter(Mandatory = $true)] + [string] $Json + ) + + return (ConvertFrom-Json -InputObject $Json | ConvertTo-Json -Depth 10 -Compress) +} + +function global:PesterBeJsonEquivalent($value, $expected) { + return (ConvertTo-CompressedJsonForAssertion -Json $value) -eq (ConvertTo-CompressedJsonForAssertion -Json $expected) +} + +function global:PesterBeJsonEquivalentFailureMessage($value, $expected) { + return "Expected JSON equivalent to: {$expected}`nBut was: {$value}" +} + +function global:NotPesterBeJsonEquivalentFailureMessage($value, $expected) { + return "Expected JSON not equivalent to: {$expected}`nBut was: {$value}" +}