Skip to content

Apply Pester.BeforeContainer.ps1 from the repo root down to the test file - #2993

Merged
nohwnd merged 6 commits into
mainfrom
nohwnd-cascading-beforecontainer
Aug 25, 2026
Merged

Apply Pester.BeforeContainer.ps1 from the repo root down to the test file#2993
nohwnd merged 6 commits into
mainfrom
nohwnd-cascading-beforecontainer

Conversation

@nohwnd

@nohwnd nohwnd commented Aug 22, 2026

Copy link
Copy Markdown
Member

Only a single Pester.BeforeContainer.ps1 in Run.RepoRoot was used, so a repository with different setup for unit and integration tests had to cram both into that one file, or repeat them in every test file. Now every Pester.BeforeContainer.ps1 from Run.RepoRoot down to the test file's own folder is applied, outermost first. Requested by @johlju in #2772, where he estimated it removes around a thousand lines of duplication in SqlServerDsc.

reporoot/Pester.BeforeContainer.ps1                 <- applies to everything
reporoot/tests/Pester.BeforeContainer.ps1           <- applies to tests/ and below
reporoot/tests/unit/Pester.BeforeContainer.ps1      <- applies to tests/unit only

The setup files are dot-sourced into the container's own scope now, not into the run session state. That is what makes the folders actually scope anything. Before this, setup dot-sourced for one file stayed visible to every container after it, so a file in tests/integration would silently inherit whatever tests/unit had set up, and the result depended on run order.

A block can hold more than one of each setup and teardown as a result, because the folder setup and the file's own BeforeAll both register on the container's root block and have to compose rather than one of them erroring out.

  • Setup and teardown on Block are Pester.ScriptBlockCollection, a List<ScriptBlock> that renders as its contents. A single one prints exactly what a plain ScriptBlock printed before, several print with the [n] numbering already used for multiple errors.
  • Setups run in the order they were registered, teardowns in reverse.
  • A folder opts out of everything above it with #pester:no-inherit, matched on real comment tokens like #pester:no-parallel. Same meaning as root = true in an .editorconfig, for a folder like doc tests that needs its own cheap setup and should not pay for the expensive one.

Which files apply is a property of the directory, not of the container, so the run shares one cache keyed by directory. Resolving a folder walks up only as far as the first directory already in the cache and takes that list, and records a list for every directory it passes on the way back down. 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 folders and a 26 KB root setup file that is 23.6 ms instead of 845 ms.

Parallel resolves the chain once in the parent and hands each worker the paths. Workers are separate runspaces that cannot share a cache, so letting them resolve would mean rediscovering the same folders over and over, in parallel.

Breaking changes

  • Two BeforeAll (or AfterAll, BeforeEach, AfterEach) in one block no longer throw.
  • Top level code in Pester.BeforeContainer.ps1 runs during discovery only, so it has to be in BeforeAll to reach the tests. Same rule a test file already follows, and it is what keeps a folder's setup out of the next container.
  • Stray output from a setup file no longer warns, it cannot escape the container to reach Invoke-Test anymore. Split-RSpecResult still covers the filtering itself.

Tests

Added tests for the cascade sequentially and in parallel, #pester:no-inherit sequentially and in parallel, composition with the file's own BeforeAll, no leaking into a sibling folder, not walking above the repo root, and the ordering of multiple setups and teardowns. The four tests that asserted the old one-per-block errors are replaced by four asserting the new ordering.

Still open

  • Report the applied setup files on the container, so "where did this function come from" has an answer.

Resolving during the Find-FileInDirectory walk is off the list. That walk starts at the -Path roots while Run.RepoRoot is found separately by searching for .git, so with Invoke-Pester -Path ./tests/unit it never sees reporoot/Pester.BeforeContainer.ps1. The walk up stays, and with the cache the disk checks it does are under a millisecond for a whole run.

🤖

nohwnd added 3 commits August 22, 2026 14:02
…file

Until now only a single Pester.BeforeContainer.ps1 in Run.RepoRoot was used, so a
repository with different setup for unit and integration tests had to put both in
that one file, or repeat them in every test file. Now every
Pester.BeforeContainer.ps1 from Run.RepoRoot down to the test file's own folder is
applied, outermost first, so shared setup lives at the root and the parts only some
tests need live next to those tests. Requested by @johlju in #2772, where he
estimated it removes around a thousand lines of duplication in SqlServerDsc.

The setup files are dot-sourced into the container's own scope now, not into the
run session state, which is what makes the folders actually scope anything. Before
this, setup dot-sourced for one file stayed visible to every container after it, so
a file in tests/integration would silently inherit whatever tests/unit had set up
and the result depended on run order.

