Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
98f0c0d
Initial plan
Copilot Jul 25, 2025
180cea5
Fix escape character handling in test display names
Copilot Jul 25, 2025
132d475
Fixes
Evangelink Jul 25, 2025
1b305a4
Parameterize tests to cover all C0 control characters (U+0000-U+001F)
Copilot Jul 25, 2025
e2994cb
Fix diagnostic
Evangelink Jul 25, 2025
1913168
Fix broken tests
Evangelink Jul 30, 2025
39969d7
Merge branch 'main' into copilot/fix-5133
Evangelink Jul 30, 2025
0ebce8c
Simplfied
Evangelink Jul 30, 2025
b58187d
Missed replace
Evangelink Jul 30, 2025
0869b81
Apply suggestion from @nohwnd
Evangelink Jul 31, 2025
d5afd2e
Apply suggestion from @nohwnd
Evangelink Jul 31, 2025
3ac28ca
Merge branch 'main' into copilot/fix-5133
Evangelink Jul 31, 2025
a5d57df
Optimize NormalizeSpecialCharacters method for better performance
Copilot Jul 31, 2025
303e533
Improve test clarity and optimize NormalizeSpecialCharacters method
Copilot Jul 31, 2025
e55b2bb
Add multi-targeting support for NormalizeSpecialCharacters optimization
Copilot Jul 31, 2025
d5e8b2e
Remove unnecessary using directive for System.Buffers
Copilot Jul 31, 2025
975f931
Fix IDE0300 warnings by simplifying collection initialization syntax
Copilot Jul 31, 2025
93513d8
Fix
Evangelink Aug 1, 2025
954bd5c
Fix diagnostic
Evangelink Aug 1, 2025
f52a77d
Merge branch 'main' into copilot/fix-5133
Evangelink Aug 4, 2025
3f995c8
Remove trailing spaces
Evangelink Aug 4, 2025
aca5b88
Rename NormalizeSpecialCharacters to MakeControlCharactersVisible and…
Copilot Aug 4, 2025
cd21778
Merge branch 'main' into copilot/fix-5133
Evangelink Aug 13, 2025
a923d0e
Fix diagnostics
Evangelink Aug 13, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ private void RenderTestCompleted(
terminal.Append(outcomeText);
terminal.ResetColor();
terminal.Append(' ');
terminal.Append(displayName);
terminal.Append(NormalizeTestDisplayName(displayName));
terminal.SetColor(TerminalColor.DarkGray);
terminal.Append(' ');
AppendLongDuration(terminal, duration);
Expand Down Expand Up @@ -536,11 +536,11 @@ private static void FormatStandardAndErrorOutput(ITerminal terminal, string? sta
terminal.SetColor(TerminalColor.DarkGray);
terminal.Append(SingleIndentation);
terminal.AppendLine(PlatformResources.StandardOutput);
string? standardOutputWithoutSpecialChars = NormalizeSpecialCharacters(standardOutput);
string? standardOutputWithoutSpecialChars = NormalizeSpecialCharacters(standardOutput, false);
Comment thread
Evangelink marked this conversation as resolved.
Outdated
Comment thread
Evangelink marked this conversation as resolved.
Outdated
AppendIndentedLine(terminal, standardOutputWithoutSpecialChars, DoubleIndentation);
terminal.Append(SingleIndentation);
terminal.AppendLine(PlatformResources.StandardError);
string? standardErrorWithoutSpecialChars = NormalizeSpecialCharacters(standardError);
string? standardErrorWithoutSpecialChars = NormalizeSpecialCharacters(standardError, false);
Comment thread
Evangelink marked this conversation as resolved.
Outdated
AppendIndentedLine(terminal, standardErrorWithoutSpecialChars, DoubleIndentation);
terminal.ResetColor();
}
Expand Down Expand Up @@ -644,10 +644,52 @@ internal void AssemblyRunCompleted()
_terminalWithProgress.RemoveWorker(assemblyRun.SlotIndex);
}

