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
16 changes: 11 additions & 5 deletions src/functions/assert/Common/New-ShouldAssertion.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,23 @@ class ShouldAssertion {
$actual = $this.Collected['Actual']
}
$because = [string] $Data['Because']
# An explicit Hint overrides the default Get-AssertionGotcha hint, for assertions that have a
# more specific thing to say about their own failure (e.g. Should-Throw explaining that the
# -ExceptionMessage filter failed only because of unescaped wildcard characters). Like the
# other reserved keys it is not turned into a <key> token.
$hintOverride = [string] $Data['Hint']

# Everything except the three special keys becomes a <key> token in the message.
# Everything except the reserved keys becomes a <key> token in the message.
$extra = @{}
foreach ($key in $Data.Keys) {
if ($key -ne 'Expected' -and $key -ne 'Actual' -and $key -ne 'Because') {
if ($key -notin 'Expected', 'Actual', 'Because', 'Hint') {
$extra[$key] = $Data[$key]
}
}

$formattedMessage = Get-AssertionMessage -Expected $expected -Actual $actual -Because $because -Data $extra -DefaultMessage $Message -Pretty:$Pretty

$hint = $this.Hint()
$hint = if (-not [string]::IsNullOrEmpty($hintOverride)) { $hintOverride } else { $this.Hint() }
if ($hint) { $formattedMessage = "$formattedMessage`n`nHint: $hint" }

Invoke-AssertionFailed -Message $formattedMessage -CallerCmdlet $this.Caller
Expand Down Expand Up @@ -133,8 +138,9 @@ function New-ShouldAssertion {
or `CollectionItems`) selects both unrolling and the wording of the input hint.
- `Fail(message [, data])` reports a failure. `message` may contain `<expected>`,
`<actual>`, `<expectedType>`, `<actualType>`, `<because>` and any `<key>` present in
`data`. `data` is a hashtable whose `Expected`, `Actual` and `Because` entries are
treated specially; all other entries become message tokens. Whether this throws
`data`. `data` is a hashtable whose `Expected`, `Actual`, `Because` and `Hint` entries
are treated specially; all other entries become message tokens. `Hint` overrides the
default input hint with your own text, appended as `Hint: <text>`. Whether this throws
immediately or records the failure and continues (a soft assertion) is decided by the
caller's `-ErrorAction` or the `Should.ErrorAction` configuration, exactly like the
built-in assertions.
Expand Down
17 changes: 15 additions & 2 deletions src/functions/assert/Exception/Should-Throw.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,19 @@
}

$filterOnMessage = -not ([string]::IsNullOrWhiteSpace($ExceptionMessage))
$messageFailedOnWildcard = $false
if ($filterOnMessage) {
$filters += "with message like '$([System.Management.Automation.WildcardPattern]::Unescape($ExceptionMessage))'"
$unescapedExceptionMessage = [System.Management.Automation.WildcardPattern]::Unescape($ExceptionMessage)
$filters += "with message like '$unescapedExceptionMessage'"
if ($err.ExceptionMessage -notlike $ExceptionMessage) {
$buts += "the message was '$($err.ExceptionMessage)'"
# -ExceptionMessage matches with -like. When the actual message is identical to the
# expected one treated literally, the only reason the match failed is unescaped wildcard
# characters ([ ] * ?) in -ExceptionMessage. Flag it so the failure message is not
# baffling (#2968, #1793).
if ($err.ExceptionMessage -eq $unescapedExceptionMessage) {
$messageFailedOnWildcard = $true
}
}
}

Expand All @@ -133,7 +142,11 @@
$filter = Add-SpaceToNonEmptyString ( Join-And $filters -Threshold 3 )
$but = Join-And $buts
$defaultMessage = "Expected an exception,$filter to be thrown, but $but."
$assert.Fail($defaultMessage, @{ Because = $Because })
$data = @{ Because = $Because }
if ($messageFailedOnWildcard) {
$data['Hint'] = "-ExceptionMessage matches using wildcards (-like). The messages are identical except for the wildcard characters [ ] * ? in -ExceptionMessage. Escape them with a backtick (``[) or use [System.Management.Automation.WildcardPattern]::Escape() to match them literally."
}
$assert.Fail($defaultMessage, $data)
}

$err.ErrorRecord
Expand Down
19 changes: 19 additions & 0 deletions tst/functions/assert/Common/New-ShouldAssertion.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,25 @@ Describe "New-ShouldAssertion" {
$err = { 'lame' | Should-BeAwesome } | Verify-AssertionFailed
($err.Exception.Message -notlike '*Hint:*') | Verify-True
}

It "uses an explicit Hint from the data instead of the default input hint" {
function Should-BeAwesomeHinted {
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseProcessBlockForPipelineCommand', '')]
[CmdletBinding()]
param (
[Parameter(Position = 1, ValueFromPipeline = $true)] $Actual,
[Parameter(Position = 0)] $Expected = 'Awesome'
)
$assert = New-ShouldAssertion -Caller $PSCmdlet -Actual $Actual -Buffer $local:Input
$assert.Fail("Expected <expected> but got <actual>.", @{ Expected = $Expected; Hint = 'try harder' })
}

# Even a multi-item collection, which would normally produce the default single-value
# hint, gets the assertion's own hint instead.
$err = { 1, 2, 3 | Should-BeAwesomeHinted } | Verify-AssertionFailed
$err.Exception.Message | Verify-Like '*Hint: try harder'
($err.Exception.Message -like '*single-value assertion*') | Verify-False
}
}

Context "Message tokens" {
Expand Down
14 changes: 14 additions & 0 deletions tst/functions/assert/Exception/Should-Throw.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,20 @@ Describe "Should-Throw" {
$err = { { throw [ArgumentException]"[!]" } | Should-Throw -ExceptionMessage '`[`]' } | Verify-AssertionFailed
$err.Exception.Message | Verify-Equal "Expected an exception, with message like '[]' to be thrown, but the message was '[!]'."
}

It "Hints at wildcard matching when the message is identical except for unescaped wildcard characters (#2968)" {
# -ExceptionMessage matches with -like, so [ ] * ? are wildcards. When the actual message
# is identical to the expected one treated literally, the match failed only because those
# characters were not escaped. The hint points that out instead of showing two
# identical-looking messages.
$err = { { throw 'value is [1]' } | Should-Throw -ExceptionMessage 'value is [1]' } | Verify-AssertionFailed
$err.Exception.Message | Verify-Equal "Expected an exception, with message like 'value is [1]' to be thrown, but the message was 'value is [1]'.`n`nHint: -ExceptionMessage matches using wildcards (-like). The messages are identical except for the wildcard characters [ ] * ? in -ExceptionMessage. Escape them with a backtick (``[) or use [System.Management.Automation.WildcardPattern]::Escape() to match them literally."
}

It "Does not hint at wildcard matching when the messages genuinely differ" {
$err = { { throw [ArgumentException]"fail!" } | Should-Throw -ExceptionMessage 'halt!' } | Verify-AssertionFailed
($err.Exception.Message -like '*matches using wildcards*') | Verify-False
}
}

Context "Unwrapping exception from different sources" {
Expand Down
Loading