Enhance module compatibility: support nested module resolution slash notation#2700
Open
wetwicky wants to merge 1 commit intopester:mainfrom
Open
Enhance module compatibility: support nested module resolution slash notation#2700wetwicky wants to merge 1 commit intopester:mainfrom
wetwicky wants to merge 1 commit intopester:mainfrom
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Summary
Enable users to specify a nested module unambiguously in
-ModuleNameparameters across all Pester mocking and scoping commands (Mock,InModuleScope,Should-Invoke,Should-NotInvoke) using rooted nested-module notation ('RootModuleName/NestedModuleName'and'RootModuleName\NestedModuleName'). When this notation is detected, resolve the root module first, then locate the nested module within it and return its session state. Plain module names (without slash notation) must continue to work without any change in behavior.PR Checklist
Create Pull Requestto mark it as a draft. PR can be markedReady for reviewwhen it's ready.Domain Concept Identification
Existing Concepts (from codebase)
Mock,Should-Invoke,Should-NotInvoke, andInModuleScopethat identifies the target PowerShell module into which a mock is injected or within which test code is executed. Currently expects a simple module name (e.g.,"Pester","MyModule").src/functions/InModuleScope.ps1) that resolves a module name string to a loadedPSModuleInfoobject viaGet-Module -Name $ModuleName -All. Used by bothInModuleScopeandResolve-Command(inMock.ps1). It is the single gateway for module resolution throughout the mocking and scoping system.src/functions/Mock.ps1) that resolves the command to be mocked within a module's session state. Delegates module lookup entirely toGet-CompatibleModule.MockBehaviorand context info objects, used as part of the mock table key ("$ModuleName||$CommandName") to track call history per module scope.psd1) where a root/manifest module loads subordinate script modules. The nested modules share the root's session state when loaded via a manifest. Pester already has a test (tst/functions/InModuleScope.Tests.ps1, line ~325) demonstratingInModuleScopeworking against manifest modules that have nested modules.Pester.psd1,RootModule = 'Pester.psm1'. In the PowerShell module system, a manifest'sRootModuleis the primary script module;NestedModuleslists additional modules loaded alongside it.New Concepts Required
RootModule/NestedModulestring format (e.g.,"Pester/Pester.Runtime") to be parsed byGet-CompatibleModule(and propagated throughResolve-Command) that uniquely identifies a nested module within a specific root module. This concept does not yet exist anywhere in the Pester codebase.RootModule/NestedModule, first finds the root module, then narrows the loadedNestedModuleslist to find the specific nested module, returning itsPSModuleInfo— enabling disambiguation when the same nested module name appears under different root modules.Key Business Rules
ModuleNamecontains exactly one/, the left side is the root/manifest module name and the right side is the nested module name. A plain name (no/) retains current behavior — no breaking change.ScriptorManifest; otherwise error with a clear message.NestedModuleslist or otherwise be a module that was loaded as a consequence of importing the root; otherwise error.TargetModulestored onMockBehaviormust remain a plain module name (or the full slash path — TBD, see Key Design Decisions), so that internal comparisons such as$ModuleName -eq $command.Mock.Hook.OriginalCommand.Module.Nameremain correct.Strategic Approach
Solution Direction
ModuleNamefor theRootModule/NestedModuleseparator inGet-CompatibleModule(the single module-resolution gateway). When detected, split on the first/, resolve the root module, then look up the nested module within it. Return the nested module'sPSModuleInfo. All callers ofGet-CompatibleModule—InModuleScopeandResolve-Command— automatically gain the capability.InModuleScopeand external-API functions (Mock,Should-Invoke,Should-NotInvoke) accept[string] $ModuleNameunchanged; no parameter type changes are needed.Key Design Decisions
Where to parse the slash notation — Options: (a) in
Get-CompatibleModuleonly, (b) at each call site. → Recommendation: Option (a).Get-CompatibleModuleis the sole resolution gateway; centralising the logic there avoids duplication and ensuresInModuleScope,Mock, and assert commands all behave consistently without touching call sites.What to store as TargetModule on MockBehavior — Options: (a) store the plain nested module name, (b) store the full
RootModule/NestedModulepath. The stored value is used in the mock call-history key ("$ModuleName||$CommandName") and in comparisons like$ModuleName -eq $command.Mock.Hook.OriginalCommand.Module.Name. PowerShell'sModule.Nameis always a plain name. → Recommendation: Store the resolved nested module's plainName(e.g.,"Pester.Runtime"), not the slash path. This keeps internal comparisons correct.Resolve-Commandalready setsTargetModule = $ModuleNameafter callingGet-CompatibleModule; the resolved plain name must be propagated back to the callers, meaningResolve-Commandshould capture$module.Name(the resolved nested module name) and use that asTargetModule.Nested module lookup strategy — Options: (a) iterate
$rootModule.NestedModules, (b) callGet-Module -Name $nestedModuleNameand filter to those where theRootModule*parent is the resolved root, (c) useGet-Module -Alland filter by checking$_.Pathstarts with the root module's directory. → Recommendation: Option (a) — directly enumerate$rootModule.NestedModulesand match by name. This is most precise and does not depend on path assumptions. It also mirrors how PowerShell itself tracks nested module relationships.Alternatives Considered
-NestedModuleNameparameter toMock,InModuleScope, etc.: Rejected because it requires changes to all public API surfaces, breaks existing scripts that splat parameters, increases complexity for consumers, and creates two mechanisms for the same concept.Get-Module -Name $nestedNameand disambiguate by checking parent: Partially viable but fragile — PowerShell's module graph doesn't always expose a clear parent link from a nested module back to its manifest. Direct enumeration of$rootModule.NestedModulesis more reliable.Risk & Gap Analysis
Requirement Ambiguities
/— is it always the manifest module name, or can it also be any parent psm1 that imported nested modules explicitly? Modules imported viaImport-Modulewith-NestedModulesbehave differently from manifest-defined nesting. Needs clarification./, but this is not enforced by the module system. If a module is legitimately namedA/B, the parsing would break. The probability is extremely low, but the edge case should be documented.Edge Cases
"Pester/") or no left side ("/Runtime"): Parser must validate and throw a descriptive error rather than silently producing unexpected behavior.Get-Moduleerror.Get-Module -Name $rootNamereturns nothing. Error message should guide the user to import it.RootModulekey is a nested module:Pester.psd1setsRootModule = 'Pester.psm1'. When someone writesPester/Pester.psm1, the right side matches theRootModulevalue, not aNestedModulesentry — edge case that may need consideration.Technical Risks
"$ModuleName||$CommandName". If the caller passes"Pester/Pester.Runtime"toMockand the resolved TargetModule stored is"Pester.Runtime", butShould-Invokeis later called withModuleName = "Pester/Pester.Runtime", the lookup must also resolve and use the plain name.Resolve-Commandis called byShould-Invoketoo; as long as it always normalises to the plain nested name, the key matches. Risk is low if the resolution always normalises — but needs a test.$ModuleName -eq $command.Mock.Hook.OriginalCommand.Module.Namecomparison inResolve-Command(lines 735, 755): This comparesTargetModuletoOriginalCommand.Module.Name. IfTargetModuleis stored as the slash path rather than the plain name, these comparisons silently fail, breakingIsFromTargetModule. Medium risk — mitigated by the decision to store the plain name.EscapeSingleQuotedStringContent $ModuleNameused in the mock bootstrap script body (Mock.ps1line 170): The bootstrap script embedsModuleNamein a single-quoted string literal. The/character is safe in single-quoted strings, but if the slash notation is used as-is (rather than the resolved plain name), the embedded string literal would be'Pester/Pester.Runtime'— still valid PowerShell but would break theInvoke_Mockdispatch which usesModuleNameas a key. Low-medium risk — resolved by always normalising before embedding.Acceptance Criteria Coverage
Mock -ModuleName 'Root/Nested' Get-Item {}sets up a mock in the nested moduleGet-CompatibleModuleto parse and resolve;TargetModulemust be normalisedInModuleScope 'Root/Nested' { ... }executes in the nested module's session stateGet-CompatibleModulechange covers thisShould-Invoke -ModuleName 'Root/Nested' Get-Itemcorrectly finds call historyTargetModuleto plain nameModuleName(no/) continues to work unchangedGet-ModuleerrorA/B/C)