Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/6.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,10 @@ both recorded calls:

### Other improvements

- **`-TagFilter 'None'` runs tests that have no tags.** `None` (case-insensitive) is a reserved filter
value that selects tests with no tag on themselves or any parent block. `-ExcludeTagFilter 'None'`
skips those untagged tests so you can run only the tagged ones, and you can combine `None` with real
tags, for example `-TagFilter None, Acceptance`. See [Tags](https://pester.dev/docs/usage/tags).
- `Run.SkipRemainingOnFailure` (`None`, `Run`, `Container`, `Block`) skips the rest of a scope once a
test fails, and remaining tests are skipped when a block setup/teardown fails.
- `Run.FailOnNullOrEmptyForEach` (on by default) fails discovery when `-ForEach` gets `$null` or
Expand Down Expand Up @@ -491,6 +495,11 @@ both recorded calls:
- **The `Pending` test status was removed.** `Set-ItResult` no longer has a `-Pending` parameter;
use `-Skipped` or `-Inconclusive` instead.
- **The `-Focus` switch was removed.** `Describe`, `Context`, and `It` no longer accept `-Focus`, and the `Focus` property is gone from the result object's blocks and tests. Use `-Skip`, tags, or the `Filter` configuration to select which tests run.
- **`None` is now a reserved tag-filter value.** `-TagFilter 'None'` / `Filter.Tag = 'None'` (and the
matching `-ExcludeTagFilter` / `Filter.ExcludeTag`) now mean "tests that have no tags" instead of
matching a literal tag named `None`. If you used `None` as a real tag, filtering by it now also
selects — or, for the exclude filter, skips — every untagged test. Rename the tag if you relied on
the old literal match. `None` is compared case-insensitively.
- **Profiler-based code coverage is the default.** Set `CodeCoverage.UseBreakpoints = $true` to
restore breakpoint-based coverage.
- **The `CodeCoverage.OutputFormat = 'CoverageGutters'` value was removed.** All coverage output is
Expand Down
52 changes: 50 additions & 2 deletions src/Pester.Runtime.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1732,6 +1732,29 @@ function Switch-Timer {
}
}

function Test-HasNoEffectiveTag {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
$Item
)

# An item has no effective tag when neither the item itself nor any of its
# parent blocks has a tag. Tags inherit downwards in Pester (a tag on a block
# applies to everything inside it), so we walk from the item up to the root
# block and stop as soon as we find any tag.
$node = $Item
while ($null -ne $node -and -not $node.IsRoot) {
if ($null -ne $node.Tag -and 0 -ne $node.Tag.Count) {
return $false
}

$node = if ('Test' -eq $node.ItemType) { $node.Block } else { $node.Parent }
}

return $true
}

