@@ -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,8 @@ function Invoke-MockInternal {
798800 switch ($FromBlock ) {
799801 Begin {
800802 $MockCallState [' InputObjects' ] = [System.Collections.Generic.List [object ]]@ ()
801- $MockCallState [' ShouldExecuteOriginalCommand' ] = $false
803+ $MockCallState [' MatchedNoBehavior' ] = $false
804+ $MockCallState [' NoBehaviors' ] = $false
802805 $MockCallState [' BeginBoundParameters' ] = $BoundParameters.Clone ()
803806 # argument list must not be null, if the bootstrap functions has no parameters
804807 # we get null and need to replace it with empty array to make the splatting work
@@ -815,8 +818,21 @@ function Invoke-MockInternal {
815818 # test caller scope here, but the scope from which the mock was called
816819 $SessionState = if ($CallerSessionState ) { $CallerSessionState } else { $Hook.SessionState }
817820
821+ # When this bootstrap function runs but no behaviors are visible (e.g. a nested
822+ # Invoke-Pester run inherits the outer mock's bootstrap function but not its
823+ # behaviors), there is nothing to throw about - fall through to the original
824+ # command so the unrelated nested call still works.
825+ if (0 -eq @ ($Behaviors ).Count) {
826+ $MockCallState [' NoBehaviors' ] = $true
827+ if ($null -ne $InputObject ) {
828+ $null = $MockCallState [' InputObjects' ].AddRange(@ ($InputObject ))
829+ }
830+
831+ return
832+ }
833+
818834 # 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
835+ $behavior , $failedFilterInvocations = FindMatchingBehavior - Behaviors @ ($Behaviors ) - BoundParameters $BoundParameters - ArgumentList @ ($ArgumentList ) - SessionState $SessionState - Hook $Hook
820836
821837 if ($null -ne $behavior ) {
822838 $call = @ {
@@ -841,7 +857,8 @@ function Invoke-MockInternal {
841857 return
842858 }
843859 else {
844- $MockCallState [' ShouldExecuteOriginalCommand' ] = $true
860+ $MockCallState [' MatchedNoBehavior' ] = $true
861+ $MockCallState [' FailedFilterInvocations' ] = $failedFilterInvocations
845862 if ($null -ne $InputObject ) {
846863 $null = $MockCallState [' InputObjects' ].AddRange(@ ($InputObject ))
847864 }
@@ -851,9 +868,12 @@ function Invoke-MockInternal {
851868 }
852869
853870 End {
854- if ($MockCallState [' ShouldExecuteOriginalCommand' ]) {
871+ if ($MockCallState [' NoBehaviors' ]) {
872+ # No behaviors are defined in the current scope at all - this means the
873+ # bootstrap function leaked from a parent scope (e.g. nested Invoke-Pester).
874+ # Invoke the original command transparently so the unrelated caller still works.
855875 if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
856- Write-PesterDebugMessage - Scope Mock " Invoking the original command."
876+ Write-PesterDebugMessage - Scope Mock " No behaviors are visible in this scope, invoking the original command."
857877 }
858878
859879 $MockCallState [' BeginBoundParameters' ] = Reset-ConflictingParameters - BoundParameters $MockCallState [' BeginBoundParameters' ]
@@ -871,49 +891,32 @@ function Invoke-MockInternal {
871891 }
872892 }
873893
874- $SessionState = if ($CallerSessionState ) {
875- $CallerSessionState
876- }
877- else {
878- $Hook.SessionState
879- }
880-
894+ $SessionState = if ($CallerSessionState ) { $CallerSessionState } else { $Hook.SessionState }
881895 Set-ScriptBlockScope - ScriptBlock $scriptBlock - SessionState $SessionState
882896
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-
904897 if ($null -eq ($MockCallState [' BeginArgumentList' ])) {
905898 $arguments = @ ()
906899 }
907900 else {
908901 $arguments = $MockCallState [' BeginArgumentList' ]
909902 }
910- if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
911- Write-ScriptBlockInvocationHint - Hint " Mock - Original Command" - ScriptBlock $scriptBlock
912- }
913903 & $scriptBlock - Command $Hook.OriginalCommand `
914904 - ArgumentList $arguments `
915905 - BoundParameters $MockCallState [' BeginBoundParameters' ] `
916906 - InputObjects $MockCallState [' InputObjects' ]
907+
908+ return
909+ }
910+
911+ if ($MockCallState [' MatchedNoBehavior' ]) {
912+ if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
913+ Write-PesterDebugMessage - Scope Mock " The mock did not match any filtered behavior, and there was no default behavior. Failing."
914+ }
915+
916+ $failedFilterInvocations = $MockCallState [' FailedFilterInvocations' ]
917+ $filterList = ($failedFilterInvocations | ForEach-Object { " $_ " }) -join [System.Environment ]::NewLine
918+
919+ 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 "
917920 }
918921 }
919922 }
@@ -980,6 +983,7 @@ function FindMatchingBehavior {
980983 Write-PesterDebugMessage - Scope Mock " Finding behavior to use, one that passes filter or a default:"
981984 }
982985
986+ $failedFilterInvocations = [System.Collections.Generic.List [String ]]@ ()
983987 $foundDefaultBehavior = $false
984988 $defaultBehavior = $null
985989 foreach ($b in $Behaviors ) {
@@ -999,11 +1003,17 @@ function FindMatchingBehavior {
9991003 SessionState = $Hook.CallerSessionState
10001004 }
10011005
1002- if (Test-ParameterFilter @params ) {
1006+ $filterResult = Test-ParameterFilter @params
1007+ $passed = $filterResult [0 ]
1008+ $filterInvocations = $filterResult [1 ]
1009+ if ($passed ) {
10031010 if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
10041011 Write-PesterDebugMessage - Scope Mock " { $ ( $b.ScriptBlock ) } passed parameter filter and will be used for the mock call."
10051012 }
1006- return $b
1013+ return $b , $null
1014+ }
1015+ else {
1016+ $failedFilterInvocations.AddRange ($filterInvocations )
10071017 }
10081018 }
10091019 }
@@ -1012,13 +1022,13 @@ function FindMatchingBehavior {
10121022 if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
10131023 Write-PesterDebugMessage - Scope Mock " { $ ( $defaultBehavior.ScriptBlock ) } is a default behavior and will be used for the mock call."
10141024 }
1015- return $defaultBehavior
1025+ return $defaultBehavior , $null
10161026 }
10171027
10181028 if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
1019- Write-PesterDebugMessage - Scope Mock " No parametrized or default behaviors were found filter ."
1029+ Write-PesterDebugMessage - Scope Mock " No parametrized or default behaviors were found."
10201030 }
1021- return $null
1031+ return $null , $failedFilterInvocations
10221032}
10231033
10241034function LastThat {
@@ -1241,8 +1251,11 @@ function Test-ParameterFilter {
12411251 else { $null }
12421252 }
12431253
1254+ $parameterFilterInvocations = [Collections.Generic.List [string ]]@ ()
1255+
12441256 $result = & $wrapper $parameters
1245- if ($result ) {
1257+ $passed = [bool ]$result
1258+ if ($passed ) {
12461259 if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
12471260 Write-PesterDebugMessage - Scope Mock - Message " Mock filter returned value '$result ', which is truthy. Filter passed."
12481261 }
@@ -1251,8 +1264,21 @@ function Test-ParameterFilter {
12511264 if ($PesterPreference.Debug.WriteDebugMessages.Value ) {
12521265 Write-PesterDebugMessage - Scope Mock - Message " Mock filter returned value '$result ', which is falsy. Filter did not pass."
12531266 }
1267+
1268+ # Filter did not pass, serialize the values and store them for future reference in case we don't find any behavior.
1269+ $filterText = $scriptBlock.ToString ().Trim()
1270+ $hasContext = 0 -lt $Context.Count
1271+ $contextText = if ($hasContext ) {
1272+ ' bound parameters: ' + (($Context.GetEnumerator () | ForEach-Object { " $ ( $_.Key ) = $ ( $_.Value ) " }) -join ' , ' )
1273+ }
1274+ else {
1275+ ' no bound parameters'
1276+ }
1277+ $filterCall = " { $filterText } $contextText "
1278+ $parameterFilterInvocations.Add ($filterCall )
12541279 }
1255- $result
1280+ # Return as a single 2-element array so multi-assignment works even when $result is empty/$null/array.
1281+ , @ ($passed , $parameterFilterInvocations )
12561282}
12571283
12581284function Get-ContextToDefine {
0 commit comments