Skip to content

Commit 1ffed2f

Browse files
committed
Find the repository root from the session location, not the process working directory
Run.RepoRoot decides which Pester.BeforeContainer.ps1 files apply, and its default is found by walking up for a .git directory. The walk ran in C# from Directory.GetCurrentDirectory(), the process working directory, and Set-Location does not change that. So a session that started somewhere else and then changed directory into a repository kept a RepoRoot pointing at the old place, and the whole setup chain silently did not apply, with nothing to say why: pwsh # process working directory is now ~ cd ~/p/myrepo Invoke-Pester ./tests # RepoRoot was ~, no Pester.BeforeContainer.ps1 applied FindRepoRoot takes the directory to start from now, and Invoke-Pester resolves RepoRoot from $ExecutionContext.SessionState.Path.CurrentFileSystemLocation before the run, next to where the shuffle seed is resolved. Same place Pester.Parallel.ps1 already takes the working directory it hands to workers. Only when the option was not set, an explicit RepoRoot is the user's to decide and is left alone. The parameterless overload stays as the value an unused configuration object shows, so [PesterConfiguration]::Default still reports a directory rather than nothing. Tests: the cascade applies after Set-Location with no RepoRoot set, and the test asserts the two locations actually disagree so it cannot pass by accident; an explicit RepoRoot survives the run; FindRepoRoot falls back to the directory it started from when there is no .git. Verified the first one fails without the change (43/44) and passes with it (44/44). Full suite on PS 7.5 macOS: P phase clean, RSpec 2921 passed, 0 failed, 3 skipped. about_PesterConfiguration.help.txt is regenerated by build.ps1 -Clean, so it is in the same commit. 🤖
1 parent 1d71968 commit 1ffed2f

5 files changed

Lines changed: 94 additions & 6 deletions

File tree

docs/NEXT-RELEASE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ The whole chain works in a sequential run and in a parallel run. In parallel the
123123
resolved once in the parent and the paths are handed to the workers, because a worker is a
124124
separate runspace and cannot share the cache.
125125

126+
`Run.RepoRoot` is where the chain starts, and it is found for you by walking up from the
127+
directory you are in until a `.git` folder shows up. That walk used to start from the process
128+
working directory, which `Set-Location` does not change, so a session that started somewhere
129+
else and then changed directory into a repository got a root pointing at the old place and
130+
none of the setup files applied, with nothing to say why. It starts from the location the
131+
session is actually in now. Set `Run.RepoRoot` yourself when your tests do not live in a git
132+
repository, or when the root is somewhere other than where `.git` is.
133+
126134
Which files apply is a property of the directory, not of the container, so the run shares one
127135
cache keyed by directory. Each directory is checked on disk once per run and each setup file
128136
is tokenized once per run, however many test folders sit below them. On a tree with 60 test

