diff --git a/src/Format2.ps1 b/src/Format2.ps1 index 6e05bcd89..64df841ea 100644 --- a/src/Format2.ps1 +++ b/src/Format2.ps1 @@ -1,7 +1,7 @@ -function Format-Collection2 ($Value, [switch]$Pretty) { +function Format-Collection2 ($Value, [switch]$Pretty, [int]$Depth = 0) { $length = 0 $o = foreach ($v in $Value) { - $formatted = Format-Nicely2 -Value $v -Pretty:$Pretty + $formatted = Format-Nicely2 -Value $v -Pretty:$Pretty -Depth ($Depth + 1) $length += $formatted.Length + 1 # 1 is for the separator $formatted } @@ -19,7 +19,7 @@ } } -function Format-Object2 ($Value, $Property, [switch]$Pretty) { +function Format-Object2 ($Value, $Property, [switch]$Pretty, [int]$Depth = 0) { if ($null -eq $Property) { $Property = foreach ($p in $Value.PSObject.Properties) { $p.Name } } @@ -31,7 +31,7 @@ function Format-Object2 ($Value, $Property, [switch]$Pretty) { $valueType = Get-ShortType $Value $items = foreach ($p in $orderedProperty) { $v = ([PSObject]$Value.$p) - $f = Format-Nicely2 -Value $v -Pretty:$Pretty + $f = Format-Nicely2 -Value $v -Pretty:$Pretty -Depth ($Depth + 1) "$p=$f" } @@ -81,31 +81,31 @@ function Format-Number2 ($Value) { [string]$Value } -function Format-Hashtable2 ($Value) { +function Format-Hashtable2 ($Value, [int]$Depth = 0) { $head = '@{' $tail = '}' $entries = foreach ($v in $Value.Keys | & $SafeCommands['Sort-Object']) { - $formattedValue = Format-Nicely2 $Value.$v + $formattedValue = Format-Nicely2 -Value $Value.$v -Depth ($Depth + 1) "$v=$formattedValue" } $head + ( $entries -join '; ') + $tail } -function Format-Dictionary2 ($Value) { +function Format-Dictionary2 ($Value, [int]$Depth = 0) { $head = 'Dictionary{' $tail = '}' $entries = foreach ($v in $Value.Keys | & $SafeCommands['Sort-Object'] ) { - $formattedValue = Format-Nicely2 $Value.$v + $formattedValue = Format-Nicely2 -Value $Value.$v -Depth ($Depth + 1) "$v=$formattedValue" } $head + ( $entries -join '; ') + $tail } -function Format-Nicely2 ($Value, [switch]$Pretty) { +function Format-Nicely2 ($Value, [switch]$Pretty, [int]$Depth = 0) { if ($null -eq $Value) { return Format-Null2 -Value $Value } @@ -130,8 +130,20 @@ function Format-Nicely2 ($Value, [switch]$Pretty) { return Format-ScriptBlock2 -Value $Value } + # Deeply nested or self-referential objects (e.g. SMO stubs or DirectoryInfo, whose + # Parent/Root point back up the tree) would otherwise recurse until PowerShell throws + # "The script failed due to call depth overflow" (#2828, #2474). Once we are past a sane + # nesting depth stop expanding and just print the value's type, which is enough for a + # diagnostic message and cannot recurse further. Scalars above are always fully formatted; + # only the container/object branches below recurse, so the guard sits in front of them. + # A depth of 10 is never useful in an assertion message and is well below PowerShell's own + # call-depth limit, so it is fixed here rather than exposed as a configurable variable. + if ($Depth -ge 10) { + return Get-ShortType2 -Value $Value + } + if (Is-Collection -Value $Value) { - return Format-Collection2 -Value $Value -Pretty:$Pretty + return Format-Collection2 -Value $Value -Pretty:$Pretty -Depth $Depth } if (Is-Value -Value $Value) { @@ -139,18 +151,18 @@ function Format-Nicely2 ($Value, [switch]$Pretty) { } if (Is-Hashtable -Value $Value) { - return Format-Hashtable2 -Value $Value + return Format-Hashtable2 -Value $Value -Depth $Depth } if (Is-Dictionary -Value $Value) { - return Format-Dictionary2 -Value $Value + return Format-Dictionary2 -Value $Value -Depth $Depth } if ((Is-DataTable -Value $Value) -or (Is-DataRow -Value $Value)) { return Format-DataTable2 -Value $Value -Pretty:$Pretty } - Format-Object2 -Value $Value -Property (Get-DisplayProperty2 $Value.GetType()) -Pretty:$Pretty + Format-Object2 -Value $Value -Property (Get-DisplayProperty2 $Value.GetType()) -Pretty:$Pretty -Depth $Depth } function Format-NicelyForTemplate ($Value) { diff --git a/tst/Format2.Tests.ps1 b/tst/Format2.Tests.ps1 index 2514e7978..0ff6432d4 100644 --- a/tst/Format2.Tests.ps1 +++ b/tst/Format2.Tests.ps1 @@ -193,6 +193,30 @@ InPesterModuleScope { $job | Remove-Job -Force $result | Should -Not -BeNullOrEmpty } + + # Regression test for https://github.com/pester/Pester/issues/2828 + # A self-referential object (Self points back at the same instance, like SMO stubs or + # DirectoryInfo.Parent/Root) used to recurse until PowerShell threw a call depth overflow. + It "Stops expanding a self-referential object instead of overflowing the call stack" { + $o = [PSCustomObject]@{ Name = 'x' } + $o | Add-Member -NotePropertyName Self -NotePropertyValue $o + + # Formatting completes at all (no ScriptCallDepthException) and the back-reference is + # cut off with a type-only marker once the max depth is reached, not expanded forever. + $formatted = Format-Nicely2 -Value $o + $formatted.Contains('Self=[PSObject]') | Verify-True + } + + It "Truncates values nested past the max depth to their type" { + # Build a chain deeper than the max depth. The leaf sits below the cut-off, so it must + # never be reached, and the deepest shown value is a type-only marker instead. + $node = [PSCustomObject]@{ Leaf = 'bottom' } + foreach ($i in 1..20) { $node = [PSCustomObject]@{ Child = $node } } + + $formatted = Format-Nicely2 -Value $node + $formatted.Contains("'bottom'") | Verify-False + $formatted.Contains('[PSObject]') | Verify-True + } } Describe "Get-DisplayProperty2" { diff --git a/tst/functions/assert/General/Should-HaveType.Tests.ps1 b/tst/functions/assert/General/Should-HaveType.Tests.ps1 index 804c73700..e491aa85a 100644 --- a/tst/functions/assert/General/Should-HaveType.Tests.ps1 +++ b/tst/functions/assert/General/Should-HaveType.Tests.ps1 @@ -19,6 +19,17 @@ Describe "Should-HaveType" { ) { Should-HaveType -Actual $Value -Expected ([string[]]) } + + # Regression test for https://github.com/pester/Pester/issues/2828 + # Formatting a self-referential actual value for the failure message used to recurse until + # PowerShell threw "The script failed due to call depth overflow", hiding the real result. + It "Reports a normal assertion failure for a self-referential value instead of overflowing" { + $o = [PSCustomObject]@{ Name = 'x' } + $o | Add-Member -NotePropertyName Self -NotePropertyValue $o + + $err = { Should-HaveType -Actual $o -Expected ([string]) } | Verify-AssertionFailed + $err.Exception.Message | Verify-Like '*Expected value to have type*' + } } Describe "Should-HaveType input hint" {