Skip to content

Commit 29d1756

Browse files
committed
Mock fallthrough
1 parent d0be71a commit 29d1756

3 files changed

Lines changed: 86 additions & 287 deletions

File tree

src/functions/Mock.ps1

Lines changed: 72 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -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

10241034
function 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

12581284
function Get-ContextToDefine {

src/functions/Pester.SessionState.Mock.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,9 +1001,9 @@ function Invoke-Mock {
10011001
)
10021002

10031003
if ('End' -eq $FromBlock) {
1004-
if (-not $MockCallState.ShouldExecuteOriginalCommand) {
1004+
if (-not $MockCallState.MatchedNoBehavior) {
10051005
if ($PesterPreference.Debug.WriteDebugMessages.Value) {
1006-
Write-PesterDebugMessage -Scope MockCore "Mock for $CommandName was invoked from block $FromBlock, and should not execute the original command, returning."
1006+
Write-PesterDebugMessage -Scope MockCore "Mock for $CommandName was invoked from block $FromBlock, and matched at least one behavior, returning."
10071007
}
10081008
return
10091009
}

0 commit comments

Comments
 (0)