From b1c8c28e3d01676c6656f396673c884f93830a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Fri, 17 Jul 2026 08:44:34 +0200 Subject: [PATCH] Exclude directories, not just files, in ExcludePath Filter-Excluded only matched discovered test files against each ExcludePath entry with -like, so a directory path never matched any file inside it and directory exclusions were silently ignored, even though Run.ExcludePath is documented as "Directories or files to be excluded from the run." Keep the existing wildcard/exact -like matching and additionally treat a non-wildcard exclusion as a directory prefix: a file is excluded when its full path starts with the exclusion plus a directory separator. Trailing separators are trimmed so 'dir' and 'dir\' behave the same, and the prefix comparison is case-insensitive on Windows and case-sensitive on Linux/macOS. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Pester.RSpec.ps1 | 30 ++++++++++++++++++++++++++++-- tst/Pester.RSpec.ts.ps1 | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/Pester.RSpec.ps1 b/src/Pester.RSpec.ps1 index e53f56a9c..c556711b1 100644 --- a/src/Pester.RSpec.ps1 +++ b/src/Pester.RSpec.ps1 @@ -118,15 +118,41 @@ function Filter-Excluded ($Files, $ExcludePath) { return @($Files) } + # Directory exclusions are compared as path prefixes below. Match them + # case-insensitively on Windows, but case-sensitively on Linux and macOS + # where the filesystem is case-sensitive. + $pathComparison = if ('Windows' -eq (GetPesterOs)) { + [System.StringComparison]::OrdinalIgnoreCase + } + else { + [System.StringComparison]::Ordinal + } + + # normalize backslashes for cross-platform ease of use + $exclusions = @($ExcludePath) -replace "/", "\" + foreach ($file in @($Files)) { # normalize backslashes for cross-platform ease of use $p = $file.FullName -replace "/", "\" $excluded = $false - foreach ($exclusion in (@($ExcludePath) -replace "/", "\")) { + foreach ($exclusion in $exclusions) { + # Wildcard patterns and exact file paths keep the original -like behavior. if ($p -like $exclusion) { $excluded = $true - continue + break + } + + # A directory exclusion (e.g. C:\proj\excluded) should exclude every file + # underneath it, see https://github.com/pester/Pester/issues/1575. Trailing + # separators are trimmed so both 'C:\proj\excluded' and 'C:\proj\excluded\' + # behave the same. Wildcard patterns are already handled by -like above. + if (-not [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($exclusion)) { + $prefix = $exclusion.TrimEnd("\") + "\" + if ($p.StartsWith($prefix, $pathComparison)) { + $excluded = $true + break + } } } diff --git a/tst/Pester.RSpec.ts.ps1 b/tst/Pester.RSpec.ts.ps1 index 0b165a9d5..af6342b50 100644 --- a/tst/Pester.RSpec.ts.ps1 +++ b/tst/Pester.RSpec.ts.ps1 @@ -280,6 +280,42 @@ i -PassThru:$PassThru { } } + b "Excluding directories from a run" { + try { + $path = $pwd + $c = 'Describe "d1" { It "i1" { $true } }' + $root = Join-Path ([IO.Path]::GetTempPath()) "excludepath-dir-$([Guid]::NewGuid().Guid)" + $includedDir = Join-Path $root "included" + $excludedDir = Join-Path $root "excluded" + New-Item -ItemType Directory -Path $includedDir -Force | Out-Null + New-Item -ItemType Directory -Path $excludedDir -Force | Out-Null + + $includedFile = Join-Path $includedDir "included.Tests.ps1" + $excludedFile = Join-Path $excludedDir "excluded.Tests.ps1" + $c | Set-Content $includedFile + $c | Set-Content $excludedFile + + t "Excluding a directory excludes the tests underneath it but keeps sibling tests" { + $result = Invoke-Pester -Path $root -ExcludePath $excludedDir -PassThru + + $result.Containers.Count | Verify-Equal 1 + $result.Containers[0].Item.FullName | Verify-Equal $includedFile + } + + t "Excluding a directory with a trailing separator excludes the tests underneath it" { + $excludedWithSeparator = $excludedDir + [System.IO.Path]::DirectorySeparatorChar + $result = Invoke-Pester -Path $root -ExcludePath $excludedWithSeparator -PassThru + + $result.Containers.Count | Verify-Equal 1 + $result.Containers[0].Item.FullName | Verify-Equal $includedFile + } + } + finally { + cd $path + Remove-Item $root -Recurse -Force -Confirm:$false -ErrorAction Stop + } + } + b "Terminating and non-terminating Should" { t "Non-terminating assertion fails the test after running to completion" { $sb = {