A block can hold more than one of each setup and teardown as a result. The folder
setup and the test file's own BeforeAll both register on the container's root block
and have to compose instead of one of them erroring out:

- Setup and teardown on Block are Pester.ScriptBlockCollection, a List<ScriptBlock>
  that renders as its contents, so a single one prints exactly what a plain
  ScriptBlock printed before and several print with the [n] numbering used for
  multiple errors.
- Setups run in the order they were registered, teardowns in reverse.
- A folder can opt out of everything above it with #pester:no-inherit, matched on
  real comment tokens like #pester:no-parallel. Same meaning as root = true in an
  .editorconfig. Useful for a folder like doc tests that needs its own cheap setup
  and should not pay for the expensive one.

Parallel resolves the chain once in the parent and hands each worker the paths.
Workers are separate runspaces that cannot share a cache, so letting them resolve
would mean rediscovering the same folders over and over, in parallel.

Breaking changes:

- Two BeforeAll (or AfterAll, BeforeEach, AfterEach) in one block no longer throw.
- Top level code in Pester.BeforeContainer.ps1 runs during discovery only, so it has
  to be in BeforeAll to reach the tests. Same rule a test file already follows, and
  it is what keeps a folder's setup out of the next container.
- Stray output from a setup file no longer warns, it cannot escape the container to
  reach Invoke-Test anymore. Split-RSpecResult still covers the filtering itself.

Note: resolving during the Find-FileInDirectory walk instead of walking up per
folder, and reporting the applied setup files on the container, are still open.

🤖
…recontainer

# Conflicts:
#	tst/Pester.RSpec.Parallel.ts.ps1
@nohwnd

nohwnd commented Aug 24, 2026

Copy link
Copy Markdown
Member Author

Needs better strategy for the walk up so we only parse each BeforeContainer once, and don't duplicate IO.

Otherwise in summary, we made it behave like if you put BeforeDiscovery or BeforeAll on top of the file, to avoid inventing new execution modes, and also to avoid polluting the script scope.

Which Pester.BeforeContainer.ps1 files apply is a property of the directory, so
the cache belongs on the directory. It used to be keyed by the container's own
folder, which meant every distinct test folder walked and tokenized the whole
chain above it again. On a tree with 60 test folders and a 26 KB root setup file
that is 845 ms of resolving, and all of it sits in front of the run, before any
parallel worker starts.

Get-PesterBeforeContainerChain now takes the cache, walks up only as far as the
first directory that is already resolved, and records the list for every
directory it passes on the way back down. Each directory is checked on disk once
per run and each setup file is tokenized once per run. The same tree resolves in
23 ms.

#pester:no-inherit does not need anything special. The opt-out belongs to the
folder that carries it, so the truncated list is simply what gets cached for that
folder, and folders below inherit the shorter list without looking above it
again.

Added tests for the cache entry per directory, for the walk stopping at an entry
that is already there, and for the truncated chain being what gets cached under
a no-inherit folder.

🤖
Comment thread src/Pester.Runtime.ps1

$sb = {
param ($private:p, $private:d)
param ($private:p, $private:d, $private:setupFiles)
@nohwnd

nohwnd commented Aug 24, 2026

Copy link
Copy Markdown
Member Author

Done in 62770f2. The cache moved off the container's folder and onto the directory, which is where it belongs, since which setup files apply is a property of the directory and not of the container.

Get-PesterBeforeContainerChain now takes the cache, walks up only as far as the first directory that is already in it, takes that list, and records a list for every directory it passes on the way back down, not just for the folder it was asked about. So a sibling folder starts from tests, a deeper folder starts from tests/unit, each directory is checked on disk once per run and each setup file is tokenized once per run.

On a tree with 60 test folders and a 26 KB root setup file that is 845 ms before and 23.6 ms after. All of it sat in front of the run, before any parallel worker started.

#pester:no-inherit needed nothing special. The opt-out belongs to the folder that carries it, so the truncated list is simply what gets cached for that folder, and the folders below inherit the shorter list without looking above it again.

Added tests for the entry per directory, for the walk stopping at an entry that is already there, and for the truncated chain being what gets cached under a no-inherit folder.

I also dropped the other open item, resolving during the Find-FileInDirectory walk. That walk starts at the -Path roots and Run.RepoRoot is found separately by searching for .git, so with Invoke-Pester -Path ./tests/unit it never sees reporoot/Pester.BeforeContainer.ps1. The walk up has to stay, and after the caching the disk checks it does are under a millisecond for a whole run.

🤖

…recontainer

# Conflicts:
#	tst/Pester.RSpec.Parallel.ts.ps1
@nohwnd
nohwnd merged commit 5daec4d into main Aug 25, 2026
20 checks passed
@nohwnd
nohwnd deleted the nohwnd-cascading-beforecontainer branch August 25, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants