diff --git a/src/NUnitTestAdapter/NUnitEngine/NUnitTestEvent.cs b/src/NUnitTestAdapter/NUnitEngine/NUnitTestEvent.cs index 10583244..af6798eb 100644 --- a/src/NUnitTestAdapter/NUnitEngine/NUnitTestEvent.cs +++ b/src/NUnitTestAdapter/NUnitEngine/NUnitTestEvent.cs @@ -127,7 +127,7 @@ protected NUnitTestEvent(XmlNode node) : base(node) public string MethodName => Node.GetAttribute("methodname"); public string ClassName => Node.GetAttribute("classname"); - public string Output => Node.SelectSingleNode("output")?.InnerText.UnEscapeUnicodeCharacters(); + public string Output => Node.SelectSingleNode("output")?.InnerText.UnEscapeUnicodeColorCodesCharacters(); public CheckedTime StartTime() @@ -165,7 +165,7 @@ public IEnumerable NUnitAttachments foreach (XmlNode attachment in Node.SelectNodes("attachments/attachment")) { var path = attachment.SelectSingleNode("filePath")?.InnerText ?? string.Empty; - var description = attachment.SelectSingleNode("description")?.InnerText.UnEscapeUnicodeCharacters(); + var description = attachment.SelectSingleNode("description")?.InnerText.UnEscapeUnicodeColorCodesCharacters(); nUnitAttachments.Add(new NUnitAttachment(path, description)); } return nUnitAttachments; diff --git a/src/NUnitTestAdapter/NUnitEngine/NUnitTestEventSuiteFinished.cs b/src/NUnitTestAdapter/NUnitEngine/NUnitTestEventSuiteFinished.cs index 6f0a900f..dbc941c5 100644 --- a/src/NUnitTestAdapter/NUnitEngine/NUnitTestEventSuiteFinished.cs +++ b/src/NUnitTestAdapter/NUnitEngine/NUnitTestEventSuiteFinished.cs @@ -28,10 +28,10 @@ public NUnitTestEventSuiteFinished(XmlNode node) : base(node) var failureNode = Node.SelectSingleNode("failure"); if (failureNode != null) { - FailureMessage = failureNode.SelectSingleNode("message")?.InnerText.UnEscapeUnicodeCharacters(); - StackTrace = failureNode.SelectSingleNode("stack-trace")?.InnerText.UnEscapeUnicodeCharacters(); + FailureMessage = failureNode.SelectSingleNode("message")?.InnerText.UnEscapeUnicodeColorCodesCharacters(); + StackTrace = failureNode.SelectSingleNode("stack-trace")?.InnerText.UnEscapeUnicodeColorCodesCharacters(); } - ReasonMessage = Node.SelectSingleNode("reason/message")?.InnerText.UnEscapeUnicodeCharacters(); + ReasonMessage = Node.SelectSingleNode("reason/message")?.InnerText.UnEscapeUnicodeColorCodesCharacters(); } public string ReasonMessage { get; } diff --git a/src/NUnitTestAdapter/NUnitEngine/NUnitTestEventTestCase.cs b/src/NUnitTestAdapter/NUnitEngine/NUnitTestEventTestCase.cs index 4b079cf5..c5f7438b 100644 --- a/src/NUnitTestAdapter/NUnitEngine/NUnitTestEventTestCase.cs +++ b/src/NUnitTestAdapter/NUnitEngine/NUnitTestEventTestCase.cs @@ -48,11 +48,11 @@ public NUnitTestEventTestCase(XmlNode node) if (failureNode != null) { Failure = new NUnitFailure( - failureNode.SelectSingleNode("message")?.InnerText.UnEscapeUnicodeCharacters(), - failureNode.SelectSingleNode("stack-trace")?.InnerText.UnEscapeUnicodeCharacters()); + failureNode.SelectSingleNode("message")?.InnerText.UnEscapeUnicodeColorCodesCharacters(), + failureNode.SelectSingleNode("stack-trace")?.InnerText.UnEscapeUnicodeColorCodesCharacters()); } - ReasonMessage = Node.SelectSingleNode("reason/message")?.InnerText.UnEscapeUnicodeCharacters(); + ReasonMessage = Node.SelectSingleNode("reason/message")?.InnerText.UnEscapeUnicodeColorCodesCharacters(); } public string ReasonMessage { get; } @@ -73,7 +73,7 @@ public string StackTrace int i = 1; foreach (XmlNode assertionStacktraceNode in Node.SelectNodes("assertions/assertion/stack-trace")) { - stackTrace += $"{i++}) " + assertionStacktraceNode.InnerText.UnEscapeUnicodeCharacters(); + stackTrace += $"{i++}) " + assertionStacktraceNode.InnerText.UnEscapeUnicodeColorCodesCharacters(); stackTrace += "\n"; } diff --git a/src/NUnitTestAdapter/NUnitEngine/UnicodeEscapeHelper.cs b/src/NUnitTestAdapter/NUnitEngine/UnicodeEscapeHelper.cs index 5949e930..616813bf 100644 --- a/src/NUnitTestAdapter/NUnitEngine/UnicodeEscapeHelper.cs +++ b/src/NUnitTestAdapter/NUnitEngine/UnicodeEscapeHelper.cs @@ -6,50 +6,90 @@ namespace NUnit.VisualStudio.TestAdapter.NUnitEngine; internal static class UnicodeEscapeHelper { - public static string UnEscapeUnicodeCharacters(this string text) + private const int EscapeAsciiValue = 0x1B; + + public static string UnEscapeUnicodeColorCodesCharacters(this string text) { - if (text == null) - return null; + if (text == null) + return null; - // Small optimization, if there are no "\u", then there is no need to rewrite the string - var firstEscapeIndex = text.IndexOf("\\u", StringComparison.Ordinal); - if (firstEscapeIndex == -1) - return text; + // Small optimization, if there are no "\u", then there is no need to rewrite the string + var firstEscapeIndex = text.IndexOf("\\u", StringComparison.Ordinal); + if (firstEscapeIndex == -1) + return text; - var stringBuilder = new StringBuilder(text.Substring(0, firstEscapeIndex)); - for (var position = firstEscapeIndex; position < text.Length; position++) + var stringBuilder = new StringBuilder(text.Substring(0, firstEscapeIndex)); + for (var position = firstEscapeIndex; position < text.Length; position++) + { + char c = text[position]; + if (c == '\\' && TryUnEscapeOneCharacter(text, position, out var escapedChar, out var extraCharacterRead)) { - char c = text[position]; - if (c == '\\' && TryUnEscapeOneCharacter(text, position, out var escapedChar, out var extraCharacterRead)) - { - stringBuilder.Append(escapedChar); - position += extraCharacterRead; - } - else - { - stringBuilder.Append(c); - } + stringBuilder.Append(escapedChar); + position += extraCharacterRead; + } + else + { + stringBuilder.Append(c); } - - return stringBuilder.ToString(); } + return stringBuilder.ToString(); + } + private static bool TryUnEscapeOneCharacter(string text, int position, out char escapedChar, out int extraCharacterRead) { - const string unicodeEscapeSample = "u0000"; + const string unicodeEscapeSample = "u0000"; - extraCharacterRead = 0; - escapedChar = '\0'; - if (position + unicodeEscapeSample.Length >= text.Length) - return false; + extraCharacterRead = 0; + escapedChar = '\0'; + if (position + unicodeEscapeSample.Length >= text.Length) + return false; + extraCharacterRead = unicodeEscapeSample.Length; + if (!int.TryParse(text.Substring(position + 2, unicodeEscapeSample.Length - 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var escapeValue)) + return false; - extraCharacterRead = unicodeEscapeSample.Length; - if (!int.TryParse(text.Substring(position + 2, unicodeEscapeSample.Length - 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var escapeValue)) - return false; + // Here we only want to escape color escape character when used in a context of a ANSI color code + // See https://github.com/nunit/nunit3-vs-adapter/issues/1124 for more information. + if (escapeValue != EscapeAsciiValue) + return false; + + if (!IsAnsiColorCodeSequence(text, position + extraCharacterRead + 1)) + return false; + + escapedChar = (char)escapeValue; - escapedChar = (char)escapeValue; + return true; + } - return true; + private static bool IsAnsiColorCodeSequence(string text, int position) + { + var start = false; + while (position < text.Length) + { + var c = text[position++]; + // Look for the begining [ + if (c == '[' && !start) + { + start = true; + continue; + } + + // Found the 'm' at the end + if (c == 'm' && start) + return true; + + // [ was not found + if (!start) + return false; + + // Ignore all number and ; + var isDigit = c is >= '0' and <= '9'; + if (!isDigit && c != ';') + return false; } + + // At the end without the ending 'm' + return false; + } } \ No newline at end of file diff --git a/src/NUnitTestAdapterTests/NUnitEngineTests/UnicodeEscapeHelperTests.cs b/src/NUnitTestAdapterTests/NUnitEngineTests/UnicodeEscapeHelperTests.cs index e13ea6b6..f8d37df6 100644 --- a/src/NUnitTestAdapterTests/NUnitEngineTests/UnicodeEscapeHelperTests.cs +++ b/src/NUnitTestAdapterTests/NUnitEngineTests/UnicodeEscapeHelperTests.cs @@ -5,14 +5,17 @@ namespace NUnit.VisualStudio.TestAdapter.Tests.NUnitEngineTests; public class UnicodeEscapeHelperTests { - [TestCase("\\u001b", "\u001b")] + [TestCase("\\u001b", "\\u001b")] [TestCase("\\u001", "\\u001")] [TestCase("\\u01", "\\u01")] [TestCase("\\u1", "\\u1")] - [TestCase("\\u001b6", "\u001b6")] + [TestCase("\\u001b6", "\\u001b6")] + [TestCase("\\u001b[0m", "\u001b[0m")] + [TestCase("\\u001b[36m", "\u001b[36m")] + [TestCase("\\u001b[48;5;122mTest", "\u001b[48;5;122mTest")] [TestCase("some-text", "some-text")] - public void UnEscapeUnicodeCharacters_ShouldReplaceBackslashU(string value, string expected) + public void UnEscapeUnicodeColorCodesCharactersShouldReplaceBackslashU(string value, string expected) { - Assert.That(value.UnEscapeUnicodeCharacters(), Is.EqualTo(expected)); + Assert.That(value.UnEscapeUnicodeColorCodesCharacters(), Is.EqualTo(expected)); } } \ No newline at end of file