tests/Help.tests.ps1 silently degrades from "test help for this module's commands" to "test
help for every command installed on the machine" whenever $env:BHProjectName is not set.
The defect
tests/Help.tests.ps1:22-28:
$params = @{
Module = (Get-Module $env:BHProjectName)
CommandType = [System.Management.Automation.CommandTypes[]]'Cmdlet, Function' # Not alias
}
if ($PSVersionTable.PSVersion.Major -lt 6) {
$params.CommandType[0] += 'Workflow'
}
$commands = Get-Command @params
With $env:BHProjectName empty, Get-Module writes a non-terminating
ParameterArgumentValidationError, $params.Module becomes $null, and Get-Command -Module $null
applies no filter at all. Reproduced on a developer machine:
BHProjectName = ''
Get-Module : Cannot validate argument on parameter 'Name'. The argument is null or empty.
Get-Module $env:BHProjectName -> count=1 null=True
Get-Command @params -> 4780 commands
Describe "Test help for <_.Name>" -ForEach $commands then generates a Describe per command, and
lines 43 and 55 each call Get-Help $command.Name — two lookups per command, across 4,780
commands.
Why it is slow rather than merely wrong
Get-Help falls back to a machine-wide search whenever the name does not resolve exactly.
HelpSystem.GetHelp tries ExactMatchHelp first and calls SearchHelp when that finds nothing;
CommandHelpProvider.SearchHelp then decorates the name as *name* and walks every installed
module through ModuleUtils.GetMatchingCommands, emitting the progress record
Searching Help for {0} .... Microsoft documents the behavior:
When using Get-Help to locate commands, it initially performs a wildcard search for command
names based on your input. If that doesn't find any matches, it conducts a comprehensive
full-text search across all PowerShell help articles on your system.
Crucially the {0} is the name that was passed, not a command being scanned past — so
progress reading Searching Help for Add-ClusteriSCSITargetServerRole means the suite really did
call Get-Help Add-ClusteriSCSITargetServerRole.
Measured on this machine:
| lookup |
cost |
| resolvable name |
2-4 ms |
| unresolvable name |
~2.6-3.5 s, not cached (3 identical calls: 5025 / 3415 / 3299 ms) |
Why Windows PowerShell 5.1 is hit far harder
Not because Get-Help is slower there — the scan is actually slower on 7. It is the number of
names that cannot resolve:
PS 5.1.26100.9168: enumerated=3987 unresolvable=653
PS 7.6.5: enumerated=3929 unresolvable=83
Modules visible to module analysis but unimportable under 5.1 — CompatiblePSEditions = 'Core',
PS7-only, or dependent on an absent OS feature — miss exact match permanently and pay the full
scan on every call. 653 x 2 calls x ~3 s is roughly 65 minutes; an observed run took 39.
Detection
A full local run of the suite under Windows PowerShell 5.1 takes tens of minutes and floods the
console with Searching Help for ... progress for commands that have nothing to do with this
repository. Under psake the environment variables are set by BuildHelpers, so CI never sees it —
the 5.1 leg runs the same files in about 161 seconds. It only bites when Pester is invoked
directly, which is exactly what a developer does when iterating on one test file.
Fix
Fail loudly instead of degrading. The module failing to resolve must not silently become "no
filter":
-ErrorAction Stop on the Get-Module call, or resolve the module explicitly and throw when it
is absent, and
- assert
$commands.Count is sane — for example that it matches the manifest's
FunctionsToExport — so this can never again pass with the wrong 4,780 commands.
Guarding each lookup with Get-Command before Get-Help is worthwhile defence in depth (56 ms
versus 2596 ms for an unresolvable name), but on its own it would only reduce the run to a few
minutes of testing the wrong commands. The real defect is the silent widening.
Not the fix: setting $ProgressPreference = 'SilentlyContinue'. Measured, it buys about 5%
(2655/2665/2678 ms versus 2754/3105/2858 ms) — it silences the console and leaves the scan intact.
Notes
- Pre-existing, and inherited from the widely copied
juneb/PesterTDD Module.Help.Tests.ps1,
so other repositories using that pattern are likely affected too. No public issue appears to
describe this.
- dbatools independently hardened the same pattern by filtering the command set explicitly, and
its file comments record an adjacent discovery-versus-runtime bug that produced zero tests for
years.
Done when
Running Invoke-Pester -Path ./tests directly, without the psake build having set the BuildHelpers
environment variables, either tests only this module's exported commands or fails with a clear
message — and never enumerates the whole machine.
tests/Help.tests.ps1silently degrades from "test help for this module's commands" to "testhelp for every command installed on the machine" whenever
$env:BHProjectNameis not set.The defect
tests/Help.tests.ps1:22-28:With
$env:BHProjectNameempty,Get-Modulewrites a non-terminatingParameterArgumentValidationError,$params.Modulebecomes$null, andGet-Command -Module $nullapplies no filter at all. Reproduced on a developer machine:
Describe "Test help for <_.Name>" -ForEach $commandsthen generates a Describe per command, andlines 43 and 55 each call
Get-Help $command.Name— two lookups per command, across 4,780commands.
Why it is slow rather than merely wrong
Get-Helpfalls back to a machine-wide search whenever the name does not resolve exactly.HelpSystem.GetHelptriesExactMatchHelpfirst and callsSearchHelpwhen that finds nothing;CommandHelpProvider.SearchHelpthen decorates the name as*name*and walks every installedmodule through
ModuleUtils.GetMatchingCommands, emitting the progress recordSearching Help for {0} .... Microsoft documents the behavior:Crucially the
{0}is the name that was passed, not a command being scanned past — soprogress reading
Searching Help for Add-ClusteriSCSITargetServerRolemeans the suite really didcall
Get-Help Add-ClusteriSCSITargetServerRole.Measured on this machine:
Why Windows PowerShell 5.1 is hit far harder
Not because
Get-Helpis slower there — the scan is actually slower on 7. It is the number ofnames that cannot resolve:
Modules visible to module analysis but unimportable under 5.1 —
CompatiblePSEditions = 'Core',PS7-only, or dependent on an absent OS feature — miss exact match permanently and pay the full
scan on every call. 653 x 2 calls x ~3 s is roughly 65 minutes; an observed run took 39.
Detection
A full local run of the suite under Windows PowerShell 5.1 takes tens of minutes and floods the
console with
Searching Help for ...progress for commands that have nothing to do with thisrepository. Under psake the environment variables are set by BuildHelpers, so CI never sees it —
the 5.1 leg runs the same files in about 161 seconds. It only bites when Pester is invoked
directly, which is exactly what a developer does when iterating on one test file.
Fix
Fail loudly instead of degrading. The module failing to resolve must not silently become "no
filter":
-ErrorAction Stopon theGet-Modulecall, or resolve the module explicitly and throw when itis absent, and
$commands.Countis sane — for example that it matches the manifest'sFunctionsToExport— so this can never again pass with the wrong 4,780 commands.Guarding each lookup with
Get-CommandbeforeGet-Helpis worthwhile defence in depth (56 msversus 2596 ms for an unresolvable name), but on its own it would only reduce the run to a few
minutes of testing the wrong commands. The real defect is the silent widening.
Not the fix: setting
$ProgressPreference = 'SilentlyContinue'. Measured, it buys about 5%(2655/2665/2678 ms versus 2754/3105/2858 ms) — it silences the console and leaves the scan intact.
Notes
juneb/PesterTDD
Module.Help.Tests.ps1,so other repositories using that pattern are likely affected too. No public issue appears to
describe this.
its file comments record an adjacent discovery-versus-runtime bug that produced zero tests for
years.
Done when
Running
Invoke-Pester -Path ./testsdirectly, without the psake build having set the BuildHelpersenvironment variables, either tests only this module's exported commands or fails with a clear
message — and never enumerates the whole machine.