Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/functions/Mock.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ function Create-MockHook ($contextInfo, $InvokeMockCallback) {
$metadata = Repair-ConflictingParameters -Metadata $metadata -RemoveParameterType $RemoveParameterType -RemoveParameterValidation $RemoveParameterValidation
$paramBlock = [Management.Automation.ProxyCommand]::GetParamBlock($metadata)
$paramBlock = Repair-EnumParameters -ParamBlock $paramBlock -Metadata $metadata
$paramBlock = Repair-OrderedType -ParamBlock $paramBlock -Metadata $metadata

# Repair-ConflictingParameters above strips validation from the static parameters, but it skips
# dynamic parameters because they are not part of the static param block. To make
Expand Down Expand Up @@ -2198,6 +2199,56 @@ function Repair-EnumParameters {
$sb.ToString()
}

function Repair-OrderedType {
param (
[string]
$ParamBlock,
[System.Management.Automation.CommandMetadata]
$Metadata
)

# ProxyCommand.GetParamBlock serializes [System.Collections.Specialized.OrderedDictionary]
# parameters using the [ordered] type accelerator on PowerShell 7+ (Windows PowerShell 5.1 emits
# the full type name). That accelerator is only valid as a cast on a hash literal ([ordered]@{}),
# not as a parameter type constraint, so [scriptblock]::Create would throw a ParseException before
# the mock is ever invoked. Replace the accelerator with the full type name for each affected
# parameter. https://github.com/pester/Pester/issues/2370
if ($ParamBlock -notmatch '\[ordered(?:\[\])?\]') {
# No ordered accelerator present (e.g. Windows PowerShell). Return original string.
return $ParamBlock
}

$orderedType = [System.Collections.Specialized.OrderedDictionary]

foreach ($param in $Metadata.Parameters.Values) {
$type = $param.ParameterType
$isArray = $type.IsArray
$elementType = if ($isArray) { $type.GetElementType() } else { $type }
if ($elementType -ne $orderedType) { continue }

$accelerator = if ($isArray) { '[ordered[]]' } else { '[ordered]' }
$fullType = if ($isArray) { '[System.Collections.Specialized.OrderedDictionary[]]' } else { '[System.Collections.Specialized.OrderedDictionary]' }
$paramName = $param.Name

# Only rewrite the accelerator that is the type constraint for this specific parameter,
# i.e. the token immediately preceding ${ParameterName}. This avoids touching any legitimate
# [ordered]@{} cast that might appear elsewhere.
$pattern = "$([regex]::Escape($accelerator))(?<ws>\s*)$([regex]::Escape('${' + $paramName + '}'))"
$evaluator = {
param($match)
"$fullType$($match.Groups['ws'].Value)`${$paramName}"
}.GetNewClosure()

if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Mock -Message "Fixed OrderedDictionary type accelerator for parameter '$paramName' from '$accelerator' to '$fullType'"
}

$ParamBlock = [regex]::Replace($ParamBlock, $pattern, $evaluator)
}

$ParamBlock
}

function Format-MockCallHistoryMessage ($callHistory, $matchingCalls, $nonMatchingCalls) {
if ($null -eq $callHistory -or $callHistory.Count -eq 0) {
return "Performed invocations:`n <none>"
Expand Down
37 changes: 37 additions & 0 deletions tst/functions/Mock.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2967,6 +2967,43 @@ Describe 'Mocking command with ValidateRange-attributes' {
}
}

Describe 'Mocking command with OrderedDictionary-parameters' {
# https://github.com/pester/Pester/issues/2370
# Bug in PowerShell. ProxyCommand-generation serializes [System.Collections.Specialized.OrderedDictionary]
# parameters using the [ordered] type accelerator on PowerShell 7+, which is invalid as a parameter type
# constraint and makes the mock bootstrap function fail to compile. Needs Repair-OrderedType.

It 'mocked function does not throw when param is <Name>' -TestCases @(
@{
Name = 'a scalar OrderedDictionary'
Parameter = '[System.Collections.Specialized.OrderedDictionary]$Context'
},
@{
Name = 'an OrderedDictionary with other params'
Parameter = '[string]$Name, [System.Collections.Specialized.OrderedDictionary]$Context, [int]$Count'
},
@{
Name = 'an OrderedDictionary array'
Parameter = '[System.Collections.Specialized.OrderedDictionary[]]$Contexts'
}
) {
Set-Item -Path 'function:Test-OrderedParameter' -Value ('param ( {0} )' -f $Parameter)

Mock -CommandName 'Test-OrderedParameter' -MockWith { 'mock' }
Test-OrderedParameter | Should -Be 'mock'
}

It 'mocked function is invoked and captures the OrderedDictionary argument' {
function Get-OrderedThing { param([System.Collections.Specialized.OrderedDictionary]$Context) 'real' }
function Invoke-OrderedWrapper { Get-OrderedThing -Context ([ordered]@{ a = 1 }) }

Mock -CommandName 'Get-OrderedThing' -MockWith { 'mock' }

Invoke-OrderedWrapper | Should -Be 'mock'
Should -Invoke Get-OrderedThing -Times 1 -Exactly
}
}

Describe "Running Mock with ModuleName in test scope" {
BeforeAll {
Get-Module "test" -ErrorAction SilentlyContinue | Remove-Module
Expand Down
Loading