private static string? NormalizeSpecialCharacters(string? text)
=> text?.Replace('\0', '\x2400')
// escape char
.Replace('\x001b', '\x241b');
private static string? NormalizeSpecialCharacters(string? text, bool normalizeWhitespaceCharacters)
Comment thread
Evangelink marked this conversation as resolved.
Outdated
{
string? normalized = text?
.Replace('\0', '\x2400') // NULL → ␀
.Replace('\x0001', '\x2401') // START OF HEADING → ␁
.Replace('\x0002', '\x2402') // START OF TEXT → ␂
.Replace('\x0003', '\x2403') // END OF TEXT → ␃
.Replace('\x0004', '\x2404') // END OF TRANSMISSION → ␄
.Replace('\x0005', '\x2405') // ENQUIRY → ␅
.Replace('\x0006', '\x2406') // ACKNOWLEDGE → ␆
.Replace('\x0007', '\x2407') // BELL → ␇
.Replace('\x0008', '\x2408') // BACKSPACE → ␈
.Replace('\x000B', '\x240B') // VERTICAL TAB → ␋
.Replace('\x000C', '\x240C') // FORM FEED → ␌
.Replace('\x000E', '\x240E') // SHIFT OUT → ␎
.Replace('\x000F', '\x240F') // SHIFT IN → ␏
.Replace('\x0010', '\x2410') // DATA LINK ESCAPE → ␐
.Replace('\x0011', '\x2411') // DEVICE CONTROL ONE → ␑
.Replace('\x0012', '\x2412') // DEVICE CONTROL TWO → ␒
.Replace('\x0013', '\x2413') // DEVICE CONTROL THREE → ␓
.Replace('\x0014', '\x2414') // DEVICE CONTROL FOUR → ␔
.Replace('\x0015', '\x2415') // NEGATIVE ACKNOWLEDGE → ␕
.Replace('\x0016', '\x2416') // SYNCHRONOUS IDLE → ␖
.Replace('\x0017', '\x2417') // END OF TRANSMISSION BLOCK → ␗
.Replace('\x0018', '\x2418') // CANCEL → ␘
.Replace('\x0019', '\x2419') // END OF MEDIUM → ␙
.Replace('\x001A', '\x241A') // SUBSTITUTE → ␚
.Replace('\x001B', '\x241B') // ESCAPE → ␛
.Replace('\x001C', '\x241C') // FILE SEPARATOR → ␜
.Replace('\x001D', '\x241D') // GROUP SEPARATOR → ␝
.Replace('\x001E', '\x241E') // RECORD SEPARATOR → ␞
.Replace('\x001F', '\x241F'); // UNIT SEPARATOR → ␟

if (normalizeWhitespaceCharacters)
{
normalized = normalized?
.Replace('\t', '\x2409') // TAB → ␉
.Replace('\n', '\x240A') // LINE FEED → ␊
.Replace('\r', '\x240D'); // CARRIAGE RETURN → ␍
}
Comment thread
Evangelink marked this conversation as resolved.
Outdated

return normalized;
}

private static string NormalizeTestDisplayName(string displayName)
=> NormalizeSpecialCharacters(displayName, true) ?? displayName;
Comment thread
Evangelink marked this conversation as resolved.
Outdated

/// <summary>
/// Appends a long duration in human readable format such as 1h 23m 500ms.
Expand Down Expand Up @@ -781,7 +823,7 @@ internal void TestDiscovered(string displayName)
asm.TotalTests++;
}

asm.DiscoveredTestDisplayNames.Add(displayName);
asm.DiscoveredTestDisplayNames.Add(NormalizeTestDisplayName(displayName));

_terminalWithProgress.UpdateWorker(asm.SlotIndex);
}
Expand Down Expand Up @@ -865,7 +907,7 @@ public void TestInProgress(
{
asm.TestNodeResultsState ??= new(Interlocked.Increment(ref _counter));
asm.TestNodeResultsState.AddRunningTestNode(
Interlocked.Increment(ref _counter), testNodeUid, displayName, CreateStopwatch());
Interlocked.Increment(ref _counter), testNodeUid, NormalizeTestDisplayName(displayName), CreateStopwatch());
}

_terminalWithProgress.UpdateWorker(asm.SlotIndex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -596,4 +596,155 @@ private class StackTraceException : Exception

public override string? StackTrace { get; }
}

