Skip to content

Commit 42e4169

Browse files
nohwndCopilot
andauthored
Fix #2561: Escape control characters in assertion error messages (#2690)
Format-String2 now escapes control characters as Unicode control pictures so they are visible in error messages. Characters like ESC (used in ANSI sequences) now display as their Unicode symbol instead of being invisible. Copilot-generated fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d0be71a commit 42e4169

3 files changed

Lines changed: 129 additions & 2 deletions

File tree

src/Format2.ps1

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,17 @@ function Format-Object2 ($Value, $Property, [switch]$Pretty) {
4646
}
4747

4848
function Format-String2 ($Value) {
49-
if ('' -eq $Value) {
49+
# Use .Length instead of '' -eq $Value because PowerShell's -eq operator
50+
# considers some control characters (NUL, BEL, BS, ESC) equal to empty string.
51+
if ($null -eq $Value -or $Value.Length -eq 0) {
5052
return '<empty>'
5153
}
5254

53-
"'$Value'"
55+
# Escape ASCII control characters (0x00..0x1F) to the Unicode "Control
56+
# Pictures" block (U+2400..U+241F) so they remain visible in error messages.
57+
# See https://github.com/pester/Pester/issues/2561. Hot loop lives in C#
58+
# (Pester.Formatter) for speed.
59+
"'" + [Pester.Formatter]::EscapeControlChars($Value) + "'"
5460
}
5561

5662
function Format-Null2 {

src/csharp/Pester/Formatter.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
3+
namespace Pester
4+
{
5+
public static class Formatter
6+
{
7+
private static readonly char[] ControlChars = BuildControlChars();
8+
9+
private static char[] BuildControlChars()
10+
{
11+
var chars = new char[0x20];
12+
for (int i = 0; i < 0x20; i++)
13+
{
14+
chars[i] = (char)i;
15+
}
16+
return chars;
17+
}
18+
19+
/// <summary>
20+
/// Replaces ASCII control characters (0x00..0x1F) in <paramref name="value"/>
21+
/// with the matching Unicode "Control Pictures" code point
22+
/// (U+2400..U+241F) so they are visible when printed. See
23+
/// https://github.com/pester/Pester/issues/2561.
24+
///
25+
/// Returns <paramref name="value"/> unchanged when it is null, empty, or
26+
/// contains no control characters — common case, no allocation beyond
27+
/// the IndexOfAny scan.
28+
/// </summary>
29+
public static string EscapeControlChars(string value)
30+
{
31+
if (string.IsNullOrEmpty(value))
32+
{
33+
return value;
34+
}
35+
36+
int firstControl = value.IndexOfAny(ControlChars);
37+
if (firstControl < 0)
38+
{
39+
return value;
40+
}
41+
42+
int len = value.Length;
43+
var buf = new char[len];
44+
value.CopyTo(0, buf, 0, len);
45+
46+
// Everything before firstControl is known to be safe; start from there.
47+
for (int i = firstControl; i < len; i++)
48+
{
49+
char c = buf[i];
50+
if (c < 0x20)
51+
{
52+
buf[i] = (char)(c + 0x2400);
53+
}
54+
}
55+
56+
return new string(buf);
57+
}
58+
}
59+
}

tst/Format2.Tests.ps1

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,5 +241,67 @@ InPesterModuleScope {
241241
It "Formats string to be sorrounded by quotes" {
242242
Format-String2 -Value "abc" | Verify-Equal "'abc'"
243243
}
244+
245+
# Regression tests for https://github.com/pester/Pester/issues/2561
246+
# Control characters must be escaped to Unicode control pictures so they are
247+
# visible in error messages instead of being invisible or breaking output.
248+
# Note: control picture chars (U+2400-U+241B) are written as literal Unicode in
249+
# single-quoted strings so the tests parse on PowerShell 5.1 too (`u{XXXX} is PS 6+).
250+
It "Escapes null character to control picture" {
251+
Format-String2 -Value "`0" | Verify-Equal "'␀'"
252+
}
253+
254+
It "Escapes bell character to control picture" {
255+
Format-String2 -Value "`a" | Verify-Equal "'␇'"
256+
}
257+
258+
It "Escapes backspace character to control picture" {
259+
Format-String2 -Value "`b" | Verify-Equal "'␈'"
260+
}
261+
262+
It "Escapes tab character to control picture" {
263+
Format-String2 -Value "`t" | Verify-Equal "'␉'"
264+
}
265+
266+
It "Escapes form feed character to control picture" {
267+
Format-String2 -Value "`f" | Verify-Equal "'␌'"
268+
}
269+
270+
It "Escapes carriage return character to control picture" {
271+
Format-String2 -Value "`r" | Verify-Equal "'␍'"
272+
}
273+
274+
It "Escapes newline character to control picture" {
275+
Format-String2 -Value "`n" | Verify-Equal "'␊'"
276+
}
277+
278+
It "Escapes ESC character to control picture" {
279+
Format-String2 -Value "$([char]27)" | Verify-Equal "'␛'"
280+
}
281+
282+
It "Leaves normal strings unchanged" {
283+
Format-String2 -Value "hello" | Verify-Equal "'hello'"
284+
}
285+
286+
It "Escapes ANSI sequence making escape char visible" {
287+
# ESC[31m is a common ANSI red color code; the ESC byte should become ␛
288+
$ansi = "$([char]27)[31m"
289+
$result = Format-String2 -Value $ansi
290+
$result | Verify-Equal "'␛[31m'"
291+
}
292+
293+
It "Escapes multiple control chars in one string" {
294+
$value = "a`t`nb"
295+
$result = Format-String2 -Value $value
296+
$result | Verify-Equal "'a␉␊b'"
297+
}
298+
299+
It "Escaped output contains no actual control characters" {
300+
# Round-trip: the escaped output should be a clean displayable string
301+
$value = "`0`a`b`t`f`r`n$([char]27)"
302+
$result = Format-String2 -Value $value
303+
# The result should not contain any of the original control characters
304+
$result | Should -Not -Match '[\x00-\x1F]'
305+
}
244306
}
245307
}

0 commit comments

Comments
 (0)