Add Run.BeforeInvoke bootstrap for Invoke-Pester - #2772
Conversation
Introduce a Run.BeforeInvoke option that runs optional bootstrap code in the caller's scope as soon as Invoke-Pester starts, before the caller's $PesterPreference is read and before discovery. Use it to import dependencies and to provide configuration by defining or modifying $PesterPreference, which Pester then picks up as the caller preference. Two sources, mirroring the container-level convention: - Run.BeforeInvoke config option (scriptblock[]). When set, it wins. - Convention file: the first Pester.BeforeInvoke.ps1 found when walking up from each Run.Path towards Run.RepoRoot is dot-sourced (deduped, in order, never escaping the repo root). Each scriptblock is bound to the caller's SessionState and dot-sourced so imports, defined functions and $PesterPreference land in the caller's scope. Bootstrap runs for top-level runs only, so nested Pester-in-Pester does not re-run it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
I really like this, looking forward to this merging. I will reduce a lot of duplicated code in test files by having a If it would go up to the repo root and find all and run them in sequence we could even reduce even more duplication, for example It would then invoke them in the order top to bottom:
But just having the option of one Pester.BeforeInvoke.ps1 would probably reduce the duplication with ~1000 rows in a larger project (like in SqlServerDsc). |
|
@johlju -rc1 is out, it has similar feature, BeforeContainer, my main problem is that we cannot just promise that it will run once, because if you need to run tests in parallel (there is experimental parallel mode), this script needs to run in every new runspace. And I wanted to minimize the overhead of looking the file up every time, and did not want to think about how to say "this is in child folder, but don't run any of the script files you find above me" like "root=true" in .editorconfig. but all those are good suggestions, show me how they simplify your workflow, and we could at least add them as options, if not as defult. I just did not want to over design and over promise in the initial release. |
|
It says in the release notes för rc1:
Assuming we can pass
A single file would not handle different setup unless it can pass current test script file so the code But I will check how far the current functionality takes us and report the GAP. If we can pass |
I actually don't know. I did not think about that, my thinking here was that it imports modules you need, adds helper functions etc. stuff that I typically do in a "test.ps1" script. But that now needs to happen in every child runspace. All in all the idea of what I want to achieve here is not super clear even to me. My goal is also that I can run tests (e.g. a single
You could probably do that through the power of powershell, e.g. by looking at your pscall stack, but I get what you mean. |
That is why there are extensive bootstrap in each file in SqlServerDsc - so there our goal is aligned 🙂 also I need to use Invoke-PesterJob in VS Code (and outside) for tests if they load classes so I do not need to kill the session each time we re-run test. |
|
So after a bit of testing
|
|
BeforeInvoke Folder-scoped
Now that BeforeAll/AfterAll Something to explore, but maybe not critical? While it wouldn't remove the boilerplate, you could deduplicate 99% by dot-sourcing, couldn't you? |
Yes, but having a standard it would be easier for contributors to understand where to look and what its for, regardless of repo. Dot-sourcing would probably work, but naming and location would most likely be different for each repo a contributor looks at. 🤔
I think so too. BeforeInvoke would be good for things that only need to run once for each |
…file (#2993) * Apply Pester.BeforeContainer.ps1 from the repo root down to the test 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. 🤖 * Fix braces lost when resolving the merge in the parallel tests 🤖 * Cache the setup file chain per directory, not per container folder 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. 🤖
Summary
Adds a
Run.BeforeInvokeoption that runs optional bootstrap code in the caller's scope, as soon asInvoke-Pesterstarts — before the caller's$PesterPreferenceis read and before discovery. Use it to import dependencies and to provide configuration by defining or modifying$PesterPreference, which Pester then picks up as the caller preference.This mirrors the container-level
BeforeContainerconvention, but for the top-level invocation itself.How it works
Two sources, in priority order:
Run.BeforeInvokeconfig option (scriptblock[]). When set, it wins and runs as-is.Pester.BeforeInvoke.ps1found when walking up from eachRun.PathtowardRun.RepoRootis dot-sourced (deduped, in order, never escaping the repo root).Each scriptblock is bound to the caller's
SessionStateand dot-sourced, so imported modules, defined functions, and$PesterPreferenceland in the caller's scope whereInvoke-Pesterreads them next. The bootstrap runs for top-level runs only, so nested Pester-in-Pester does not re-run it.Changes
BeforeInvokeScriptBlockArrayOptiononRunConfiguration(mirrorsRepoRoot/ScriptBlock).src/functions/Pester.BeforeInvoke.ps1—Resolve-PesterBeforeInvoke+Invoke-PesterBeforeInvoke.Main.ps1— resolves a preliminary preference and runs the bootstrap before the caller-preference read.BeforeInvokeentry inabout_PesterConfiguration.tst/Pester.BeforeInvoke.ts.ps1(9 P-tests). Full suite green.Open design question (reason this is a draft)
trueBeforeInvokecurrently runs once, in the orchestratingInvoke-Pestercall. For a parallel run, test containers execute in separate runspaces that would not have re-run the bootstrap — so dependency setup done here would be missing in the workers. Configuration ($PesterPreference) is inherently a once-per-invocation concern and is fine, but dependency provisioning may need to also happen per-container (e.g. viaBeforeContainer) for parallel scenarios. Want to settle this before merging.