// Test data for all C0 control characters (U+0000-U+001F) that are normalized
[DataRow('\x0000', '\x2400', "NULL")]
[DataRow('\x0001', '\x2401', "START OF HEADING")]
[DataRow('\x0002', '\x2402', "START OF TEXT")]
[DataRow('\x0003', '\x2403', "END OF TEXT")]
[DataRow('\x0004', '\x2404', "END OF TRANSMISSION")]
[DataRow('\x0005', '\x2405', "ENQUIRY")]
[DataRow('\x0006', '\x2406', "ACKNOWLEDGE")]
[DataRow('\x0007', '\x2407', "BELL")]
[DataRow('\x0008', '\x2408', "BACKSPACE")]
[DataRow('\t', '\x2409', "TAB")]
[DataRow('\n', '\x240A', "LINE FEED")]
[DataRow('\x000B', '\x240B', "VERTICAL TAB")]
[DataRow('\x000C', '\x240C', "FORM FEED")]
[DataRow('\r', '\x240D', "CARRIAGE RETURN")]
[DataRow('\x000E', '\x240E', "SHIFT OUT")]
[DataRow('\x000F', '\x240F', "SHIFT IN")]
[DataRow('\x0010', '\x2410', "DATA LINK ESCAPE")]
[DataRow('\x0011', '\x2411', "DEVICE CONTROL ONE")]
[DataRow('\x0012', '\x2412', "DEVICE CONTROL TWO")]
[DataRow('\x0013', '\x2413', "DEVICE CONTROL THREE")]
[DataRow('\x0014', '\x2414', "DEVICE CONTROL FOUR")]
[DataRow('\x0015', '\x2415', "NEGATIVE ACKNOWLEDGE")]
[DataRow('\x0016', '\x2416', "SYNCHRONOUS IDLE")]
[DataRow('\x0017', '\x2417', "END OF TRANSMISSION BLOCK")]
[DataRow('\x0018', '\x2418', "CANCEL")]
[DataRow('\x0019', '\x2419', "END OF MEDIUM")]
[DataRow('\x001A', '\x241A', "SUBSTITUTE")]
[DataRow('\x001B', '\x241B', "ESCAPE")]
[DataRow('\x001C', '\x241C', "FILE SEPARATOR")]
[DataRow('\x001D', '\x241D', "GROUP SEPARATOR")]
[DataRow('\x001E', '\x241E', "RECORD SEPARATOR")]
[DataRow('\x001F', '\x241F', "UNIT SEPARATOR")]
[TestMethod]
public void TestDisplayNames_WithControlCharacters_AreNormalized(char controlChar, char expectedChar, string charName)
{
string targetFramework = "net8.0";
string architecture = "x64";
string assembly = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\work\assembly.dll" : "/mnt/work/assembly.dll";

var stringBuilderConsole = new StringBuilderConsole();
var terminalReporter = new TerminalTestReporter(assembly, targetFramework, architecture, stringBuilderConsole, new CTRLPlusCCancellationTokenSource(), new TerminalTestReporterOptions
{
ShowPassedTests = () => true,
UseAnsi = false,
ShowProgress = () => false,
});

DateTimeOffset startTime = DateTimeOffset.MinValue;
DateTimeOffset endTime = DateTimeOffset.MaxValue;
terminalReporter.TestExecutionStarted(startTime, 1, isDiscovery: false);

terminalReporter.AssemblyRunStarted();

// Test display name with the specific control character
string testDisplayName = $"Test{controlChar}Name";
terminalReporter.TestCompleted(testNodeUid: "Test1", testDisplayName, TestOutcome.Passed, TimeSpan.FromSeconds(1),
informativeMessage: null, errorMessage: null, exception: null, expected: null, actual: null, standardOutput: null, errorOutput: null);

terminalReporter.AssemblyRunCompleted();
terminalReporter.TestExecutionCompleted(endTime);

string output = stringBuilderConsole.Output;

// Verify that the control character is replaced with its Unicode control picture
string expectedDisplayName = $"Test{expectedChar}Name";
Assert.Contains(expectedDisplayName, output, $"{charName} should be replaced with {expectedChar}");

// Verify that the literal control character is not present (unless it's printable in the output)
string originalDisplayName = $"Test{controlChar}Name";
if (controlChar is not '\t' and not '\n' and not '\r')
{
Assert.DoesNotContain(originalDisplayName, output, $"Literal {charName} should not be present");
}
}