src/Main.ps1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,16 @@ function Invoke-Pester {
560560
# Write-PesterDebugMessage is used regardless of WriteScreenPlugin.
561561
Resolve-OutputConfiguration -PesterPreference $PesterPreference
562562

563+
# Resolve the repository root once for the whole run, from the location the session is
564+
# actually in. The default the configuration object carries is found in C# from the
565+
# process working directory, and Set-Location does not change that, so a session that
566+
# started somewhere else and then changed directory into a repository kept a RepoRoot
567+
# pointing at the old place, and every Pester.BeforeContainer.ps1 silently did not apply.
568+
# Only when the user did not set it, an explicit RepoRoot is theirs to decide.
569+
if (-not $PesterPreference.Run.RepoRoot.IsModified) {
570+
$PesterPreference.Run.RepoRoot = [Pester.RunConfiguration]::FindRepoRoot($ExecutionContext.SessionState.Path.CurrentFileSystemLocation.Path)
571+
}
572+
563573
# Resolve the shuffle seed once for the whole run (#2425), so it is reported a single
564574
# time and shared by every container - including parallel workers, which each receive
565575
# this resolved configuration. ShuffleSeed 0 means "pick a new seed for this run".

src/csharp/Pester/RunConfiguration.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public RunConfiguration(IDictionary configuration) : this()
8686
FailOnNullOrEmptyForEach = new BoolOption("Fails discovery when -ForEach is provided $null or @() in a block or test. Can be overridden for a specific Describe/Context/It using -AllowNullOrEmptyForEach.", true);
8787
Shuffle = new BoolOption("EXPERIMENTAL: Shuffle the order in which test files, and the blocks (Describe/Context) and tests (It) inside them, are executed. Items are only reordered within their own level. Uses Run.ShuffleSeed so a run can be repeated, and helps surface hidden dependencies between tests. A single file can opt out with a '#pester:no-shuffle' comment.", false);
8888
ShuffleSeed = new IntOption("EXPERIMENTAL: Seed used to shuffle execution order when Run.Shuffle is enabled. The default 0 picks a new seed for each run and reports it at the start, so the run can be repeated by setting Run.ShuffleSeed to that value.", 0);
89-
RepoRoot = new StringOption("EXPERIMENTAL: Root directory of the repository. Found by searching for the .git directory recursively. When not found, the current working directory is used. Before each test file is discovered and run - in both sequential and parallel runs - Pester dot-sources every 'Pester.BeforeContainer.ps1' found from this directory down to the test file's own folder, outermost first, so helper modules or dot-sourced setup the parent session would normally provide are available to every container. Setup shared by all tests can live at the root while setup only some tests need lives in their folder. The files run before each container and must be safe to run more than once. This is especially useful in parallel runs where each worker starts from a clean runspace and re-runs them.", FindRepoRoot());
89+
RepoRoot = new StringOption("EXPERIMENTAL: Root directory of the repository. Found when the run starts, by searching for the .git directory upwards from the current location. When not found, the current location is used. Before each test file is discovered and run - in both sequential and parallel runs - Pester dot-sources every 'Pester.BeforeContainer.ps1' found from this directory down to the test file's own folder, outermost first, so helper modules or dot-sourced setup the parent session would normally provide are available to every container. Setup shared by all tests can live at the root while setup only some tests need lives in their folder. The files run before each container and must be safe to run more than once. This is especially useful in parallel runs where each worker starts from a clean runspace and re-runs them.", FindRepoRoot());
9090
}
9191

9292
public StringArrayOption Path
@@ -345,21 +345,37 @@ public IntOption ShuffleSeed
345345
}
346346
}
347347

348-
private static string FindRepoRoot()
348+
/// <summary>
349+
/// Walks up from startDirectory looking for a .git directory and returns the first directory
350+
/// that has one, or startDirectory when there is none above it.
351+
/// </summary>
352+
public static string FindRepoRoot(string startDirectory)
349353
{
350-
var originalDir = Directory.GetCurrentDirectory();
351-
var currentDir = originalDir;
354+
if (string.IsNullOrWhiteSpace(startDirectory))
355+
{
356+
return startDirectory;
357+
}
358+
359+
var currentDir = startDirectory;
352360
while (!Directory.Exists(System.IO.Path.Combine(currentDir, ".git")))
353361
{
354362
var parentDir = Directory.GetParent(currentDir);
355363
if (parentDir == null)
356364
{
357-
return originalDir;
365+
return startDirectory;
358366
}
359367
currentDir = parentDir.FullName;
360368
}
361369
return currentDir;
370+
}
362371

372+
private static string FindRepoRoot()
373+
{
374+
// The process working directory, which is not the caller's PowerShell location: Set-Location
375+
// changes the location without changing this. So this is only a placeholder for a configuration
376+
// object that has not been used for a run yet, Invoke-Pester resolves RepoRoot again from the
377+
// session's own location before the run starts.
378+
return FindRepoRoot(Directory.GetCurrentDirectory());
363379
}
364380
}
365381
}

src/en-US/about_PesterConfiguration.help.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ SECTIONS AND OPTIONS
8181
Type: bool
8282
Default value: $true
8383

