diff --git a/src/Pester.RSpec.ps1 b/src/Pester.RSpec.ps1 index e15094b98..ba650d639 100644 --- a/src/Pester.RSpec.ps1 +++ b/src/Pester.RSpec.ps1 @@ -8,6 +8,12 @@ [string] $Extension ) + # Folders we never want to descend into during discovery. .git in particular can + # hold hundreds of thousands of small files on a real repo; enumerating them only + # to filter the results out later wastes a lot of time, so the recursive walk + # below stops as soon as it sees one of these names. + $skipFolders = @('.git', '.svn', '.hg') + $files = foreach ($p in $Path) { if ([String]::IsNullOrWhiteSpace($p)) { continue @@ -29,8 +35,11 @@ foreach ($item in $items) { if ($item.PSIsContainer) { - # this is an existing directory search it for tests file - & $SafeCommands['Get-ChildItem'] -Recurse -Path $item -Filter "*$Extension" -File + # Walk the directory tree ourselves so we never open the contents of + # VCS folders. Get-ChildItem -Force returns hidden items so this works + # both for dot-prefixed folders on Linux and for folders with the + # Hidden file attribute on Windows. + Find-FileInDirectory -Directory $item -Extension $Extension -SkipFolders $skipFolders } elseif ("FileSystem" -ne $item.PSProvider.Name) { # item is not a directory and exists but is not a file so we are not interested @@ -58,9 +67,22 @@ } } else { - # this is a path that does not exist so let's hope it is - # a wildcarded path that will resolve to some files - & $SafeCommands['Get-ChildItem'] -Recurse -Path $p -Filter "*$Extension" -File + # The path didn't resolve to anything, so let Get-ChildItem try to expand + # whatever shape it is (typically a wildcard pattern that currently has no + # matches). Use -Force so hidden folders are still considered, and strip any + # results that landed inside a VCS metadata directory. + foreach ($f in (& $SafeCommands['Get-ChildItem'] -Recurse -Path $p -Filter "*$Extension" -File -Force)) { + $inSkipFolder = $false + $parent = $f.Directory + while ($null -ne $parent) { + if ($skipFolders -contains $parent.Name) { + $inSkipFolder = $true + break + } + $parent = $parent.Parent + } + if (-not $inSkipFolder) { $f } + } } } @@ -70,6 +92,27 @@ Filter-Excluded -Files $uniqueFiles -ExcludePath $ExcludePath | & $SafeCommands['Where-Object'] { $_ } } +function Find-FileInDirectory { + param( + [Parameter(Mandatory = $true)] + [System.IO.DirectoryInfo] $Directory, + [Parameter(Mandatory = $true)] + [string] $Extension, + [string[]] $SkipFolders + ) + + # Files in this directory first, then descend into each subdirectory that we are + # allowed to enter. The set returned is equivalent to + # `Get-ChildItem -Recurse -Filter "*$Extension" -File -Force` rooted at $Directory, + # minus anything under one of $SkipFolders. + & $SafeCommands['Get-ChildItem'] -LiteralPath $Directory.FullName -Filter "*$Extension" -File -Force + + foreach ($d in (& $SafeCommands['Get-ChildItem'] -LiteralPath $Directory.FullName -Directory -Force)) { + if ($SkipFolders -contains $d.Name) { continue } + Find-FileInDirectory -Directory $d -Extension $Extension -SkipFolders $SkipFolders + } +} + function Filter-Excluded ($Files, $ExcludePath) { if ($null -eq $ExcludePath -or @($ExcludePath).Length -eq 0) { return @($Files) diff --git a/tst/Pester.Tests.ps1 b/tst/Pester.Tests.ps1 index 3510cac91..9d2493c22 100644 --- a/tst/Pester.Tests.ps1 +++ b/tst/Pester.Tests.ps1 @@ -249,6 +249,74 @@ InPesterModuleScope { $result.Count | Should -Be 2 } + Context 'Hidden folders and VCS metadata' { + BeforeAll { + # Reset TestDrive contents to a known shape for these tests. + Get-ChildItem 'TestDrive:\' -Force | Remove-Item -Recurse -Force + + # Visible test file in a normal subfolder. + New-Item -ItemType Directory 'TestDrive:\normal' | Out-Null + New-Item -ItemType File 'TestDrive:\normal\Visible.Tests.ps1' + + # Hidden (dot-prefixed) folder that we still want to discover. + New-Item -ItemType Directory 'TestDrive:\.hidden' | Out-Null + New-Item -ItemType File 'TestDrive:\.hidden\InHidden.Tests.ps1' + + # Hidden folder one level deep. + New-Item -ItemType Directory 'TestDrive:\.config\tests' | Out-Null + New-Item -ItemType File 'TestDrive:\.config\tests\NestedHidden.Tests.ps1' + + # .git tree that must never be enumerated. Files at multiple depths + # so we can tell if the walker descended into it. + New-Item -ItemType Directory 'TestDrive:\.git\objects\aa' | Out-Null + New-Item -ItemType File 'TestDrive:\.git\HeadLevel.Tests.ps1' + New-Item -ItemType File 'TestDrive:\.git\objects\aa\DeepInGit.Tests.ps1' + + # On Windows the dot-prefix alone does not flag a folder as hidden, + # so set the attribute explicitly. Without this the original bug + # would not reproduce on Windows because Get-ChildItem already + # returned dot-prefixed folders. Use a version short-circuit so + # $IsWindows is not evaluated under Strict mode in PowerShell 5.1 + # (where the automatic variable does not exist). + if (($PSVersionTable.PSVersion.Major -lt 6) -or $IsWindows) { + foreach ($d in '.hidden', '.config', '.git') { + $di = Get-Item -LiteralPath "TestDrive:\$d" -Force + $di.Attributes = $di.Attributes -bor [System.IO.FileAttributes]::Hidden + } + } + } + + It 'discovers test files inside dot-prefixed (hidden) folders' { + $names = @(Find-File -Path 'TestDrive:\' -Extension '.Tests.ps1' | Select-Object -ExpandProperty Name) + $names | Should -Contain 'InHidden.Tests.ps1' + } + + It 'discovers test files in nested hidden folders' { + $names = @(Find-File -Path 'TestDrive:\' -Extension '.Tests.ps1' | Select-Object -ExpandProperty Name) + $names | Should -Contain 'NestedHidden.Tests.ps1' + } + + It 'does not descend into .git directories' { + # Both files live under .git at different depths; neither should + # appear in the result, proving the walker stops at .git without + # opening its contents. + $names = @(Find-File -Path 'TestDrive:\' -Extension '.Tests.ps1' | Select-Object -ExpandProperty Name) + $names | Should -Not -Contain 'HeadLevel.Tests.ps1' + $names | Should -Not -Contain 'DeepInGit.Tests.ps1' + } + + It 'returns the same set as Get-ChildItem -Recurse -Force minus VCS folders' { + $found = @(Find-File -Path 'TestDrive:\' -Extension '.Tests.ps1' | Select-Object -ExpandProperty FullName | Sort-Object) + $expected = @( + Get-ChildItem -Path 'TestDrive:\' -Recurse -Filter '*.Tests.ps1' -File -Force | + Where-Object { $_.FullName -notmatch '[\\/]\.git[\\/]' } | + Select-Object -ExpandProperty FullName | + Sort-Object + ) + $found | Should -Be $expected + } + } + # It 'Assigns empty array and hashtable to the Arguments and Parameters properties when none are specified by the caller' { # $result = @(Find-File 'TestDrive:\SomeFile.ps1' -Extension ".Tests.ps1")