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
46 changes: 37 additions & 9 deletions src/functions/assert/Exception/Should-Throw.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -143,17 +143,45 @@
function Get-ErrorObject ($ErrorRecord) {

if ($ErrorRecord.Exception -like '*"InvokeWithContext"*') {
$e = $ErrorRecord.Exception.InnerException.ErrorRecord
# The scriptblock ran via InvokeWithContext, so a terminating error is wrapped in
# a MethodInvocationException. Unwrap it so we report the same exception that
# & { } would have surfaced in $_ (#2873).
$inner = $ErrorRecord.Exception.InnerException
$record = $inner.ErrorRecord

# When the inner exception carries an error record whose own exception is just a
# ParentContainsErrorRecordException placeholder (e.g. a parameter binding failure,
# or a division by zero), the real exception is the inner exception itself.
# Otherwise the record's exception is the one the caller threw - for example
# Write-Error -ErrorAction Stop wraps the original record in an
# ActionPreferenceStopException, and Get-Item -ErrorAction Stop does the same.
if ($null -eq $record) {
$realException = $inner
$record = $ErrorRecord
}
elseif ($record.Exception -is [System.Management.Automation.ParentContainsErrorRecordException]) {
$realException = $inner
}
else {
$realException = $record.Exception
}

[PSCustomObject] @{
ErrorRecord = $record
ExceptionMessage = $realException.Message
Exception = $realException
ExceptionType = $realException.GetType()
FullyQualifiedErrorId = $record.FullyQualifiedErrorId
}
}
else {
$e = $ErrorRecord
}
[PSCustomObject] @{
ErrorRecord = $e
ExceptionMessage = $e.Exception.Message
Exception = $e.Exception
ExceptionType = $e.Exception.GetType()
FullyQualifiedErrorId = $e.FullyQualifiedErrorId
[PSCustomObject] @{
ErrorRecord = $ErrorRecord
ExceptionMessage = $ErrorRecord.Exception.Message
Exception = $ErrorRecord.Exception
ExceptionType = $ErrorRecord.Exception.GetType()
FullyQualifiedErrorId = $ErrorRecord.FullyQualifiedErrorId
}
}
}

Expand Down
40 changes: 40 additions & 0 deletions tst/functions/assert/Exception/Should-Throw.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ Describe "Should-Throw" {
It "Fails when exception is thrown, but is not the expected type nor iheriting form the expected type" {
{ { throw [InvalidOperationException]"This operation is invalid!" } | Should-Throw -ExceptionType ([ArgumentException]) } | Verify-AssertionFailed
}

It "Matches the real exception type for a parameter binding failure (#2873)" {
# Used to report ParentContainsErrorRecordException instead of the real type.
{ Get-Item "/non-existing" -NonExistentParameter 1 } |
Should-Throw -ExceptionType ([System.Management.Automation.ParameterBindingException])
}
}

Context "Filtering with exception message" {
Expand Down Expand Up @@ -242,5 +248,39 @@ InPesterModuleScope {
$err.ExceptionType | Verify-Equal ([Management.Automation.ItemNotFoundException])
$err.FullyQualifiedErrorId | Verify-Equal 'PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand'
}

# #2873 - the exception type must match what & { } would surface in $_, not the
# ParentContainsErrorRecordException placeholder that some inner error records carry.
BeforeAll {
function Get-ThrownError ([scriptblock] $ScriptBlock) {
try {
$eap = [PSVariable]::new("erroractionpreference", 'Stop')
$null = $ScriptBlock.InvokeWithContext($null, $eap, $null) 2>&1
}
catch {
return Get-ErrorObject $_
}
}
}

It 'Reports the real exception type for a parameter binding failure (#2873)' {
$err = Get-ThrownError { Get-Item "/non-existing" -NonExistentParameter 1 }
$err.ExceptionType | Verify-Equal ([System.Management.Automation.ParameterBindingException])
}

It 'Reports the real exception type for a division by zero (#2873)' {
$err = Get-ThrownError { 1 / $null }
$err.ExceptionType | Verify-Equal ([System.Management.Automation.RuntimeException])
}

It 'Reports the wrapped exception for Write-Error -ErrorAction Stop (#2873)' {
$err = Get-ThrownError { Write-Error -ErrorAction Stop -Exception ([System.ArgumentException]"boom") }
$err.ExceptionType | Verify-Equal ([System.ArgumentException])
}

It 'Reports the wrapped exception for a cmdlet with -ErrorAction Stop (#2873)' {
$err = Get-ThrownError { Get-Item "/non-existing" -ErrorAction Stop }
$err.ExceptionType | Verify-Equal ([System.Management.Automation.ItemNotFoundException])
}
}
}
Loading