// Test data for all C0 control characters (U+0000-U+001F) that are normalized
[DataRow('\x0000', '\x2400', "NULL")]
[DataRow('\x0001', '\x2401', "START OF HEADING")]
[DataRow('\x0002', '\x2402', "START OF TEXT")]
[DataRow('\x0003', '\x2403', "END OF TEXT")]
[DataRow('\x0004', '\x2404', "END OF TRANSMISSION")]
[DataRow('\x0005', '\x2405', "ENQUIRY")]
[DataRow('\x0006', '\x2406', "ACKNOWLEDGE")]
[DataRow('\x0007', '\x2407', "BELL")]
[DataRow('\x0008', '\x2408', "BACKSPACE")]
[DataRow('\t', '\x2409', "TAB")]
[DataRow('\n', '\x240A', "LINE FEED")]
[DataRow('\x000B', '\x240B', "VERTICAL TAB")]
[DataRow('\x000C', '\x240C', "FORM FEED")]
[DataRow('\r', '\x240D', "CARRIAGE RETURN")]
[DataRow('\x000E', '\x240E', "SHIFT OUT")]
[DataRow('\x000F', '\x240F', "SHIFT IN")]
[DataRow('\x0010', '\x2410', "DATA LINK ESCAPE")]
[DataRow('\x0011', '\x2411', "DEVICE CONTROL ONE")]
[DataRow('\x0012', '\x2412', "DEVICE CONTROL TWO")]
[DataRow('\x0013', '\x2413', "DEVICE CONTROL THREE")]
[DataRow('\x0014', '\x2414', "DEVICE CONTROL FOUR")]
[DataRow('\x0015', '\x2415', "NEGATIVE ACKNOWLEDGE")]
[DataRow('\x0016', '\x2416', "SYNCHRONOUS IDLE")]
[DataRow('\x0017', '\x2417', "END OF TRANSMISSION BLOCK")]
[DataRow('\x0018', '\x2418', "CANCEL")]
[DataRow('\x0019', '\x2419', "END OF MEDIUM")]
[DataRow('\x001A', '\x241A', "SUBSTITUTE")]
[DataRow('\x001B', '\x241B', "ESCAPE")]
[DataRow('\x001C', '\x241C', "FILE SEPARATOR")]
[DataRow('\x001D', '\x241D', "GROUP SEPARATOR")]
[DataRow('\x001E', '\x241E', "RECORD SEPARATOR")]
[DataRow('\x001F', '\x241F', "UNIT SEPARATOR")]
[TestMethod]
public void TestDiscovery_WithControlCharacters_AreNormalized(char controlChar, char expectedChar, string charName)
{
string targetFramework = "net8.0";
string architecture = "x64";
string assembly = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\work\assembly.dll" : "/mnt/work/assembly.dll";

var stringBuilderConsole = new StringBuilderConsole();
var terminalReporter = new TerminalTestReporter(assembly, targetFramework, architecture, stringBuilderConsole, new CTRLPlusCCancellationTokenSource(), new TerminalTestReporterOptions
{
ShowPassedTests = () => true,
UseAnsi = false,
ShowProgress = () => false,
});

DateTimeOffset startTime = DateTimeOffset.MinValue;
DateTimeOffset endTime = DateTimeOffset.MaxValue;
terminalReporter.TestExecutionStarted(startTime, 1, isDiscovery: true);

terminalReporter.AssemblyRunStarted();

// Test discovery with the specific control character
string testDisplayName = $"Test{controlChar}Name";
terminalReporter.TestDiscovered(testDisplayName);

terminalReporter.AssemblyRunCompleted();
terminalReporter.TestExecutionCompleted(endTime);

string output = stringBuilderConsole.Output;

// Verify that the control character is replaced with its Unicode control picture
string expectedDisplayName = $"Test{expectedChar}Name";
Assert.Contains(expectedDisplayName, output, $"{charName} should be replaced with {expectedChar} in discovery");

// Verify that the literal control character is not present (unless it's printable in the output)
string originalDisplayName = $"Test{controlChar}Name";
if (controlChar is not '\t' and not '\n' and not '\r')
{
Assert.DoesNotContain(originalDisplayName, output, $"Literal {charName} should not be present in discovery");
}
Comment thread
Evangelink marked this conversation as resolved.
Outdated
}
}
Loading