function Test-ShouldRun {
[CmdletBinding()]
param (
Expand Down Expand Up @@ -1778,6 +1801,17 @@ function Test-ShouldRun {
# item is excluded when any of the exclude tags match
$tagFilter = $Filter.ExcludeTag
if ($tagFilter -and 0 -ne $tagFilter.Count) {
# the special 'None' filter excludes tests that have no tags on themselves
# or on any of their parent blocks. It only applies to tests (leaf items),
# so that a tagged test inside an otherwise untagged block is kept.
if (('Test' -eq $Item.ItemType) -and ($tagFilter -contains 'None') -and (Test-HasNoEffectiveTag -Item $Item)) {
if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Filter "($fullDottedPath) $($Item.ItemType) is excluded, because it has no tags and the exclude tag filter contains 'None'."
}
$result.Exclude = $true
return $result
}

foreach ($f in $tagFilter) {
foreach ($t in $Item.Tag) {
if ($t -like $f) {
Expand Down Expand Up @@ -1845,11 +1879,25 @@ function Test-ShouldRun {
return $result
}

# test is included when it has tags and the any of the tags match
# test is included when it has tags and any of the tags match,
# or when the filter contains the special 'None' value and the test has no
# tags on itself or on any of its parent blocks
$tagFilter = $Filter.Tag
if ($tagFilter -and 0 -ne $tagFilter.Count) {
$anyIncludeFilters = $true
if ($null -eq $Item.Tag -or 0 -eq $Item.Tag) {

# the special 'None' filter includes tests that have no tags on themselves
# or on any of their parent blocks. It only applies to tests (leaf items),
# so that an untagged block does not force-include its tagged children.
if (('Test' -eq $Item.ItemType) -and ($tagFilter -contains 'None') -and (Test-HasNoEffectiveTag -Item $Item)) {
if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Filter "($fullDottedPath) $($Item.ItemType) is included, because it has no tags and the tag filter contains 'None'."
}
$result.Include = $true
return $result
}

if ($null -eq $Item.Tag -or 0 -eq $Item.Tag.Count) {
if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Filter "($fullDottedPath) $($Item.ItemType) has no tags, moving to next include filter."
}
Expand Down
4 changes: 2 additions & 2 deletions src/csharp/Pester/FilterConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ public FilterConfiguration(IDictionary configuration) : this()
}
public FilterConfiguration() : base("Filter options to include/exclude tests and blocks in the targeted containers using tags, name or location. Include by default when no include filters are provided. Exclude filters take precedence.")
{
Tag = new StringArrayOption("Tags of Describe, Context or It to be run.", new string[0]);
ExcludeTag = new StringArrayOption("Tags of Describe, Context or It to be excluded from the run.", new string[0]);
Tag = new StringArrayOption("Tags of Describe, Context or It to be run. Use 'None' to run only tests that have no tags.", new string[0]);
ExcludeTag = new StringArrayOption("Tags of Describe, Context or It to be excluded from the run. Use 'None' to exclude tests that have no tags.", new string[0]);
Line = new StringArrayOption(@"Filter by file and scriptblock start line, useful to run parsed tests programmatically to avoid problems with expanded names. Explicit filter that overrides -Skip. Example: 'C:\tests\file1.Tests.ps1:37'", new string[0]);
ExcludeLine = new StringArrayOption("Exclude by file and scriptblock start line, takes precedence over Line.", new string[0]);
FullName = new StringArrayOption("Full name of test with -like wildcards, joined by dot. Example: '*.describe Get-Item.test1'", new string[0]);
Expand Down
4 changes: 2 additions & 2 deletions src/en-US/about_PesterConfiguration.help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ SECTIONS AND OPTIONS
Default value: '<path of .git>'

Filter:
Tag: Tags of Describe, Context or It to be run.
Tag: Tags of Describe, Context or It to be run. Use 'None' to run only tests that have no tags.
Type: string[]
Default value: @()

ExcludeTag: Tags of Describe, Context or It to be excluded from the run.
ExcludeTag: Tags of Describe, Context or It to be excluded from the run. Use 'None' to exclude tests that have no tags.
Type: string[]
Default value: @()

Expand Down
149 changes: 149 additions & 0 deletions tst/Pester.Rspec.Filtering.ts.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,155 @@ i -PassThru:$PassThru {
}
}

b "Filtering on the 'None' tag (tests without tags)" {
t "Including 'None' runs tests that have no tags and skips tagged tests" {
$sb = {
Describe "a" {
It "untagged" { }
It "tagged" -Tag "x" { }
}
}
$r = Invoke-Pester -Configuration ([PesterConfiguration]@{ Run = @{ ScriptBlock = $sb; PassThru = $true }; Filter = @{ Tag = "None" } })

$r.Containers[0].Blocks[0].Tests[0].Result | Verify-Equal "Passed"
$r.Containers[0].Blocks[0].Tests[1].Result | Verify-Equal "NotRun"
}

t "Including 'None' does not run an untagged test when a parent block is tagged" {
$sb = {
Describe "tagged block" -Tag "x" {
It "no own tag" { }
}
Describe "untagged block" {
It "no tag" { }
}
}
$r = Invoke-Pester -Configuration ([PesterConfiguration]@{ Run = @{ ScriptBlock = $sb; PassThru = $true }; Filter = @{ Tag = "None" } })

# inherited tag from parent block counts, so this test is effectively tagged
$r.Containers[0].Blocks[0].Tests[0].Result | Verify-Equal "NotRun"
$r.Containers[0].Blocks[1].Tests[0].Result | Verify-Equal "Passed"
}

t "Including 'None' runs untagged tests nested in untagged blocks" {
$sb = {
Describe "a" {
Context "b" {
It "t" { }
}
}
}
$r = Invoke-Pester -Configuration ([PesterConfiguration]@{ Run = @{ ScriptBlock = $sb; PassThru = $true }; Filter = @{ Tag = "None" } })

$r.Containers[0].Blocks[0].Blocks[0].Tests[0].Result | Verify-Equal "Passed"
}

t "Including 'None' does not force-run tagged siblings inside an untagged block" {
$sb = {
Describe "a" {
It "untagged" { }
It "fast" -Tag "Fast" { }
}
}
$r = Invoke-Pester -Configuration ([PesterConfiguration]@{ Run = @{ ScriptBlock = $sb; PassThru = $true }; Filter = @{ Tag = "None" } })

$r.Containers[0].Blocks[0].Tests[0].Result | Verify-Equal "Passed"
$r.Containers[0].Blocks[0].Tests[1].Result | Verify-Equal "NotRun"
}

t "Including 'None' also matches a test that is literally tagged 'None'" {
$sb = {
Describe "a" {
It "literal none" -Tag "None" { }
It "other" -Tag "x" { }
}
}
$r = Invoke-Pester -Configuration ([PesterConfiguration]@{ Run = @{ ScriptBlock = $sb; PassThru = $true }; Filter = @{ Tag = "None" } })

$r.Containers[0].Blocks[0].Tests[0].Result | Verify-Equal "Passed"
$r.Containers[0].Blocks[0].Tests[1].Result | Verify-Equal "NotRun"
}

t "Including 'None' is case-insensitive" {
$sb = {
Describe "a" {
It "untagged" { }
It "tagged" -Tag "x" { }
}
}
$r = Invoke-Pester -Configuration ([PesterConfiguration]@{ Run = @{ ScriptBlock = $sb; PassThru = $true }; Filter = @{ Tag = "nOnE" } })

$r.Containers[0].Blocks[0].Tests[0].Result | Verify-Equal "Passed"
$r.Containers[0].Blocks[0].Tests[1].Result | Verify-Equal "NotRun"
}

t "Combining 'None' with another tag runs untagged tests and tests with that tag" {
$sb = {
Describe "a" {
It "untagged" { }
It "fast" -Tag "Fast" { }
It "slow" -Tag "Slow" { }
}
}
$r = Invoke-Pester -Configuration ([PesterConfiguration]@{ Run = @{ ScriptBlock = $sb; PassThru = $true }; Filter = @{ Tag = "None", "Fast" } })

$r.Containers[0].Blocks[0].Tests[0].Result | Verify-Equal "Passed"
$r.Containers[0].Blocks[0].Tests[1].Result | Verify-Equal "Passed"
$r.Containers[0].Blocks[0].Tests[2].Result | Verify-Equal "NotRun"
}

t "Excluding 'None' skips untagged tests and runs tagged tests" {
$sb = {
Describe "a" {
It "untagged" { }
It "tagged" -Tag "x" { }
}
}
$r = Invoke-Pester -Configuration ([PesterConfiguration]@{ Run = @{ ScriptBlock = $sb; PassThru = $true }; Filter = @{ ExcludeTag = "None" } })

$r.Containers[0].Blocks[0].Tests[0].Result | Verify-Equal "NotRun"
$r.Containers[0].Blocks[0].Tests[1].Result | Verify-Equal "Passed"
}

t "Excluding 'None' keeps a tagged test inside an untagged block" {
$sb = {
Describe "a" {
It "fast" -Tag "Fast" { }
It "untagged" { }
}
}
$r = Invoke-Pester -Configuration ([PesterConfiguration]@{ Run = @{ ScriptBlock = $sb; PassThru = $true }; Filter = @{ ExcludeTag = "None" } })

$r.Containers[0].Blocks[0].Tests[0].Result | Verify-Equal "Passed"
$r.Containers[0].Blocks[0].Tests[1].Result | Verify-Equal "NotRun"
}

t "Excluding 'None' keeps an untagged test when a parent block is tagged" {
$sb = {
Describe "tagged block" -Tag "x" {
It "no own tag" { }
}
}
$r = Invoke-Pester -Configuration ([PesterConfiguration]@{ Run = @{ ScriptBlock = $sb; PassThru = $true }; Filter = @{ ExcludeTag = "None" } })

# inherited tag from parent block counts, so this test is not excluded
$r.Containers[0].Blocks[0].Tests[0].Result | Verify-Equal "Passed"
}

t "Excluding 'None' also excludes a test that is literally tagged 'None'" {
$sb = {
Describe "a" {
It "literal none" -Tag "None" { }
It "other" -Tag "x" { }
}
}
$r = Invoke-Pester -Configuration ([PesterConfiguration]@{ Run = @{ ScriptBlock = $sb; PassThru = $true }; Filter = @{ ExcludeTag = "None" } })

$r.Containers[0].Blocks[0].Tests[0].Result | Verify-Equal "NotRun"
$r.Containers[0].Blocks[0].Tests[1].Result | Verify-Equal "Passed"
}
}

b "Running skipped tests explicitly" {
t "Having a skipped test will skip it" {
$sb = {
Expand Down
Loading