diff --git a/src/functions/assert/Exception/Should-Throw.ps1 b/src/functions/assert/Exception/Should-Throw.ps1 index 27b1809cf..54243901a 100644 --- a/src/functions/assert/Exception/Should-Throw.ps1 +++ b/src/functions/assert/Exception/Should-Throw.ps1 @@ -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 + } } } diff --git a/tst/functions/assert/Exception/Should-Throw.Tests.ps1 b/tst/functions/assert/Exception/Should-Throw.Tests.ps1 index cf0c516a9..ad3bc79b9 100644 --- a/tst/functions/assert/Exception/Should-Throw.Tests.ps1 +++ b/tst/functions/assert/Exception/Should-Throw.Tests.ps1 @@ -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" { @@ -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]) + } } }