Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Should -HaveParamter -DefaultValue #2398

Merged
merged 3 commits into from
Nov 9, 2023
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
83 changes: 67 additions & 16 deletions src/functions/assertions/HaveParameter.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
function Get-ParameterInfo {
param (
[Parameter(Mandatory = $true)]
[Management.Automation.CommandInfo]$Command
[Management.Automation.CommandInfo]$Command,
[Parameter(Mandatory = $true)]
[string] $Name
)

# Resolve alias to the actual command so we can access scriptblock
Expand Down Expand Up @@ -71,26 +73,65 @@
}

foreach ($parameter in $parameters) {
if ($Name -ne $parameter.Name.VariablePath.UserPath) {
continue
}

$paramInfo = [PSCustomObject] @{
Name = $parameter.Name.VariablePath.UserPath
Type = "[$($parameter.StaticType.Name.ToLower())]"
HasDefaultValue = $false
DefaultValue = $null
DefaultValueType = $parameter.StaticType.Name
}

# Default value here contains a descriptor object of the default value,
# so this is null only when default value is not present at all, if default value
# is actually $null, this will have an object describing the type and the $null value.
if ($null -ne $parameter.DefaultValue) {
if ($parameter.DefaultValue.PSObject.Properties['Value']) {
$paramInfo.DefaultValue = $parameter.DefaultValue.Value
}
else {
$paramInfo.DefaultValue = $parameter.DefaultValue.Extent.Text
}
# The actual value of the default value can be falsy (e.g. $null, $false or 0)
# use this flag to communicate if default value was found in the AST or not,
# no matter if the actual default value is falsy.
# That is: param($param1 = $false) will set this to true for $param1
# but param($param1) will have this set to false, because there was no default value.
$paramInfo.HasDefaultValue = $true
# When the value has a known fully realized value (indicated by .Value being on the DefaultValue object)
# we take that and use it, otherwise we take the extent (how it was written in code). This will make
# 1, 2, or "abc", appear as 1, 2, abc to the assertion, but (Get-Date) will be (Get-Date).
$paramInfo.DefaultValue = Get-DefaultValue $parameter.DefaultValue
}

$paramInfo
break
}
}

function Get-DefaultValue {
param($DefaultValue)

# This is a value like 1, or 0, return it direcly.
if ($DefaultValue.PSObject.Properties["Value"]) {
return $DefaultValue.Value
}

# This is for backwards compatibility with Pester v5.4.0.
# Existing assertions check for -DefaultValue "false", while the definition
# of the function says $MyParam = $false.
if ('$true' -eq $DefaultValue.Extent.Text -or '$false' -eq $DefaultValue.Extent.Text) {
# returns "true", or "false" without $ prefix
return $DefaultValue.VariablePath
}

# This is for backwards compatibility with Pester v5.4.0.
# Existing assertions check for -DefaultValue "", while the definition
# of the function says $MyParam = $null or $MyParam without any default value.
if ('$null' -eq $DefaultValue.Extent.Text) {
return ""
}

$DefaultValue.Extent.Text
}

function Get-ArgumentCompleter {
<#
.SYNOPSIS
Expand Down Expand Up @@ -181,7 +222,8 @@
if ($ActualValue.Definition -match '^PesterMock_') {
$type = 'mock'
$suggestion = "'Get-Command $($ActualValue.Name) | Where-Object Parameters | Should -HaveParameter ...'"
} else {
}
else {
$type = 'alias'
$suggestion = "using the actual command name. For example: 'Get-Command $($ActualValue.Definition) | Should -HaveParameter ...'"
}
Expand Down Expand Up @@ -249,21 +291,30 @@
}

if ($PSBoundParameters.Keys -contains "DefaultValue") {
$parameterMetadata = Get-ParameterInfo $ActualValue | & $SafeCommands['Where-Object'] { $_.Name -eq $ParameterName }
$actualDefault = if ($parameterMetadata.DefaultValue) {
$parameterMetadata.DefaultValue
}
else {
""
$parameterMetadata = Get-ParameterInfo -Name $ParameterName -Command $ActualValue
if ($null -eq $parameterMetadata) {
# For safety, but this probably won't happen because if the parameter is not on the command we will fail much sooner.
throw "Metadata for parameter '$ParameterName' were not found."
}
$testDefault = ($actualDefault -eq $DefaultValue)

$filters += "the default value$(if ($Negate) {" not"}) to be $(Format-Nicely $DefaultValue)"

# We could determine if the value is present and what is it's exact value, and also always use the
# code literal that was used in the definition of the function (e.g. $true instead of "True"),
# but that would be a breaking change for Pester 5, and in case of strings it would be a little
# inconvenient for the users, because they would always have to provide doubled quotes, like '"aaa"'.
# So instead we force the values to be strings, and when the value is not there we define it as $null
# which prevents us from full checking if there was or was not an actual $null definition, but that is
# okay because you would rarely need to do that.
$defaultIsUnspecified = -not $parameterMetadata.HasDefaultValue
[string] $actualDefault = if ($defaultIsUnspecified) { $null } else { $parameterMetadata.DefaultValue }
$testDefault = ($actualDefault -eq $DefaultValue)

if (-not $Negate -and -not $testDefault) {
$buts += "the default value was $(Format-Nicely $actualDefault)"
}
elseif ($Negate -and $testDefault) {
$buts += "the default value was $(Format-Nicely $DefaultValue)"
$buts += "the default value was $(Format-Nicely $actualDefault)"
}
}

Expand Down
32 changes: 32 additions & 0 deletions tst/functions/assertions/HaveParameter.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,38 @@ InPesterModuleScope {
$err.Exception.Message | Verify-Equal "Expected command Invoke-DummyFunction to have a parameter ParamWithNotNullOrEmptyValidation, which is mandatory, of type [System.TimeSpan] and the default value to be 'wrong value', because of reasons, but it wasn't mandatory, it was of type [System.DateTime] and the default value was '(Get-Date)'."
}

It 'passes when object parameter has default parameter value $null' {
function Test-Parameter {
param ( [Parameter()] [object] $objParam = $null )
}

Get-Command Test-Parameter | Should -HaveParameter 'objParam' -Type 'object' -DefaultValue $null
}

It 'passes when integer parameter has default parameter value 0' {
function Test-Parameter {
param ( [Parameter()] [int] $intParam = 0 )
}

Get-Command Test-Parameter | Should -HaveParameter 'intParam' -Type 'int' -DefaultValue 0
}

It 'passes when bool parameter has default parameter value $true' {
function Test-Parameter {
param ( [Parameter()] [bool] $boolParam = $true )
nohwnd marked this conversation as resolved.
Show resolved Hide resolved
}

Get-Command Test-Parameter | Should -HaveParameter 'boolParam' -Type 'bool' -DefaultValue $true
}

It 'passes when bool parameter has default parameter value $false' {
function Test-Parameter {
param ( [Parameter()] [bool] $boolParam = $false )
}

Get-Command Test-Parameter | Should -HaveParameter 'boolParam' -Type 'bool' -DefaultValue $false
}

if ($PSVersionTable.PSVersion.Major -ge 5) {
It "returns the correct assertion message when parameter ParamWithNotNullOrEmptyValidation is not mandatory, of the wrong type, has a different default value than expected and has no ArgumentCompleter" {
$err = { Get-Command "Invoke-DummyFunction" | Should -HaveParameter ParamWithNotNullOrEmptyValidation -Mandatory -Type [TimeSpan] -DefaultValue "wrong value" -HasArgumentCompleter -Because 'of reasons' } | Verify-AssertionFailed
Expand Down