84-
RepoRoot: EXPERIMENTAL: Root directory of the repository. Found by searching for the .git directory recursively. When not found, the current working directory is used. Before each test file is discovered and run - in both sequential and parallel runs - Pester dot-sources every 'Pester.BeforeContainer.ps1' found from this directory down to the test file's own folder, outermost first, so helper modules or dot-sourced setup the parent session would normally provide are available to every container. Setup shared by all tests can live at the root while setup only some tests need lives in their folder. The files run before each container and must be safe to run more than once. This is especially useful in parallel runs where each worker starts from a clean runspace and re-runs them.
84+
RepoRoot: EXPERIMENTAL: Root directory of the repository. Found when the run starts, by searching for the .git directory upwards from the current location. When not found, the current location is used. Before each test file is discovered and run - in both sequential and parallel runs - Pester dot-sources every 'Pester.BeforeContainer.ps1' found from this directory down to the test file's own folder, outermost first, so helper modules or dot-sourced setup the parent session would normally provide are available to every container. Setup shared by all tests can live at the root while setup only some tests need lives in their folder. The files run before each container and must be safe to run more than once. This is especially useful in parallel runs where each worker starts from a clean runspace and re-runs them.
8585
Type: string
8686
Default value: '<path of .git>'
8787

tst/Pester.RSpec.Parallel.ts.ps1

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,60 @@ Describe 'Second' {
409409
finally { Remove-Item -Path $folder -Recurse -Force }
410410
}
411411

412+
t "finds the repository root from the session location, not the process working directory" {
413+
# Run.RepoRoot's default used to be found in C# from Directory.GetCurrentDirectory(),
414+
# the process working directory, which Set-Location does not change. So a session that
415+
# started somewhere else and then changed directory into a repository kept a RepoRoot
416+
# pointing at the old place and the whole cascade silently did not apply. Nothing here
417+
# sets Run.RepoRoot, the run has to find it on its own.
418+
$folder = New-CascadingBeforeContainerFolder
419+
$null = New-Item -ItemType Directory -Path (Join-Path $folder '.git') -Force
420+
try {
421+
Push-Location -Path $folder
422+
try {
423+
# The test only means something while the two locations disagree, which is the
424+
# situation the fix is about.
425+
$processDirectory = [System.IO.Directory]::GetCurrentDirectory()
426+
($processDirectory -eq $ExecutionContext.SessionState.Path.CurrentFileSystemLocation.Path) | Verify-False
427+
428+
$c = [PesterConfiguration]::Default
429+
$c.Run.Path = $folder
430+
$c.Run.PassThru = $true
431+
$c.Output.Verbosity = 'None'
432+
$r = Invoke-Pester -Configuration $c
433+
434+
$r.FailedCount | Verify-Equal 0
435+
$r.PassedCount | Verify-Equal 2
436+
}
437+
finally { Pop-Location }
438+
}
439+
finally { Remove-Item -Path $folder -Recurse -Force }
440+
}
441+
442+
t "does not overwrite a Run.RepoRoot the user set" {
443+
$c = [PesterConfiguration]::Default
444+
$c.Run.RepoRoot = 'TestDrive:not-a-real-path-but-mine'
445+
$c.Run.ScriptBlock = { Describe 'd' { It 'i' { 1 | Should -Be 1 } } }
446+
$c.Run.PassThru = $true
447+
$c.Output.Verbosity = 'None'
448+
$r = Invoke-Pester -Configuration $c
449+
450+
$r.Configuration.Run.RepoRoot.Value | Verify-Equal 'TestDrive:not-a-real-path-but-mine'
451+
}
452+
453+
t "FindRepoRoot returns the directory it started from when there is no .git above it" {
454+
# The walk stops at the filesystem root and falls back to where it started, rather than
455+
# returning null or the drive root, so RepoRoot is always a usable directory.
456+
$start = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().Guid)
457+
$null = New-Item -ItemType Directory -Path $start -Force
458+
try {
459+
# GetFullPath, because the temp path is a symlink on macOS and the walk normalizes.
460+
$expected = [System.IO.Path]::GetFullPath($start)
461+
[Pester.RunConfiguration]::FindRepoRoot($expected) | Verify-Equal $expected
462+
}
463+
finally { Remove-Item -Path $start -Recurse -Force }
464+
}
465+
412466
t "applies the same cascade inside each parallel worker" {
413467
$folder = New-CascadingBeforeContainerFolder
414468
try {

0 commit comments

Comments
 (0)