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
8 changes: 8 additions & 0 deletions docs/NEXT-RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ The whole chain works in a sequential run and in a parallel run. In parallel the
resolved once in the parent and the paths are handed to the workers, because a worker is a
separate runspace and cannot share the cache.

`Run.RepoRoot` is where the chain starts, and it is found for you by walking up from the
directory you are in until a `.git` folder shows up. That walk used to start from the process
working directory, which `Set-Location` does not change, so a session that started somewhere
else and then changed directory into a repository got a root pointing at the old place and
none of the setup files applied, with nothing to say why. It starts from the location the
session is actually in now. Set `Run.RepoRoot` yourself when your tests do not live in a git
repository, or when the root is somewhere other than where `.git` is.

Which files apply is a property of the directory, not of the container, so the run shares one
cache keyed by directory. Each directory is checked on disk once per run and each setup file
is tokenized once per run, however many test folders sit below them. On a tree with 60 test
Expand Down
10 changes: 10 additions & 0 deletions src/Main.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,16 @@ function Invoke-Pester {
# Write-PesterDebugMessage is used regardless of WriteScreenPlugin.
Resolve-OutputConfiguration -PesterPreference $PesterPreference

# Resolve the repository root once for the whole run, from the location the session is
# actually in. The default the configuration object carries is found in C# from 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 every Pester.BeforeContainer.ps1 silently did not apply.
# Only when the user did not set it, an explicit RepoRoot is theirs to decide.
if (-not $PesterPreference.Run.RepoRoot.IsModified) {
$PesterPreference.Run.RepoRoot = [Pester.RunConfiguration]::FindRepoRoot($ExecutionContext.SessionState.Path.CurrentFileSystemLocation.Path)
}

# Resolve the shuffle seed once for the whole run (#2425), so it is reported a single
# time and shared by every container - including parallel workers, which each receive
# this resolved configuration. ShuffleSeed 0 means "pick a new seed for this run".
Expand Down
26 changes: 21 additions & 5 deletions src/csharp/Pester/RunConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public RunConfiguration(IDictionary configuration) : this()
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);
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);
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);
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());
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());
}

public StringArrayOption Path
Expand Down Expand Up @@ -345,21 +345,37 @@ public IntOption ShuffleSeed
}
}

private static string FindRepoRoot()
/// <summary>
/// Walks up from startDirectory looking for a .git directory and returns the first directory
/// that has one, or startDirectory when there is none above it.
/// </summary>
public static string FindRepoRoot(string startDirectory)
{
var originalDir = Directory.GetCurrentDirectory();
var currentDir = originalDir;
if (string.IsNullOrWhiteSpace(startDirectory))
{
return startDirectory;
}

var currentDir = startDirectory;
while (!Directory.Exists(System.IO.Path.Combine(currentDir, ".git")))
{
var parentDir = Directory.GetParent(currentDir);
if (parentDir == null)
{
return originalDir;
return startDirectory;
}
currentDir = parentDir.FullName;
}
return currentDir;
}

private static string FindRepoRoot()
{
// The process working directory, which is not the caller's PowerShell location: Set-Location
// changes the location without changing this. So this is only a placeholder for a configuration
// object that has not been used for a run yet, Invoke-Pester resolves RepoRoot again from the
// session's own location before the run starts.
return FindRepoRoot(Directory.GetCurrentDirectory());
}
}
}
2 changes: 1 addition & 1 deletion src/en-US/about_PesterConfiguration.help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ SECTIONS AND OPTIONS
Type: bool
Default value: $true

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.
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.
Type: string
Default value: '<path of .git>'

Expand Down
63 changes: 63 additions & 0 deletions tst/Pester.RSpec.Parallel.ts.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,69 @@ Describe 'Second' {
finally { Remove-Item -Path $folder -Recurse -Force }
}

t "finds the repository root from the session location, not the process working directory" {
# Run.RepoRoot's default used to be found in C# from Directory.GetCurrentDirectory(),
# the process working directory, which Set-Location does not change. 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 cascade silently did not apply. Nothing here
# sets Run.RepoRoot, the run has to find it on its own.
$folder = New-CascadingBeforeContainerFolder
$null = New-Item -ItemType Directory -Path (Join-Path $folder '.git') -Force
try {
Push-Location -Path $folder
try {
# The test only means something while the two locations disagree, which is the
# situation the fix is about.
$processDirectory = [System.IO.Directory]::GetCurrentDirectory()
($processDirectory -eq $ExecutionContext.SessionState.Path.CurrentFileSystemLocation.Path) | Verify-False

$c = [PesterConfiguration]::Default
$c.Run.Path = $folder
$c.Run.PassThru = $true
$c.Output.Verbosity = 'None'
$r = Invoke-Pester -Configuration $c

$r.FailedCount | Verify-Equal 0
$r.PassedCount | Verify-Equal 2
}
finally { Pop-Location }
}
finally { Remove-Item -Path $folder -Recurse -Force }
}

t "does not overwrite a Run.RepoRoot the user set" {
# A real directory. Resolving the chain runs the value through GetFullPath, and a made
# up path is not portable: on Windows something like 'TestDrive:whatever' reads as a
# drive qualifier and throws, while on Unix it is a legal relative file name.
$mine = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().Guid)
$null = New-Item -ItemType Directory -Path $mine -Force
try {
$c = [PesterConfiguration]::Default
$c.Run.RepoRoot = $mine
$c.Run.ScriptBlock = { Describe 'd' { It 'i' { 1 | Should -Be 1 } } }
$c.Run.PassThru = $true
$c.Output.Verbosity = 'None'
$r = Invoke-Pester -Configuration $c

$r.FailedCount | Verify-Equal 0
$r.Configuration.Run.RepoRoot.Value | Verify-Equal $mine
}
finally { Remove-Item -Path $mine -Recurse -Force }
}

t "FindRepoRoot returns the directory it started from when there is no .git above it" {
# The walk stops at the filesystem root and falls back to where it started, rather than
# returning null or the drive root, so RepoRoot is always a usable directory.
$start = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().Guid)
$null = New-Item -ItemType Directory -Path $start -Force
try {
# GetFullPath, because the temp path is a symlink on macOS and the walk normalizes.
$expected = [System.IO.Path]::GetFullPath($start)
[Pester.RunConfiguration]::FindRepoRoot($expected) | Verify-Equal $expected
}
finally { Remove-Item -Path $start -Recurse -Force }
}

t "applies the same cascade inside each parallel worker" {
$folder = New-CascadingBeforeContainerFolder
try {
Expand Down
Loading