diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml
index d984a8ef3..46fdfbef5 100644
--- a/.github/workflows/backport.yml
+++ b/.github/workflows/backport.yml
@@ -14,6 +14,7 @@ permissions:
jobs:
backport:
- uses: dotnet/arcade/.github/workflows/backport-base.yml@main
+ uses: dotnet/arcade/.github/workflows/backport-base.yml@backport-just-author
with:
repository_owners: 'pester'
+ pr_title_template: '%source_pr_title% by @%source_pr_author% in #%source_pr_number% (backport to %target_branch%)'
diff --git a/BACKERS.md b/BACKERS.md
index 2ef2ac64b..493efa14b 100644
--- a/BACKERS.md
+++ b/BACKERS.md
@@ -18,3 +18,5 @@
@tabs-not-spaces
@avanreijn
@DevOpsCollectiveINC
+@chocolatey
+
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index c9a22f910..9261634f0 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -82,12 +82,12 @@ stages:
PS7_Ubuntu_22_04:
vmImage: ubuntu-22.04
pwsh: true
- PS7_macOS_12:
- vmImage: macOS-12
- pwsh: true
PS7_macOS_13:
vmImage: macOS-13
pwsh: true
+ PS7_macOS_14:
+ vmImage: macOS-14
+ pwsh: true
PS7_Windows_Server2019:
vmImage: windows-2019
pwsh: true
@@ -114,9 +114,8 @@ stages:
script: |
& ./test.ps1 -CI -CC -PassThru -NoBuild
workingDirectory: '$(Build.SourcesDirectory)'
- - task: PublishCodeCoverageResults@1
+ - task: PublishCodeCoverageResults@2
inputs:
- codeCoverageTool: 'JaCoCo'
summaryFileLocation: 'coverage.xml'
pathToSources: '$(Build.SourcesDirectory)/bin/'
failIfCoverageEmpty: false
diff --git a/build.ps1 b/build.ps1
index 254e8484a..7587e1a33 100644
--- a/build.ps1
+++ b/build.ps1
@@ -94,7 +94,7 @@ if ($Clean) {
if ($Clean) {
# Update PesterConfiguration help in about_PesterConfiguration
if ($PSVersionTable.PSVersion.Major -ge 6) {
- $null = [Reflection.Assembly]::LoadFrom("$PSScriptRoot/src/csharp/Pester/bin/$Configuration/net6.0/Pester.dll")
+ $null = [Reflection.Assembly]::LoadFrom("$PSScriptRoot/src/csharp/Pester/bin/$Configuration/net8.0/Pester.dll")
}
else {
$null = [Reflection.Assembly]::LoadFrom("$PSScriptRoot/src/csharp/Pester/bin/$Configuration/net462/Pester.dll")
@@ -202,9 +202,10 @@ if ($Clean) {
, ("$PSScriptRoot/src/schemas/JUnit4/*.xsd", "$PSScriptRoot/bin/schemas/JUnit4/")
, ("$PSScriptRoot/src/schemas/NUnit25/*.xsd", "$PSScriptRoot/bin/schemas/NUnit25/")
, ("$PSScriptRoot/src/schemas/NUnit3/*.xsd", "$PSScriptRoot/bin/schemas/NUnit3/")
+ , ("$PSScriptRoot/src/schemas/Cobertura/*.dtd", "$PSScriptRoot/bin/schemas/Cobertura/")
, ("$PSScriptRoot/src/schemas/JaCoCo/*.dtd", "$PSScriptRoot/bin/schemas/JaCoCo/")
, ("$PSScriptRoot/src/csharp/Pester/bin/$Configuration/net462/Pester.dll", "$PSScriptRoot/bin/bin/net462/")
- , ("$PSScriptRoot/src/csharp/Pester/bin/$Configuration/net6.0/Pester.dll", "$PSScriptRoot/bin/bin/net6.0/")
+ , ("$PSScriptRoot/src/csharp/Pester/bin/$Configuration/net8.0/Pester.dll", "$PSScriptRoot/bin/bin/net8.0/")
)
}
diff --git a/publish/filesToPublish.ps1 b/publish/filesToPublish.ps1
index 6ae35efc8..ca9f45457 100644
--- a/publish/filesToPublish.ps1
+++ b/publish/filesToPublish.ps1
@@ -5,9 +5,10 @@
'Pester.Format.ps1xml'
'PesterConfiguration.Format.ps1xml'
'bin/net462/Pester.dll'
- 'bin/net6.0/Pester.dll'
+ 'bin/net8.0/Pester.dll'
'en-US/about_Pester.help.txt'
'en-US/about_PesterConfiguration.help.txt'
+ 'schemas/Cobertura/coverage-loose.dtd'
'schemas/JaCoCo/report.dtd'
'schemas/JUnit4/junit_schema_4.xsd'
'schemas/NUnit25/nunit_schema_2.5.xsd'
diff --git a/src/Main.ps1 b/src/Main.ps1
index 900978ef2..2c71b497f 100644
--- a/src/Main.ps1
+++ b/src/Main.ps1
@@ -80,6 +80,8 @@ function Add-ShouldOperator {
[switch] $SupportsArrayInput
)
+ Assert-BoundScriptBlockInput -ScriptBlock $Test
+
$entry = [PSCustomObject]@{
Test = $Test
SupportsArrayInput = [bool]$SupportsArrayInput
@@ -1260,6 +1262,8 @@ function BeforeDiscovery {
[ScriptBlock]$ScriptBlock
)
+ Assert-BoundScriptBlockInput -ScriptBlock $ScriptBlock
+
if ($ExecutionContext.SessionState.PSVariable.Get('invokedViaInvokePester')) {
if ($state.CurrentBlock.IsRoot -and -not $state.CurrentBlock.FrameworkData.MissingParametersProcessed) {
# For undefined parameters in container, add parameter's default value to Data
diff --git a/src/Pester.RSpec.ps1 b/src/Pester.RSpec.ps1
index 633557cf5..e15094b98 100644
--- a/src/Pester.RSpec.ps1
+++ b/src/Pester.RSpec.ps1
@@ -333,9 +333,6 @@ function New-PesterConfiguration {
.LINK
https://pester.dev/docs/commands/Invoke-Pester
-
- .LINK
- about_PesterConfiguration
#>
[CmdletBinding()]
[OutputType([PesterConfiguration])]
diff --git a/src/Pester.Runtime.ps1 b/src/Pester.Runtime.ps1
index 965ac9ab2..40ef93a56 100644
--- a/src/Pester.Runtime.ps1
+++ b/src/Pester.Runtime.ps1
@@ -2474,6 +2474,10 @@ function New-BlockContainerObject {
default { throw [System.ArgumentOutOfRangeException]'' }
}
+ if ($item -is [scriptblock]) {
+ Assert-BoundScriptBlockInput -ScriptBlock $item
+ }
+
$c = [Pester.ContainerInfo]::Create()
$c.Type = $type
$c.Item = $item
@@ -2604,3 +2608,20 @@ function Add-MissingContainerParameters ($RootBlock, $Container, $CallingFunctio
$RootBlock.FrameworkData.MissingParametersProcessed = $true
}
+
+function Assert-BoundScriptBlockInput {
+ param(
+ [Parameter(Mandatory = $true)]
+ [ScriptBlock] $ScriptBlock
+ )
+ $internalSessionState = $script:ScriptBlockSessionStateInternalProperty.GetValue($ScriptBlock, $null)
+ if ($null -eq $internalSessionState) {
+ $maxLength = 250
+ $prettySb = (Format-Nicely2 $ScriptBlock) -replace '\s{2,}', ' '
+ if ($prettySb.Length -gt $maxLength) {
+ $prettySb = "$($prettySb.Remove($maxLength))..."
+ }
+
+ throw [System.ArgumentException]::new("Unbound scriptblock is not allowed, because it would run inside of Pester session state and produce unexpected results. See https://github.com/pester/Pester/issues/2411 for more details and workarounds. ScriptBlock: '$prettySb'")
+ }
+}
diff --git a/src/Pester.Types.ps1 b/src/Pester.Types.ps1
index d891dddaf..b63d20d43 100644
--- a/src/Pester.Types.ps1
+++ b/src/Pester.Types.ps1
@@ -13,13 +13,13 @@ if ($null -ne $configurationType) {
}
if ($PSVersionTable.PSVersion.Major -ge 6) {
- $path = "$PSScriptRoot/bin/net6.0/Pester.dll"
+ $path = "$PSScriptRoot/bin/net8.0/Pester.dll"
# PESTER_BUILD
if ((Get-Variable -Name "PESTER_BUILD" -ValueOnly -ErrorAction Ignore)) {
- $path = "$PSScriptRoot/../bin/bin/net6.0/Pester.dll"
+ $path = "$PSScriptRoot/../bin/bin/net8.0/Pester.dll"
}
else {
- $path = "$PSScriptRoot/../bin/bin/net6.0/Pester.dll"
+ $path = "$PSScriptRoot/../bin/bin/net8.0/Pester.dll"
}
# end PESTER_BUILD
& $SafeCommands['Add-Type'] -Path $path
diff --git a/src/Pester.psd1 b/src/Pester.psd1
index 3ae04f666..7d574845a 100644
--- a/src/Pester.psd1
+++ b/src/Pester.psd1
@@ -161,10 +161,10 @@
LicenseUri = "https://www.apache.org/licenses/LICENSE-2.0.html"
# Release notes for this particular version of the module
- ReleaseNotes = 'https://github.com/pester/Pester/releases/tag/6.0.0-alpha4'
+ ReleaseNotes = 'https://github.com/pester/Pester/releases/tag/6.0.0-alpha5'
# Prerelease string of this module
- Prerelease = 'alpha4'
+ Prerelease = 'alpha5'
}
# Minimum assembly version required
diff --git a/src/csharp/Pester/CodeCoverageConfiguration.cs b/src/csharp/Pester/CodeCoverageConfiguration.cs
index b43d7ece3..1f1b3eda0 100644
--- a/src/csharp/Pester/CodeCoverageConfiguration.cs
+++ b/src/csharp/Pester/CodeCoverageConfiguration.cs
@@ -41,7 +41,7 @@ public static CodeCoverageConfiguration ShallowClone(CodeCoverageConfiguration c
public CodeCoverageConfiguration() : base("Options to enable and configure Pester's code coverage feature.")
{
Enabled = new BoolOption("Enable CodeCoverage.", false);
- OutputFormat = new StringOption("Format to use for code coverage report. Possible values: JaCoCo, CoverageGutters", "JaCoCo");
+ OutputFormat = new StringOption("Format to use for code coverage report. Possible values: JaCoCo, CoverageGutters, Cobertura", "JaCoCo");
OutputPath = new StringOption("Path relative to the current directory where code coverage report is saved.", "coverage.xml");
OutputEncoding = new StringOption("Encoding of the output file.", "UTF8");
Path = new StringArrayOption("Directories or files to be used for code coverage, by default the Path(s) from general settings are used, unless overridden here.", new string[0]);
diff --git a/src/csharp/Pester/CoverageLocationVisitor.cs b/src/csharp/Pester/CoverageLocationVisitor.cs
new file mode 100644
index 000000000..46e1362f0
--- /dev/null
+++ b/src/csharp/Pester/CoverageLocationVisitor.cs
@@ -0,0 +1,80 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Management.Automation.Language;
+
+namespace Pester
+{
+ ///
+ /// A visitor class for traversing the PowerShell AST to collect coverage-relevant locations.
+ /// This replaces predicate-based filtering with a centralized, extensible approach.
+ ///
+ /// Advantages:
+ /// - Efficiently skips nodes with attributes like [ExcludeFromCodeCoverage].
+ /// - Simplifies logic by handling each AST type in dedicated methods.
+ ///
+ public class CoverageLocationVisitor : AstVisitor2
+ {
+ public readonly List CoverageLocations = new();
+
+ public override AstVisitAction VisitScriptBlock(ScriptBlockAst scriptBlockAst)
+ {
+ if (scriptBlockAst.ParamBlock?.Attributes != null)
+ {
+ foreach (var attribute in scriptBlockAst.ParamBlock.Attributes)
+ {
+ if (attribute.TypeName.GetReflectionType() == typeof(ExcludeFromCodeCoverageAttribute))
+ {
+ return AstVisitAction.SkipChildren;
+ }
+ }
+ }
+ return AstVisitAction.Continue;
+ }
+
+ public override AstVisitAction VisitCommand(CommandAst commandAst)
+ {
+ CoverageLocations.Add(commandAst);
+ return AstVisitAction.Continue;
+ }
+
+ public override AstVisitAction VisitCommandExpression(CommandExpressionAst commandExpressionAst)
+ {
+ CoverageLocations.Add(commandExpressionAst);
+ return AstVisitAction.Continue;
+ }
+
+ public override AstVisitAction VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordStatementAst)
+ {
+ CoverageLocations.Add(dynamicKeywordStatementAst);
+ return AstVisitAction.Continue;
+ }
+
+ public override AstVisitAction VisitBreakStatement(BreakStatementAst breakStatementAst)
+ {
+ CoverageLocations.Add(breakStatementAst);
+ return AstVisitAction.Continue;
+ }
+
+ public override AstVisitAction VisitContinueStatement(ContinueStatementAst continueStatementAst)
+ {
+ CoverageLocations.Add(continueStatementAst);
+ return AstVisitAction.Continue;
+ }
+
+ public override AstVisitAction VisitExitStatement(ExitStatementAst exitStatementAst)
+ {
+ CoverageLocations.Add(exitStatementAst);
+ return AstVisitAction.Continue;
+ }
+
+ public override AstVisitAction VisitThrowStatement(ThrowStatementAst throwStatementAst)
+ {
+ CoverageLocations.Add(throwStatementAst);
+ return AstVisitAction.Continue;
+ }
+
+ // ReturnStatementAst is excluded as it's not behaving consistent.
+ // "return" is not hit in 5.1 but fixed in a later version. Using "return 123" we get hit on 123 but not return.
+ // See https://github.com/pester/Pester/issues/1465#issuecomment-604323645
+ }
+}
diff --git a/src/csharp/Pester/Pester.csproj b/src/csharp/Pester/Pester.csproj
index 6392dbb9f..bea226a3e 100644
--- a/src/csharp/Pester/Pester.csproj
+++ b/src/csharp/Pester/Pester.csproj
@@ -1,7 +1,7 @@
- net6.0;net462
+ net8.0;net462
latest
true
embedded
@@ -11,14 +11,10 @@
$(DefineConstants);PESTER
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tst/Pester.RSpec.ts.ps1 b/tst/Pester.RSpec.ts.ps1
index bc400befa..2002e4d78 100644
--- a/tst/Pester.RSpec.ts.ps1
+++ b/tst/Pester.RSpec.ts.ps1
@@ -1668,6 +1668,12 @@ i -PassThru:$PassThru {
}
}
}
+
+ t "Does not accept unbound scriptblocks" {
+ # Would execute in Pester's internal module state
+ $ex = { New-PesterContainer -ScriptBlock ([ScriptBlock]::Create('$true')) } | Verify-Throw
+ $ex.Exception.Message | Verify-Like 'Unbound scriptblock*'
+ }
}
b "BeforeDiscovery" {
@@ -1691,6 +1697,15 @@ i -PassThru:$PassThru {
$r.Containers[0].Blocks[0].Tests[0].Result | Verify-Equal "Passed"
$r.Containers[0].Blocks[1].Tests[0].Result | Verify-Equal "Passed"
}
+
+ t "Does not accept unbound scriptblocks" {
+ # Would execute in Pester's internal module state
+ $sb = { BeforeDiscovery ([ScriptBlock]::Create('$true')) }
+ $container = New-PesterContainer -ScriptBlock $sb
+ $r = Invoke-Pester -Container $container -PassThru
+ $r.Containers[0].Result | Verify-Equal 'Failed'
+ $r.Containers[0].ErrorRecord.Exception.Message | Verify-Like 'Unbound scriptblock*'
+ }
}
b "Parametric tests" {
@@ -2922,4 +2937,33 @@ i -PassThru:$PassThru {
$pwd.Path | Verify-Equal $beforePWD
}
}
+
+ b 'Unbound scriptblocks' {
+ # Would execute in Pester's internal module state
+ t 'Throws when provided to Run.ScriptBlock' {
+ $sb = [scriptblock]::Create('')
+ $conf = New-PesterConfiguration
+ $conf.Run.ScriptBlock = $sb
+ $conf.Run.Throw = $true
+ $conf.Output.CIFormat = 'None'
+
+ $ex = { Invoke-Pester -Configuration $conf } | Verify-Throw
+ $ex.Exception.Message | Verify-Like '*Unbound scriptblock*'
+ }
+
+ t 'Throws when provided to Run.Container' {
+ $c = [Pester.ContainerInfo]::Create()
+ $c.Type = 'ScriptBlock'
+ $c.Item = [scriptblock]::Create('')
+ $c.Data = @{}
+
+ $conf = New-PesterConfiguration
+ $conf.Run.Container = $c
+ $conf.Run.Throw = $true
+ $conf.Output.CIFormat = 'None'
+
+ $ex = { Invoke-Pester -Configuration $conf } | Verify-Throw
+ $ex.Exception.Message | Verify-Like '*Unbound scriptblock*'
+ }
+ }
}
diff --git a/tst/functions/Add-ShouldOperator.ts.ps1 b/tst/functions/Add-ShouldOperator.ts.ps1
index 71c4143c9..4d5be5a08 100644
--- a/tst/functions/Add-ShouldOperator.ts.ps1
+++ b/tst/functions/Add-ShouldOperator.ts.ps1
@@ -70,6 +70,17 @@ i -PassThru:$PassThru {
}
}
+ b 'Add-ShouldOperator input validation' {
+ Get-Module Pester | Remove-Module
+ Import-Module "$PSScriptRoot\..\..\bin\Pester.psd1"
+
+ t 'Does not allow unbound scriptblocks' {
+ # Would execute in Pester's internal module state
+ $ex = { Add-ShouldOperator -Name DenyUnbound -Test ([ScriptBlock]::Create('$true')) } | Verify-Throw
+ $ex.Exception.Message | Verify-Like 'Unbound scriptblock*'
+ }
+ }
+
b 'Executing custom Should assertions' {
# Testing paramter and output syntax described in docs (https://pester.dev/docs/assertions/custom-assertions)
Get-Module Pester | Remove-Module
diff --git a/tst/functions/Context.Tests.ps1 b/tst/functions/Context.Tests.ps1
index 2ce94eaa7..c962c322a 100644
--- a/tst/functions/Context.Tests.ps1
+++ b/tst/functions/Context.Tests.ps1
@@ -7,7 +7,7 @@ Describe 'Testing Context' {
}
}
- } | should -Throw 'Test fixture name has multiple lines and no test fixture is provided. (Have you provided a name for the test group?)'
+ } | Should -Throw 'Test fixture name has multiple lines and no test fixture is provided. (Have you provided a name for the test group?)'
}
It "Has a name that looks like a script block" {
@@ -17,6 +17,11 @@ Describe 'Testing Context' {
}
}
- } | should -Throw 'No test fixture is provided. (Have you put the open curly brace on the next line?)'
+ } | Should -Throw 'No test fixture is provided. (Have you put the open curly brace on the next line?)'
+ }
+
+ It 'Throws when provided unbound scriptblock' {
+ # Unbound scriptblocks would execute in Pester's internal module state
+ { Context 'c' -Fixture ([scriptblock]::Create('')) } | Should -Throw -ExpectedMessage 'Unbound scriptblock*'
}
}
diff --git a/tst/functions/Coverage.Tests.ps1 b/tst/functions/Coverage.Tests.ps1
index 7b87327d8..115eb7780 100644
--- a/tst/functions/Coverage.Tests.ps1
+++ b/tst/functions/Coverage.Tests.ps1
@@ -17,6 +17,8 @@ InPesterModuleScope {
$testScriptPath = Join-Path -Path $root -ChildPath TestScript.ps1
$testScript2Path = Join-Path -Path $root -ChildPath TestScript2.ps1
$testScript3Path = Join-Path -Path $rootSubFolder -ChildPath TestScript3.ps1
+ $testScriptStatementsPath = Join-Path -Path $root -ChildPath TestScriptStatements.ps1
+ $testScriptExitPath = Join-Path -Path $root -ChildPath TestScriptExit.ps1
$null = New-Item -Path $testScriptPath -ItemType File -ErrorAction SilentlyContinue
@@ -42,7 +44,7 @@ InPesterModuleScope {
function FunctionTwo
{
- 'I am function two. I never get called.'
+ 'I am function two. I never get called.'
}
FunctionOne
@@ -97,7 +99,7 @@ InPesterModuleScope {
#{
function MyClass
{
- MyBaseClass; 'I am the constructor.'
+ MyBaseClass # call the base constructor
}
function MethodOne
@@ -133,6 +135,54 @@ InPesterModuleScope {
-f `
'other'
+'@
+
+ $null = New-Item -Path $testScriptStatementsPath -ItemType File -ErrorAction SilentlyContinue
+
+ Set-Content -Path $testScriptStatementsPath -Value @'
+ try {
+ try {
+ throw 'omg'
+ }
+ catch {
+ throw
+ }
+ }
+ catch { }
+
+ switch (1,2,3) {
+ 1 { continue; }
+ 2 { break }
+ 3 { 'I was skipped because 2 called break in switch.' }
+ }
+
+ :myBreakLabel foreach ($i in 1..100) {
+ foreach ($o in 1) {
+ break myBreakLabel
+ }
+ 'I was skipped by a labeled break.'
+ }
+
+ :myLoopLabel foreach ($i in 1) {
+ foreach ($o in 1..100) {
+ continue myLoopLabel
+ }
+ 'I was skipped by a labeled contiune.'
+ }
+
+ # These should not be included in code coverage
+ & { return }
+ & { return 123 }
+
+ # will exit the script
+ exit
+'@
+
+ $null = New-Item -Path $testScriptExitPath -ItemType File -ErrorAction SilentlyContinue
+
+ Set-Content -Path $testScriptExitPath -Value @'
+ # will exit the script, so keep in own file
+ exit 123
'@
}
@@ -143,14 +193,16 @@ InPesterModuleScope {
BeforeAll {
# TODO: renaming, breakpoints mean "code point of interests" in most cases here, not actual breakpoints
# Path deliberately duplicated to make sure the code doesn't produce multiple breakpoints for the same commands
- $breakpoints = Enter-CoverageAnalysis -CodeCoverage $testScriptPath, $testScriptPath, $testScript2Path, $testScript3Path -UseBreakpoints $UseBreakpoints
+ $breakpoints = Enter-CoverageAnalysis -CodeCoverage $testScriptPath, $testScriptPath, $testScript2Path, $testScript3Path, $testScriptStatementsPath, $testScriptExitPath -UseBreakpoints $UseBreakpoints
- @($breakpoints).Count | Should -Be 19 -Because 'it has the proper number of breakpoints defined'
+ @($breakpoints).Count | Should -Be 40 -Because 'it has the proper number of breakpoints defined'
$sb = {
$null = & $testScriptPath
$null = & $testScript2Path
$null = & $testScript3Path
+ $null = & $testScriptStatementsPath
+ $null = & $testScriptExitPath
}
if ($UseBreakpoints) {
@@ -167,29 +219,32 @@ InPesterModuleScope {
}
It 'Reports the proper number of executed commands' {
- $coverageReport.NumberOfCommandsExecuted | Should -Be 16
+ $coverageReport.NumberOfCommandsExecuted | Should -Be 34
}
It 'Reports the proper number of analyzed commands' {
- $coverageReport.NumberOfCommandsAnalyzed | Should -Be 19
+ $coverageReport.NumberOfCommandsAnalyzed | Should -Be 40
}
It 'Reports the proper number of analyzed files' {
- $coverageReport.NumberOfFilesAnalyzed | Should -Be 3
+ $coverageReport.NumberOfFilesAnalyzed | Should -Be 5
}
It 'Reports the proper number of missed commands' {
- $coverageReport.MissedCommands.Count | Should -Be 3
+ $coverageReport.MissedCommands.Count | Should -Be 6
}
It 'Reports the correct missed command' {
$coverageReport.MissedCommands[0].Command | Should -Be "'I cannot get called.'"
- $coverageReport.MissedCommands[1].Command | Should -Be "'I am function two. I never get called.'"
+ $coverageReport.MissedCommands[1].Command | Should -Be "'I am function two. I never get called.'"
$coverageReport.MissedCommands[2].Command | Should -Be "'I am method two. I never get called.'"
+ $coverageReport.MissedCommands[3].Command | Should -Be "'I was skipped because 2 called break in switch.'"
+ $coverageReport.MissedCommands[4].Command | Should -Be "'I was skipped by a labeled break.'"
+ $coverageReport.MissedCommands[5].Command | Should -Be "'I was skipped by a labeled contiune.'"
}
It 'Reports the proper number of hit commands' {
- $coverageReport.HitCommands.Count | Should -Be 16
+ $coverageReport.HitCommands.Count | Should -Be 34
}
It 'Reports the correct hit command' {
@@ -289,6 +344,28 @@ InPesterModuleScope {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -317,10 +394,40 @@ InPesterModuleScope {
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
')
}
@@ -429,6 +536,28 @@ InPesterModuleScope {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -457,10 +586,40 @@ InPesterModuleScope {
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -486,14 +645,148 @@ InPesterModuleScope {
-
-
-
-
+
+
+
+
')
}
+ It 'Cobertura report must be correct' {
+ [String]$coberturaReportXml = Get-CoberturaReportXml -TotalMilliseconds 10000 -CoverageReport $coverageReport
+ $coberturaReportXml = $coberturaReportXml -replace 'timestamp="[0-9]*"', 'timestamp=""'
+ $coberturaReportXml = $coberturaReportXml -replace "$([System.Environment]::NewLine)", ''
+ $coberturaReportXml = $coberturaReportXml.Replace($root, 'CommonRoot')
+ $coberturaReportXml = $coberturaReportXml.Replace($root.Replace('\', '/'), 'CommonRoot')
+ (Clear-WhiteSpace $coberturaReportXml) | Should -Be (Clear-WhiteSpace '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ')
+ }
+
It 'JaCoCo returns empty string when there are 0 analyzed commands' {
$coverageReport = [PSCustomObject] @{ NumberOfCommandsAnalyzed = 0 }
[String]$jaCoCoReportXml = Get-JaCoCoReportXml -CommandCoverage @{} -TotalMilliseconds 10000 -CoverageReport $coverageReport -Format "CoverageGutters"
@@ -501,6 +794,13 @@ InPesterModuleScope {
$jaCoCoReportXml | Should -Be ([String]::Empty)
}
+ It 'Cobertura returns empty string when there are 0 analyzed commands' {
+ $coverageReport = [PSCustomObject] @{ NumberOfCommandsAnalyzed = 0 }
+ [String]$coberturaReportXml = Get-CoberturaReportXml -CoverageReport $coverageReport -TotalMilliseconds 10000
+ $coberturaReportXml | Should -Not -Be $null
+ $coberturaReportXml | Should -Be ([String]::Empty)
+ }
+
It 'Reports the right line numbers' {
$coverageReport.HitCommands[$coverageReport.NumberOfCommandsExecuted - 1].Line | Should -Be 1
$coverageReport.HitCommands[$coverageReport.NumberOfCommandsExecuted - 1].StartLine | Should -Be 1
@@ -552,7 +852,7 @@ InPesterModuleScope {
}
It 'Reports the correct missed command' {
- $coverageReport.MissedCommands[0].Command | Should -Be "'I am function two. I never get called.'"
+ $coverageReport.MissedCommands[0].Command | Should -Be "'I am function two. I never get called.'"
}
It 'Reports the proper number of hit commands' {
@@ -703,7 +1003,7 @@ InPesterModuleScope {
It 'Reports the correct missed command' {
$coverageReport.MissedCommands[0].Command | Should -Be "'I cannot get called.'"
- $coverageReport.MissedCommands[1].Command | Should -Be "'I am function two. I never get called.'"
+ $coverageReport.MissedCommands[1].Command | Should -Be "'I am function two. I never get called.'"
}
It 'Reports the proper number of hit commands' {
@@ -984,6 +1284,131 @@ InPesterModuleScope {
}
}
+ Describe 'Coverage Location Visitor' {
+ BeforeAll {
+ $testScript = @'
+ using namespace System.Diagnostics.CodeAnalysis
+
+ function FunctionIncluded {
+ "I am included"
+ }
+
+ function FunctionExcluded {
+ [ExcludeFromCodeCoverageAttribute()]
+ param()
+
+ "I am not included"
+ }
+
+ FunctionIncluded
+ FunctionExcluded
+
+'@
+ $tokens = $null
+ $errors = $null
+ $ast = [System.Management.Automation.Language.Parser]::ParseInput($testScript, [ref]$tokens, [ref]$errors)
+ }
+
+ Context 'Collect coverage locations' {
+ BeforeAll {
+ $visitor = [Pester.CoverageLocationVisitor]::new()
+ $ast.Visit($visitor)
+ }
+
+ It 'Skips excluded script blocks' {
+ $excludedCommand = $visitor.CoverageLocations | Where-Object {
+ $_ -is [System.Management.Automation.Language.CommandExpressionAst] -and
+ $_.Expression.Value -eq "I am not included"
+ }
+
+ $excludedCommand.Count | Should -Be 0 -Because "Command in excluded script blocks should not be collected."
+ }
+
+ It 'Processes included script blocks' {
+ $includedCommand = $visitor.CoverageLocations | Where-Object {
+ $_ -is [System.Management.Automation.Language.CommandExpressionAst] -and
+ $_.Expression.Value -eq "I am included"
+ }
+
+ $includedCommand.Count | Should -Be 1 -Because "Command in included script blocks should be collected."
+ }
+ }
+
+ Context 'Collect coverage locations for other AST types' {
+ It 'Collects all relevant AST types' {
+ $script = @'
+ foreach ($i in 1..10) { # 1 location
+ break # 1 location
+ continue # 1 location
+ if ($i -eq 5) { # 1 location
+ throw # 1 location
+ }
+ if ($i -eq 7) { # 1 location
+ exit # 1 location
+ }
+ return # not collected
+ }
+'@
+ $tokens = $null
+ $errors = $null
+ $ast = [System.Management.Automation.Language.Parser]::ParseInput($script, [ref]$tokens, [ref]$errors)
+
+ $visitor = [Pester.CoverageLocationVisitor]::new()
+ $ast.Visit($visitor)
+
+ $visitor.CoverageLocations.Count | Should -Be 7 -Because "Break, Continue, Throw, and Exit statements should be collected."
+ }
+ }
+
+ Context 'Coverage analysis with exclusion using ' -Foreach @(
+ @{ UseBreakpoints = $true; Description = "With breakpoints" }
+ @{ UseBreakpoints = $false; Description = "Profiler-based coverage collection" }
+ ) {
+ BeforeAll {
+ $root = (Get-PSDrive TestDrive).Root
+ $testScriptPath = Join-Path -Path $root -ChildPath TestScript.ps1
+ Set-Content -Path $testScriptPath -Value $testScript
+
+ $breakpoints = Enter-CoverageAnalysis -CodeCoverage @{ Path = $testScriptPath } -UseBreakpoints $UseBreakpoints
+
+ @($breakpoints).Count | Should -Be 3 -Because 'The correct number of breakpoints should be defined.'
+
+ if ($UseBreakpoints) {
+ & $testScriptPath
+ }
+ else {
+ $patched, $tracer = Start-TraceScript $breakpoints
+ try { & $testScriptPath } finally { Stop-TraceScript -Patched $patched }
+ $measure = $tracer.Hits
+ }
+
+ $coverageReport = Get-CoverageReport -CommandCoverage $breakpoints -Measure $measure
+ }
+
+ It 'Correctly reports executed commands' {
+ $coverageReport.NumberOfCommandsExecuted | Should -Be 3 -Because 'The executed commands count should match.'
+ }
+
+ It 'Correctly reports analyzed commands' {
+ $coverageReport.NumberOfCommandsAnalyzed | Should -Be 3 -Because 'All commands should be analyzed.'
+ }
+
+ It 'Correctly reports missed commands' {
+ $coverageReport.MissedCommands.Count | Should -Be 0 -Because 'No command should be missed.'
+ }
+
+ It 'Correctly reports hit commands' {
+ $coverageReport.HitCommands.Count | Should -Be 3 -Because 'Three commands should be hit.'
+ }
+
+ AfterAll {
+ if ($UseBreakpoints) {
+ Exit-CoverageAnalysis -CommandCoverage $breakpoints
+ }
+ }
+ }
+ }
+
# Describe 'Stripping common parent paths' {
# If ( (& $SafeCommands['Get-Variable'] -Name IsLinux -Scope Global -ErrorAction SilentlyContinue) -or
diff --git a/tst/functions/Describe.Tests.ps1 b/tst/functions/Describe.Tests.ps1
index 0ade9f093..fa292512e 100644
--- a/tst/functions/Describe.Tests.ps1
+++ b/tst/functions/Describe.Tests.ps1
@@ -36,6 +36,11 @@ Describe 'Testing Describe' {
}
} | Should -Throw 'Test fixture name has multiple lines and no test fixture is provided. (Have you provided a name for the test group?)'
}
+
+ It 'Throws when provided unbound scriptblock' {
+ # Unbound scriptblocks would execute in Pester's internal module state
+ { Describe 'd' -Fixture ([scriptblock]::Create('')) } | Should -Throw -ExpectedMessage 'Unbound scriptblock*'
+ }
}
diff --git a/tst/functions/It.Tests.ps1 b/tst/functions/It.Tests.ps1
new file mode 100644
index 000000000..6249f10af
--- /dev/null
+++ b/tst/functions/It.Tests.ps1
@@ -0,0 +1,24 @@
+Set-StrictMode -Version Latest
+
+Describe 'Testing It' {
+ It 'Throws when missing name' {
+ { It {
+
+ 'something'
+ }
+ } | Should -Throw -ExpectedMessage 'Test name has multiple lines and no test scriptblock is provided*'
+ }
+
+ It 'Throws when missing scriptblock' {
+ { It 'runs a test'
+ {
+ # This scriptblock is a new statement as scriptblock didn't start on It-line nor used a backtick
+ }
+ } | Should -Throw -ExpectedMessage 'No test scriptblock is provided*'
+ }
+
+ It 'Throws when provided unbound scriptblock' {
+ # Unbound scriptblocks would execute in Pester's internal module state
+ { It 'i' -Test ([scriptblock]::Create('')) } | Should -Throw -ExpectedMessage 'Unbound scriptblock*'
+ }
+}
diff --git a/tst/functions/Mock.Tests.ps1 b/tst/functions/Mock.Tests.ps1
index ab769e95a..575b8ee56 100644
--- a/tst/functions/Mock.Tests.ps1
+++ b/tst/functions/Mock.Tests.ps1
@@ -2366,15 +2366,25 @@ Describe 'Naming conflicts in mocked functions' {
}
Describe 'Passing unbound script blocks as mocks' {
- It 'Does not produce an error' {
+ BeforeAll {
function TestMe {
'Original'
}
+ }
+ It 'Does not produce an error' {
$scriptBlock = [scriptblock]::Create('"Mocked"')
{ Mock TestMe $scriptBlock } | Should -Not -Throw
TestMe | Should -Be Mocked
}
+
+ It 'Should not execute in Pester internal state' {
+ $filter = [scriptblock]::Create('if ("pester" -eq $ExecutionContext.SessionState.Module) { throw "executed parameter filter in internal state" } else { $true }')
+ $scriptBlock = [scriptblock]::Create('if ("pester" -eq $ExecutionContext.SessionState.Module) { throw "executed mock in internal state" } else { "Mocked" }')
+
+ { Mock -CommandName TestMe -ParameterFilter $filter -MockWith $scriptBlock } | Should -Not -Throw
+ TestMe -SomeParam | Should -Be Mocked
+ }
}
Describe 'Should -Invoke when mock called outside of It block' {
@@ -3150,3 +3160,66 @@ Describe 'Mocking with nested Pester runs' {
Get-Command Get-ChildItem | Should -Not -Be 2
}
}
+
+Describe 'Usage of Alias in DynamicParams' {
+ # https://github.com/pester/Pester/issues/1274
+
+ BeforeAll {
+ function New-DynamicAttr($ParamDictionary, $Name, $Alias = $null) {
+ $attr = New-Object -Type System.Management.Automation.ParameterAttribute
+ $attr.Mandatory = $false
+ $attr.ParameterSetName = '__AllParameterSets'
+ $attributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
+ $attributeCollection.Add($attr)
+
+ if ($null -ne $Alias) {
+ $attr = New-Object -Type System.Management.Automation.AliasAttribute -ArgumentList @($Alias)
+ $attributeCollection.Add($attr)
+ }
+
+ $dynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter($Name, [string], $attributeCollection)
+
+ $ParamDictionary.Add($Name, $dynParam1)
+ }
+
+ function Test-DynamicParam {
+ [CmdletBinding()]
+ param(
+ [String]$Name
+ )
+
+ dynamicparam {
+ if ($Name.StartsWith("Hello")) {
+ $paramDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
+ New-DynamicAttr -ParamDictionary $paramDictionary -Name "PSEdition"
+
+ return $paramDictionary
+ }
+ }
+
+ process {
+ if ($PSBoundParameters.PSEdition) {
+ Write-Host "PSEdition value: $($PSBoundParameters.PSEdition)"
+ }
+ }
+ }
+ }
+
+ Context 'Mocking with ParameterFilter' {
+ It 'Mocks Test-DynamicParam with PSEdition set to Desktop' {
+ Mock Test-DynamicParam { "World" } -ParameterFilter { $_PSEdition -eq 'Desktop' }
+
+ Test-DynamicParam -Name "Hello" -PSEdition 'Desktop' | Should -Be 'World'
+ }
+ }
+
+ Context 'Validating Mock Invocation' {
+ It 'Invokes Test-DynamicParam with correct parameters' {
+ Mock Test-DynamicParam { "World" }
+
+ Test-DynamicParam -Name "Hello" -PSEdition 'Desktop' | Should -Be 'World'
+
+ Should -Invoke Test-DynamicParam -Exactly 1 -Scope It
+ }
+ }
+}
diff --git a/tst/functions/Set-ItResult.Tests.ps1 b/tst/functions/Set-ItResult.Tests.ps1
index ed7f642a9..91622e4be 100644
--- a/tst/functions/Set-ItResult.Tests.ps1
+++ b/tst/functions/Set-ItResult.Tests.ps1
@@ -19,6 +19,15 @@ Describe "Testing Set-ItResult" {
}
}
+ It "Set-ItResult appends the -Because reason to the message" {
+ try {
+ Set-ItResult -Skipped -Because "we are forcing it to skip"
+ }
+ catch {
+ $_.Exception.Message | Should -Be "is skipped, because we are forcing it to skip"
+ }
+ }
+
It "Set-ItResult can be called without -Because" {
try {
Set-ItResult -Skipped
diff --git a/tst/functions/SetupTeardown.Tests.ps1 b/tst/functions/SetupTeardown.Tests.ps1
index 864ffa0e2..8b0f6236f 100644
--- a/tst/functions/SetupTeardown.Tests.ps1
+++ b/tst/functions/SetupTeardown.Tests.ps1
@@ -190,6 +190,25 @@ Describe 'Finishing TestGroup Setup and Teardown tests' {
}
}
+Describe 'Unbound scriptsblocks as input' {
+ # Unbound scriptblocks would execute in Pester's internal module state
+ BeforeAll {
+ $sb = [scriptblock]::Create('')
+ $expectedMessage = 'Unbound scriptblock*'
+ }
+ It 'Throws when provided to BeforeAll' {
+ { BeforeAll -Scriptblock $sb } | Should -Throw -ExpectedMessage $expectedMessage
+ }
+ It 'Throws when provided to AfterAll' {
+ { AfterAll -Scriptblock $sb } | Should -Throw -ExpectedMessage $expectedMessage
+ }
+ It 'Throws when provided to BeforeEach' {
+ { BeforeEach -Scriptblock $sb } | Should -Throw -ExpectedMessage $expectedMessage
+ }
+ It 'Throws when provided to AfterEach' {
+ { AfterEach -Scriptblock $sb } | Should -Throw -ExpectedMessage $expectedMessage
+ }
+}
# if ($PSVersionTable.PSVersion.Major -ge 3) {
# # TODO: this depends on the old pester internals it would be easier to test in P
diff --git a/tst/functions/assert/Collection/Should-All.Tests.ps1 b/tst/functions/assert/Collection/Should-All.Tests.ps1
index b02c7efca..51e431664 100644
--- a/tst/functions/assert/Collection/Should-All.Tests.ps1
+++ b/tst/functions/assert/Collection/Should-All.Tests.ps1
@@ -57,4 +57,10 @@ Expected [int] 2, but got [int] 1." -replace "`r`n", "`n")
It 'It fails when the only item not matching the filter is 0' {
{ 0 | Should-All -FilterScript { $_ -gt 0 } } | Verify-AssertionFailed
}
+
+ It 'Throws when provided unbound scriptblock' {
+ # Unbound scriptblocks would execute in Pester's internal module state
+ $ex = { 1 | Should-All ([scriptblock]::Create('')) } | Verify-Throw
+ $ex.Exception.Message | Verify-Like 'Unbound scriptblock*'
+ }
}
diff --git a/tst/functions/assert/Collection/Should-Any.Tests.ps1 b/tst/functions/assert/Collection/Should-Any.Tests.ps1
index 03fe6fd9f..bfd5f1279 100644
--- a/tst/functions/assert/Collection/Should-Any.Tests.ps1
+++ b/tst/functions/assert/Collection/Should-Any.Tests.ps1
@@ -61,4 +61,10 @@ Expected [int] 2, but got [int] 1." -replace "`r`n", "`n")
It "Accepts FilterScript and Actual by position" {
Should-Any { $true } 1, 2
}
+
+ It 'Throws when provided unbound scriptblock' {
+ # Unbound scriptblocks would execute in Pester's internal module state
+ $ex = { 1 | Should-Any ([scriptblock]::Create('')) } | Verify-Throw
+ $ex.Exception.Message | Verify-Like 'Unbound scriptblock*'
+ }
}
diff --git a/tst/functions/assert/Collection/Should-BeCollection.Tests.ps1 b/tst/functions/assert/Collection/Should-BeCollection.Tests.ps1
index ef5dab0de..7038df09c 100644
--- a/tst/functions/assert/Collection/Should-BeCollection.Tests.ps1
+++ b/tst/functions/assert/Collection/Should-BeCollection.Tests.ps1
@@ -24,4 +24,26 @@ Describe "Should-BeCollection" {
$err = { $actual | Should-BeCollection $expected } | Verify-AssertionFailed
$err.Exception.Message | Verify-Equal "Expected [Object[]] @(5, 6, 7, 8, 9) to be present in [Object[]] @(1, 2, 3, 4, 5) in any order, but some values were not.`nMissing in actual: '6 (index 1), 7 (index 2), 8 (index 3), 9 (index 4)'`nExtra in actual: '1 (index 0), 2 (index 1), 3 (index 2), 4 (index 3)'"
}
+
+ Describe "-Count" {
+ It "Counts empty collection @() correctly" {
+ @() | Should-BeCollection -Count 0
+ }
+
+ It "Counts collection with one item correctly" -ForEach @(
+ @(1),
+ (, @()), # array in array
+ @($null),
+ @(""),
+ # we also cannot distinguish between a single item and a single item array
+ 1
+ ) {
+ $_ | Should-BeCollection -Count 1
+ }
+
+ It "Fails when collection does not have the expected number of items" {
+ $err = { @(1, 2) | Should-BeCollection -Count 3 } | Verify-AssertionFailed
+ $err.Exception.Message | Verify-Equal "Expected 3 items in [Object[]] @(1, 2), but it has 2 items."
+ }
+ }
}
diff --git a/tst/functions/assert/Exception/Should-Throw.Tests.ps1 b/tst/functions/assert/Exception/Should-Throw.Tests.ps1
index 4c2cc6084..cf0c516a9 100644
--- a/tst/functions/assert/Exception/Should-Throw.Tests.ps1
+++ b/tst/functions/assert/Exception/Should-Throw.Tests.ps1
@@ -10,7 +10,6 @@ Describe "Should-Throw" {
}
It "Passes when non-terminating exception is thrown" {
-
{ Write-Error "fail!" } | Should-Throw
}
@@ -23,6 +22,12 @@ Describe "Should-Throw" {
Should-Throw 'MockErrorMessage' 'MockErrorId' ([Microsoft.PowerShell.Commands.WriteErrorException]) 'MockBecauseString'
}
+ It 'Throws when provided unbound scriptblock' {
+ # Unbound scriptblocks would execute in Pester's internal module state
+ $ex = { ([scriptblock]::Create('')) | Should-Throw } | Verify-Throw
+ $ex.Exception.Message | Verify-Like 'Unbound scriptblock*'
+ }
+
Context "Filtering with exception type" {
It "Passes when exception has the expected type" {
{ throw [ArgumentException]"A is null!" } | Should-Throw -ExceptionType ([ArgumentException])