Skip to content

Commit 2b0ddf6

Browse files
committed
Mock fallthrough
1 parent d0be71a commit 2b0ddf6

3 files changed

Lines changed: 68 additions & 313 deletions

File tree

src/functions/Mock.ps1

Lines changed: 48 additions & 70 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,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

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

12581236
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)