From 0935809f2c7abea55d5acbc03b03ba08da15200b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Thu, 30 Jan 2025 09:12:05 +0100 Subject: [PATCH 1/4] Replace escape characters in Format-String2 --- src/Format2.ps1 | 2 +- tst/Format2.Tests.ps1 | 17 ++++++++++++++++- .../assert/String/Should-BeString.Tests.ps1 | 5 +++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Format2.ps1 b/src/Format2.ps1 index 38348fb0d..3be7f3897 100644 --- a/src/Format2.ps1 +++ b/src/Format2.ps1 @@ -50,7 +50,7 @@ function Format-String2 ($Value) { return '' } - "'$Value'" + "'$Value'".Replace("`0", '␀').Replace("`a", '␇').Replace("`b", '␈').Replace("`t", '␉').Replace("`f", '␌').Replace("`r", '␍').Replace("`n", '␊').Replace("`e", '␛') } function Format-Null2 { diff --git a/tst/Format2.Tests.ps1 b/tst/Format2.Tests.ps1 index ecb4b9ee2..04484be76 100644 --- a/tst/Format2.Tests.ps1 +++ b/tst/Format2.Tests.ps1 @@ -209,8 +209,23 @@ InPesterModuleScope { Format-String2 -Value "" | Verify-Equal '' } - It "Formats string to be sorrounded by quotes" { + It "Formats string to be surrounded by quotes" { Format-String2 -Value "abc" | Verify-Equal "'abc'" } + + + It "Replaces ansi escapes with their showable equivalent" -ForEach @( + @{ Value = "`0"; Expected = '␀' } + @{ Value = "`a"; Expected = '␇' } + @{ Value = "`b"; Expected = '␈' } + @{ Value = "`t"; Expected = '␉' } + @{ Value = "`f"; Expected = '␌' } + @{ Value = "`r"; Expected = '␍' } + @{ Value = "`n"; Expected = '␊' } + @{ Value = "`e"; Expected = '␛' } + ) { + Format-String2 -Value "-$value-" | Verify-Equal "'-$expected-'" + } + } } diff --git a/tst/functions/assert/String/Should-BeString.Tests.ps1 b/tst/functions/assert/String/Should-BeString.Tests.ps1 index db9e702fe..92c21119b 100644 --- a/tst/functions/assert/String/Should-BeString.Tests.ps1 +++ b/tst/functions/assert/String/Should-BeString.Tests.ps1 @@ -120,4 +120,9 @@ Describe "Should-BeString" { $err = { Should-BeString -Expected "abc" -Actual "bde" } | Verify-AssertionFailed $err.Exception.Message | Verify-Equal "Expected [string] 'abc', but got [string] 'bde'." } + + It "Handles escape character in the error message" { + $err = { Should-BeString -Expected "a`eb" -Actual "mmmmm" } | Verify-AssertionFailed + $err.Exception.Message | Verify-Equal "Expected [string] 'a␛b', but got [string] 'mmmmm'." + } } From 7586810c0390a05a4d60a223f43e4de9539ac8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Thu, 30 Jan 2025 09:46:23 +0100 Subject: [PATCH 2/4] Special case for esc, which was introduced in pwsh 7 --- src/Format2.ps1 | 8 ++++- tst/Format2.Tests.ps1 | 34 ++++++++++++++----- .../assert/String/Should-BeString.Tests.ps1 | 4 +-- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/Format2.ps1 b/src/Format2.ps1 index 3be7f3897..e80098827 100644 --- a/src/Format2.ps1 +++ b/src/Format2.ps1 @@ -50,7 +50,13 @@ function Format-String2 ($Value) { return '' } - "'$Value'".Replace("`0", '␀').Replace("`a", '␇').Replace("`b", '␈').Replace("`t", '␉').Replace("`f", '␌').Replace("`r", '␍').Replace("`n", '␊').Replace("`e", '␛') + $value = "'$Value'".Replace("`0", '␀').Replace("`a", '␇').Replace("`b", '␈').Replace("`t", '␉').Replace("`f", '␌').Replace("`r", '␍').Replace("`n", '␊') + + if ($PSVersionTable.PSVersion.Major -ge 7) { + $value = $value.Replace("`e", '␛') + } + + $value } function Format-Null2 { diff --git a/tst/Format2.Tests.ps1 b/tst/Format2.Tests.ps1 index 04484be76..ee2d3f8b6 100644 --- a/tst/Format2.Tests.ps1 +++ b/tst/Format2.Tests.ps1 @@ -215,14 +215,32 @@ InPesterModuleScope { It "Replaces ansi escapes with their showable equivalent" -ForEach @( - @{ Value = "`0"; Expected = '␀' } - @{ Value = "`a"; Expected = '␇' } - @{ Value = "`b"; Expected = '␈' } - @{ Value = "`t"; Expected = '␉' } - @{ Value = "`f"; Expected = '␌' } - @{ Value = "`r"; Expected = '␍' } - @{ Value = "`n"; Expected = '␊' } - @{ Value = "`e"; Expected = '␛' } + @{ Value = "`0"; Expected = '␀'; PowerShellVersion = 5 } + @{ Value = "`a"; Expected = '␇'; PowerShellVersion = 5 } + @{ Value = "`b"; Expected = '␈'; PowerShellVersion = 5 } + @{ Value = "`t"; Expected = '␉'; PowerShellVersion = 5 } + @{ Value = "`f"; Expected = '␌'; PowerShellVersion = 5 } + @{ Value = "`r"; Expected = '␍'; PowerShellVersion = 5 } + @{ Value = "`n"; Expected = '␊'; PowerShellVersion = 5 } + # Escape for escape was introduced in PowerShell 7 + @{ Value = "`e"; Expected = '␛'; PowerShellVersion = 7 } + ) { + if ($PSVersionTable.PSVersion.Major -lt $PowerShellVersion) { + continue + } + + Format-String2 -Value "-$value-" | Verify-Equal "'-$expected-'" + } + + It "Does not replace non escaped values with escapes" -ForEach @( + @{ Value = "0"; Expected = '0' } + @{ Value = "a"; Expected = 'a' } + @{ Value = "b"; Expected = 'b' } + @{ Value = "t"; Expected = 't' } + @{ Value = "f"; Expected = 'f' } + @{ Value = "r"; Expected = 'r' } + @{ Value = "n"; Expected = 'n' } + @{ Value = "e"; Expected = 'e' } ) { Format-String2 -Value "-$value-" | Verify-Equal "'-$expected-'" } diff --git a/tst/functions/assert/String/Should-BeString.Tests.ps1 b/tst/functions/assert/String/Should-BeString.Tests.ps1 index 92c21119b..e12626965 100644 --- a/tst/functions/assert/String/Should-BeString.Tests.ps1 +++ b/tst/functions/assert/String/Should-BeString.Tests.ps1 @@ -122,7 +122,7 @@ Describe "Should-BeString" { } It "Handles escape character in the error message" { - $err = { Should-BeString -Expected "a`eb" -Actual "mmmmm" } | Verify-AssertionFailed - $err.Exception.Message | Verify-Equal "Expected [string] 'a␛b', but got [string] 'mmmmm'." + $err = { Should-BeString -Expected "ee `e ee" -Actual "mmmmm" } | Verify-AssertionFailed + $err.Exception.Message | Verify-Equal "Expected [string] 'ee ␛ ee', but got [string] 'mmmmm'." } } From f2e8f549f6e63ba4cd668ba644f445cefe189932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Fri, 31 Jan 2025 15:04:56 +0100 Subject: [PATCH 3/4] Replace special chars --- src/Format2.ps1 | 32 +++++++++++++++++++++++----- src/functions/TestResults.NUnit3.ps1 | 2 +- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Format2.ps1 b/src/Format2.ps1 index e80098827..67c1759c9 100644 --- a/src/Format2.ps1 +++ b/src/Format2.ps1 @@ -1,4 +1,8 @@ -function Format-Collection2 ($Value, [switch]$Pretty) { + +# 0x1B is escape, the control code "`e" was introduces in Powershell 7, and so we write it directly as the number here. +[char[]] $script:specialChars = foreach ($ch in ("`0", "`a", "`b", "`t", "`f", "`r", "`n", 0x1B)) { [char]$ch } + +function Format-Collection2 ($Value, [switch]$Pretty) { $length = 0 $o = foreach ($v in $Value) { $formatted = Format-Nicely2 -Value $v -Pretty:$Pretty @@ -45,18 +49,36 @@ function Format-Object2 ($Value, $Property, [switch]$Pretty) { $o } +function Replace-SpecialCharactersInString ($Value) { + +} function Format-String2 ($Value) { if ('' -eq $Value) { return '' } - $value = "'$Value'".Replace("`0", '␀').Replace("`a", '␇').Replace("`b", '␈').Replace("`t", '␉').Replace("`f", '␌').Replace("`r", '␍').Replace("`n", '␊') - if ($PSVersionTable.PSVersion.Major -ge 7) { - $value = $value.Replace("`e", '␛') + # Special characters like `n or `e should be rendered as their visible versions. + # Especially `e which is used in ANSI codes and can entirely break our output. + # + # We convert each of these using the unicode printable version, + # which is obtained by adding 0x2400 + [int]$unicodeControlPictures = 0x2400 + + if (0 -lt $Value.IndexOfAny($script:specialChars)) { + $chars = [char[]]$Value + $charCount = $chars.Length + for ($j = 0; $j -lt $charCount; $j++) { + $char = $chars[$j] + if ($char -in $script:specialChars) { + $chars[$j] = [char]([int]$char + $unicodeControlPictures) + } + } + + $Value = $chars -join '' } - $value + "'$Value'" } function Format-Null2 { diff --git a/src/functions/TestResults.NUnit3.ps1 b/src/functions/TestResults.NUnit3.ps1 index a4b08a70f..b0989aa2e 100644 --- a/src/functions/TestResults.NUnit3.ps1 +++ b/src/functions/TestResults.NUnit3.ps1 @@ -1,6 +1,6 @@ # NUnit3 schema docs: https://docs.nunit.org/articles/nunit/technical-notes/usage/Test-Result-XML-Format.html -[char[]] $script:invalidCDataChars = foreach ($ch in (0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0B, 0x0C, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F)) { [char]$ch } +[char[]] $script:invalidCDataChars = foreach ($ch in (0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0B, 0x0C, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F)) { [char]$ch } function Write-NUnit3Report([Pester.Run] $Result, [System.Xml.XmlWriter] $XmlWriter) { # Write the XML Declaration From 13af0b7a68cd2a5ac77415daf92ae8f1986398b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Thu, 20 Feb 2025 20:44:08 +0100 Subject: [PATCH 4/4] More escaping in nunit3 xml --- src/functions/TestResults.NUnit3.ps1 | 7 ++++--- test.ps1 | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/functions/TestResults.NUnit3.ps1 b/src/functions/TestResults.NUnit3.ps1 index 69675059b..0831a8466 100644 --- a/src/functions/TestResults.NUnit3.ps1 +++ b/src/functions/TestResults.NUnit3.ps1 @@ -516,8 +516,9 @@ function Write-NUnit3TestCaseAttributes { $XmlWriter.WriteAttributeString('id', (Get-NUnit3NodeId)) # Workaround - name-attribute should be $name, but CI-reports don't show the tree-view nor use fullname # See https://github.com/pester/Pester/issues/1530#issuecomment-1186187298 - $XmlWriter.WriteAttributeString('name', $fullname) - $XmlWriter.WriteAttributeString('fullname', $fullname) + $escapedFullName = (Format-CDataString $fullname) + $XmlWriter.WriteAttributeString('name', $escapedFullName) + $XmlWriter.WriteAttributeString('fullname', $escapedFullName) $XmlWriter.WriteAttributeString('methodname', $TestResult.Name) $XmlWriter.WriteAttributeString('classname', $TestResult.Block.Path -join '.') $XmlWriter.WriteAttributeString('runstate', $runstate) @@ -646,7 +647,7 @@ function Write-NUnit3DataProperty ([System.Collections.IDictionary] $Data, [Syst $XmlWriter.WriteStartElement('property') $XmlWriter.WriteAttributeString('name', $name) - $XmlWriter.WriteAttributeString('value', $formattedValue) + $XmlWriter.WriteAttributeString('value', (Format-CDataString $formattedValue)) $XmlWriter.WriteEndElement() # Close property } } diff --git a/test.ps1 b/test.ps1 index 553cd698a..dfbb5c89b 100644 --- a/test.ps1 +++ b/test.ps1 @@ -185,6 +185,7 @@ if ($CI) { $configuration.CodeCoverage.Enabled = $false $configuration.TestResult.Enabled = $true + $configuration.TestResult.OutputFormat = 'NUnit3' } $r = Invoke-Pester -Configuration $configuration