@@ -348,7 +348,7 @@ function Should-InvokeVerifiableInternal {
348348 }
349349
350350 return [Pester.ShouldResult ] @ {
351- Succeeded = $true
351+ Succeeded = $true
352352 }
353353}
354354
@@ -460,7 +460,9 @@ function Should-InvokeInternal {
460460 # $params.ScriptBlock = New-BlockWithoutParameterAliases -Metadata $ContextInfo.Hook.Metadata -Block $params.ScriptBlock
461461 # }
462462
463- if (Test-ParameterFilter @params ) {
463+ $filterResult = Test-ParameterFilter @params
464+ $passed = $filterResult [0 ]
465+ if ($passed ) {
464466 $null = $matchingCalls.Add ($historyEntry )
465467 }
466468 else {
@@ -530,7 +532,7 @@ function Should-InvokeInternal {
530532 }
531533
532534 return [Pester.ShouldResult ] @ {
533- Succeeded = $true
535+ Succeeded = $true
534536 }
535537}
536538
@@ -798,7 +800,7 @@ function Invoke-MockInternal {
798800 switch ($FromBlock ) {
799801 Begin {
800802 $MockCallState [' InputObjects' ] = [System.Collections.Generic.List [object ]]@ ()
801- $MockCallState [' ShouldExecuteOriginalCommand ' ] = $false
803+ $MockCallState [' MatchedNoBehavior ' ] = $false
802804 $MockCallState [' BeginBoundParameters' ] = $BoundParameters.Clone ()
803805 # argument list must not be null, if the bootstrap functions has no parameters
804806 # we get null and need to replace it with empty array to make the splatting work
@@ -816,7 +818,7 @@ function Invoke-MockInternal {
816818 $SessionState = if ($CallerSessionState ) { $CallerSessionState } else { $Hook.SessionState }
817819
818820 # the @() are needed for powerShell3 otherwise it throws CheckAutomationNullInCommandArgumentArray (unless there is any breakpoint defined anywhere, then it works just fine :DDD)
819- $behavior = FindMatchingBehavior - Behaviors @ ($Behaviors ) - BoundParameters $BoundParameters - ArgumentList @ ($ArgumentList ) - SessionState $SessionState - Hook $Hook
821+ $behavior , $failedFilterInvocations = FindMatchingBehavior - Behaviors @ ($Behaviors ) - BoundParameters $BoundParameters - ArgumentList @ ($ArgumentList ) - SessionState $SessionState - Hook $Hook
820822
821823 if ($null -ne $behavior ) {
822824 $call = @ {
@@ -841,7 +843,8 @@ function Invoke-MockInternal {
841843 return
842844 }
843845 else {
844- $MockCallState [' ShouldExecuteOriginalCommand' ] = $true
846+ $MockCallState [' MatchedNoBehavior' ] = $true
847+ $MockCallState [' FailedFilterInvocations' ] = $failedFilterInvocations
845848 if ($null -ne $InputObject ) {
846849 $null = $MockCallState [' InputObjects' ].AddRange(@ ($InputObject ))
847850 }
@@ -851,69 +854,21 @@ function Invoke-MockInternal {
851854 }
852855
853856 End {
854- if ($MockCallState [' ShouldExecuteOriginalCommand ' ]) {
857+ if ($MockCallState [' MatchedNoBehavior ' ]) {
855858 if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
856- Write-PesterDebugMessage - Scope Mock " Invoking the original command ."
859+ Write-PesterDebugMessage - Scope Mock " The mock did not match any filtered behavior, and there was no default behavior. Failing ."
857860 }
858861
859- $MockCallState [' BeginBoundParameters' ] = Reset-ConflictingParameters - BoundParameters $MockCallState [' BeginBoundParameters' ]
860-
861- if ($MockCallState [' InputObjects' ].Count -gt 0 ) {
862- $scriptBlock = {
863- param ($Command , $ArgumentList , $BoundParameters , $InputObjects )
864- $InputObjects | & $Command @ArgumentList @BoundParameters
865- }
866- }
867- else {
868- $scriptBlock = {
869- param ($Command , $ArgumentList , $BoundParameters , $InputObjects )
870- & $Command @ArgumentList @BoundParameters
871- }
872- }
873-
874- $SessionState = if ($CallerSessionState ) {
875- $CallerSessionState
876- }
877- else {
878- $Hook.SessionState
862+ $failedFilterInvocations = $MockCallState [' FailedFilterInvocations' ]
863+ if ($null -eq $failedFilterInvocations -or $failedFilterInvocations.Count -eq 0 ) {
864+ # No behaviors in this scope, but the bootstrap function is installed —
865+ # an outer Mock leaked into a nested Invoke-Pester run.
866+ throw " No mock for command '$ ( $Hook.CommandName ) ' is defined in this scope, but the bootstrap is active (typically a Mock from an outer scope leaked into a nested Invoke-Pester run). Add a Mock for '$ ( $Hook.CommandName ) ' in this scope, or restructure the test so the outer Mock does not leak."
879867 }
880868
881- Set-ScriptBlockScope - ScriptBlock $scriptBlock - SessionState $SessionState
869+ $filterList = ( $failedFilterInvocations | & $SafeCommands [ ' ForEach-Object ' ] { " $_ " }) -join [ System.Environment ]::NewLine
882870
883- # In order to mock Set-Variable correctly we need to write the variable
884- # two scopes above
885- if (" Set-Variable" -eq $Hook.OriginalCommand.Name ) {
886- if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
887- Write-PesterDebugMessage - Scope Mock " Original command is Set-Variable, patching the call."
888- }
889- if ($MockCallState [' BeginBoundParameters' ].Keys -notcontains " Scope" ) {
890- $MockCallState [' BeginBoundParameters' ].Add( " Scope" , 2 )
891- }
892- # local is the same as scope 0, in that case we also write to scope 2
893- elseif (" Local" , " 0" -contains $MockCallState [' BeginBoundParameters' ].Scope) {
894- $MockCallState [' BeginBoundParameters' ].Scope = 2
895- }
896- elseif ($MockCallState [' BeginBoundParameters' ].Scope -match " \d+" ) {
897- $MockCallState [' BeginBoundParameters' ].Scope = 2 + $matches [0 ]
898- }
899- else {
900- # not sure what the user did, but we won't change it
901- }
902- }
903-
904- if ($null -eq ($MockCallState [' BeginArgumentList' ])) {
905- $arguments = @ ()
906- }
907- else {
908- $arguments = $MockCallState [' BeginArgumentList' ]
909- }
910- if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
911- Write-ScriptBlockInvocationHint - Hint " Mock - Original Command" - ScriptBlock $scriptBlock
912- }
913- & $scriptBlock - Command $Hook.OriginalCommand `
914- - ArgumentList $arguments `
915- - BoundParameters $MockCallState [' BeginBoundParameters' ] `
916- - InputObjects $MockCallState [' InputObjects' ]
871+ throw " No mock for command '$ ( $Hook.CommandName ) ' matched the call: none of the parameter filters matched, and there is no default mock to fall back to. Add a default mock (e.g. `` Mock $ ( $Hook.CommandName ) { ... }`` ) or adjust an existing -ParameterFilter.$ ( [System.Environment ]::NewLine) $ ( [System.Environment ]::NewLine) The following parameter filters were evaluated and did not match:$ ( [System.Environment ]::NewLine) $filterList "
917872 }
918873 }
919874 }
@@ -980,6 +935,7 @@ function FindMatchingBehavior {
980935 Write-PesterDebugMessage - Scope Mock " Finding behavior to use, one that passes filter or a default:"
981936 }
982937
938+ $failedFilterInvocations = [System.Collections.Generic.List [String ]]@ ()
983939 $foundDefaultBehavior = $false
984940 $defaultBehavior = $null
985941 foreach ($b in $Behaviors ) {
@@ -999,11 +955,17 @@ function FindMatchingBehavior {
999955 SessionState = $Hook.CallerSessionState
1000956 }
1001957
1002- if (Test-ParameterFilter @params ) {
958+ $filterResult = Test-ParameterFilter @params
959+ $passed = $filterResult [0 ]
960+ $filterInvocations = $filterResult [1 ]
961+ if ($passed ) {
1003962 if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
1004963 Write-PesterDebugMessage - Scope Mock " { $ ( $b.ScriptBlock ) } passed parameter filter and will be used for the mock call."
1005964 }
1006- return $b
965+ return $b , $null
966+ }
967+ else {
968+ $failedFilterInvocations.AddRange ($filterInvocations )
1007969 }
1008970 }
1009971 }
@@ -1012,13 +974,13 @@ function FindMatchingBehavior {
1012974 if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
1013975 Write-PesterDebugMessage - Scope Mock " { $ ( $defaultBehavior.ScriptBlock ) } is a default behavior and will be used for the mock call."
1014976 }
1015- return $defaultBehavior
977+ return $defaultBehavior , $null
1016978 }
1017979
1018980 if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
1019- Write-PesterDebugMessage - Scope Mock " No parametrized or default behaviors were found filter ."
981+ Write-PesterDebugMessage - Scope Mock " No parametrized or default behaviors were found."
1020982 }
1021- return $null
983+ return $null , $failedFilterInvocations
1022984}
1023985
1024986function LastThat {
@@ -1241,8 +1203,11 @@ function Test-ParameterFilter {
12411203 else { $null }
12421204 }
12431205
1206+ $parameterFilterInvocations = [Collections.Generic.List [string ]]@ ()
1207+
12441208 $result = & $wrapper $parameters
1245- if ($result ) {
1209+ $passed = [bool ]$result
1210+ if ($passed ) {
12461211 if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
12471212 Write-PesterDebugMessage - Scope Mock - Message " Mock filter returned value '$result ', which is truthy. Filter passed."
12481213 }
@@ -1251,8 +1216,21 @@ function Test-ParameterFilter {
12511216 if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
12521217 Write-PesterDebugMessage - Scope Mock - Message " Mock filter returned value '$result ', which is falsy. Filter did not pass."
12531218 }
1219+
1220+ # Filter did not pass, serialize the values and store them for future reference in case we don't find any behavior.
1221+ $filterText = $scriptBlock.ToString ().Trim()
1222+ $hasContext = 0 -lt $Context.Count
1223+ $contextText = if ($hasContext ) {
1224+ ' bound parameters: ' + (($Context.GetEnumerator () | & $SafeCommands [' ForEach-Object' ] { " $ ( $_.Key ) = $ ( $_.Value ) " }) -join ' , ' )
1225+ }
1226+ else {
1227+ ' no bound parameters'
1228+ }
1229+ $filterCall = " { $filterText } $contextText "
1230+ $parameterFilterInvocations.Add ($filterCall )
12541231 }
1255- $result
1232+ # Return as a single 2-element array so multi-assignment works even when $result is empty/$null/array.
1233+ , @ ($passed , $parameterFilterInvocations )
12561234}
12571235
12581236function Get-ContextToDefine {
0 commit comments