diff --git a/eng/Versions.props b/eng/Versions.props
index 9945a4ee39..4bc6c5bd28 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -19,9 +19,18 @@
1.2.0
- 16.0.461
+
+ 17.8.3
$(MicrosoftBuildFrameworkPackageVersion)
- $(MicrosoftBuildPackageVersion)
+ $(MicrosoftBuildFrameworkPackageVersion)
+ $(MicrosoftBuildFrameworkPackageVersion)
+ $(MicrosoftBuildFrameworkPackageVersion)
+ $(MicrosoftBuildFrameworkPackageVersion)
3.11.0
3.11.0-beta1.23525.2
3.11.0-beta1.23525.2
diff --git a/playground/TestPlatform.Playground/Program.cs b/playground/TestPlatform.Playground/Program.cs
index 875026c24e..142b324cb3 100644
--- a/playground/TestPlatform.Playground/Program.cs
+++ b/playground/TestPlatform.Playground/Program.cs
@@ -24,7 +24,7 @@ static void Main()
{
// This project references TranslationLayer, vstest.console, TestHostProvider, testhost and MSTest1 projects, to make sure
// we build all the dependencies of that are used to run tests via VSTestConsoleWrapper. It then copies the components from
- // their original build locations, to $(TargetDir)\vstest.console directory, and its subfolders to create an executable
+ // their original build locations, to $(TargetDir)\netfx\vstest.console directory, and its subfolders to create an executable
// copy of TestPlatform that is similar to what we ship.
//
// The copying might trigger only on re-build, if you see outdated dependencies, Rebuild this project instead of just Build.
@@ -37,7 +37,7 @@ static void Main()
var here = Path.GetDirectoryName(thisAssemblyPath)!;
var playground = Path.GetFullPath(Path.Combine(here, "..", "..", "..", ".."));
- var console = Path.Combine(here, "vstest.console", "vstest.console.exe");
+ var console = Path.Combine(here, "vstest.console", "netfx", "vstest.console.exe");
var sourceSettings = $$$"""
diff --git a/playground/TestPlatform.Playground/TestPlatform.Playground.csproj b/playground/TestPlatform.Playground/TestPlatform.Playground.csproj
index 9fb644cc4c..d385d07dcc 100644
--- a/playground/TestPlatform.Playground/TestPlatform.Playground.csproj
+++ b/playground/TestPlatform.Playground/TestPlatform.Playground.csproj
@@ -30,7 +30,8 @@
-
+
+
@@ -47,31 +48,53 @@
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.TestPlatform.Build/Microsoft.TestPlatform.Build.csproj b/src/Microsoft.TestPlatform.Build/Microsoft.TestPlatform.Build.csproj
index c29921073e..2cd96cddda 100644
--- a/src/Microsoft.TestPlatform.Build/Microsoft.TestPlatform.Build.csproj
+++ b/src/Microsoft.TestPlatform.Build/Microsoft.TestPlatform.Build.csproj
@@ -25,8 +25,8 @@
-
-
+
+
diff --git a/src/Microsoft.TestPlatform.Build/Microsoft.TestPlatform.targets b/src/Microsoft.TestPlatform.Build/Microsoft.TestPlatform.targets
index dd1dc44ef1..6c4b0b8853 100644
--- a/src/Microsoft.TestPlatform.Build/Microsoft.TestPlatform.targets
+++ b/src/Microsoft.TestPlatform.Build/Microsoft.TestPlatform.targets
@@ -34,9 +34,14 @@ Copyright (c) .NET Foundation. All rights reserved.
-
+
+
+
+
diff --git a/src/Microsoft.TestPlatform.Build/Tasks/VSTestTask2.cs b/src/Microsoft.TestPlatform.Build/Tasks/VSTestTask2.cs
index 9be0e08151..f0f01d3dae 100644
--- a/src/Microsoft.TestPlatform.Build/Tasks/VSTestTask2.cs
+++ b/src/Microsoft.TestPlatform.Build/Tasks/VSTestTask2.cs
@@ -2,8 +2,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
+using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
@@ -45,28 +47,12 @@ public class VSTestTask2 : ToolTask, ITestTask
protected override Encoding StandardErrorEncoding => _disableUtf8ConsoleEncoding ? base.StandardErrorEncoding : Encoding.UTF8;
protected override Encoding StandardOutputEncoding => _disableUtf8ConsoleEncoding ? base.StandardOutputEncoding : Encoding.UTF8;
- private readonly string _testResultSplitter = "++++";
- private readonly string[] _testResultSplitterArray = new[] { "++++" };
+ private readonly string _messageSplitter = "||||";
+ private readonly string[] _messageSplitterArray = new[] { "||||" };
- private readonly string _errorSplitter = "||||";
- private readonly string[] _errorSplitterArray = new[] { "||||" };
-
- private readonly string _fullErrorSplitter = "~~~~";
- private readonly string[] _fullErrorSplitterArray = new[] { "~~~~" };
-
- private readonly string _fullErrorNewlineSplitter = "!!!!";
private readonly bool _disableUtf8ConsoleEncoding;
- protected override string? ToolName
- {
- get
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- return "dotnet.exe";
- else
- return "dotnet";
- }
- }
+ protected override string? ToolName => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet";
public VSTestTask2()
{
@@ -78,77 +64,245 @@ public VSTestTask2()
protected override void LogEventsFromTextOutput(string singleLine, MessageImportance messageImportance)
{
- Debug.WriteLine($"vstestTask2: received output {singleLine}, importance {messageImportance}");
- if (singleLine.StartsWith(_errorSplitter))
+ var useTerminalLogger = true;
+ Debug.WriteLine($"VSTESTTASK2: Received output {singleLine}, importance {messageImportance}");
+ if (TryGetMessage(singleLine, out string name, out string?[] data))
{
- var parts = singleLine.Split(_errorSplitterArray, StringSplitOptions.None);
- if (parts.Length == 5)
+ // See MSBuildLogger.cs for the messages produced.
+ // The number suffix is the amount of parameters that are sent along with the message.
+ switch (name)
{
- var line = 0;
- var file = parts[1];
- var _ = !StringUtils.IsNullOrWhiteSpace(parts[3]) && int.TryParse(parts[2], out line);
- var code = parts[3];
- var message = parts[4];
-
- // Join them with space if both are not null,
- // otherwise use the one that is not null.
- string? error = code != null && message != null
- ? code + " " + message
- : code ?? message;
-
- file ??= string.Empty;
-
- Log.LogError(null, "VSTEST1", null, file, line, 0, 0, 0, error, null);
- return;
- }
- }
- else if (singleLine.StartsWith(_fullErrorSplitter))
- {
- var parts = singleLine.Split(_fullErrorSplitterArray, StringSplitOptions.None);
- if (parts.Length > 1)
- {
- var message = parts[1];
- if (message != null)
- {
- message = message.Replace(_fullErrorNewlineSplitter, Environment.NewLine);
- }
+ // Forward the output we receive as messages.
+ case "output-info1":
+ Log.LogMessage(MessageImportance.Low, data[0]);
+ break;
+ case "output-warning1":
+ Log.LogWarning(data[0]);
+ break;
+ case "output-error1":
+ Log.LogError(data[0]);
+ break;
- string? stackTrace = null;
- if (parts.Length > 2)
- {
- stackTrace = parts[2];
- if (stackTrace != null)
+ case "run-cancel1":
+ case "run-abort1":
+ Log.LogError(data[0]);
+ break;
+ case "run-finish6":
+ // 0 - Localized summary
+ // 1 - total tests
+ // 2 - passed tests
+ // 3 - skipped tests
+ // 4 - failed tests
+ // 5 - duration
+ var summary = data[0];
+ if (useTerminalLogger)
{
- stackTrace = stackTrace.Replace(_fullErrorNewlineSplitter, Environment.NewLine);
+ var message = new ExtendedBuildMessageEventArgs("TLTESTFINISH", summary, null, null, MessageImportance.High)
+ {
+ ExtendedMetadata = new Dictionary
+ {
+ ["total"] = data[1],
+ ["passed"] = data[2],
+ ["skipped"] = data[3],
+ ["failed"] = data[4],
+ ["duration"] = data[5],
+ }
+ };
+
+ BuildEngine.LogMessageEvent(message);
}
- }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, summary);
+ }
+ break;
+ case "test-passed4":
+ {
+ // 0 - localized result indicator
+ // 1 - display name
+ // 2 - duration
+ // 3 - outputs
+ var indicator = data[0];
+ var displayName = data[1];
+ var duration = data[2];
+ var outputs = data[3];
+
+ double durationNumber = 0;
+ var _ = duration != null && double.TryParse(duration, out durationNumber);
- var logMessage = $"{message}{Environment.NewLine}StackTrace:{Environment.NewLine}{stackTrace}";
+ string? formattedDuration = GetFormattedDurationString(TimeSpan.FromMilliseconds(durationNumber));
+ var testResultWithTime = !formattedDuration.IsNullOrEmpty() ? $"{indicator} {displayName} [{formattedDuration}]" : $"{indicator} {displayName}";
+ var n = Environment.NewLine;
- Log.LogMessage(MessageImportance.Low, logMessage);
- return;
+ var testPassed = StringUtils.IsNullOrWhiteSpace(outputs)
+ ? testResultWithTime
+ : $"{testResultWithTime}{n}Outputs:{n}{outputs}";
+
+ if (useTerminalLogger)
+ {
+ var message = new ExtendedBuildMessageEventArgs("TLTESTPASSED", testPassed, null, null, MessageImportance.High)
+ {
+ ExtendedMetadata = new Dictionary
+ {
+ ["localizedResult"] = data[0],
+ ["displayName"] = data[1],
+ }
+ };
+ BuildEngine.LogMessageEvent(message);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, testPassed);
+ }
+ }
+ break;
+ case "test-skipped2":
+ {
+ // 0 - localized result indicator
+ // 1 - display name
+ var indicator = data[0];
+ var displayName = data[1];
+
+ var testSkipped = $"{indicator} {displayName}";
+ if (useTerminalLogger)
+ {
+ var message = new ExtendedBuildMessageEventArgs("TLTESTSKIPPED", testSkipped, null, null, MessageImportance.High)
+ {
+ ExtendedMetadata = new Dictionary
+ {
+ ["localizedResult"] = data[0],
+ ["displayName"] = data[1],
+ }
+ };
+ BuildEngine.LogMessageEvent(message);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, testSkipped);
+ }
+ }
+ break;
+ case "test-failed7":
+ {
+ // 0 - display name
+ // 1 - error message
+ // 2 - error stack trace
+ // 3 - outputs
+ // 4 - file
+ // 5 - line
+ // 6 - place
+ var displayName = data[0]; // Display name
+ var fullErrorMessage = data[1];
+ var fullStackTrace = data[2];
+ var outputs = data[3];
+ var file = data[4];
+ var line = data[5];
+ var place = data[6];
+ var lineNumber = 0;
+ var _ = !StringUtils.IsNullOrWhiteSpace(place) && int.TryParse(line, out lineNumber);
+
+ string? singleLineError = JoinSingleLineAndShorten(place, fullErrorMessage);
+
+ file ??= string.Empty;
+
+ // Report error to msbuild.
+ Log.LogError(null, "VSTEST1", null, file, lineNumber, 0, 0, 0, singleLineError, null);
+
+ // Write the full error to verbose log.
+ //
+ // Log without location information, because it will give better experience in binary log viewer. By default you will see the output shortened followed by "Space: view, Ctrl+C: copy).
+ // Pressing space will navigate to code, instead of showing the full output. To show full output you have to right-click and select View full text.
+ // So we avoid providing source info
+ // Log.LogMessage(null, "VSTEST1", null, file, lineNumber, 0, 0, 0, MessageImportance.Low, $"{displayName}: {fullErrorMessage}{n}Stack Trace:{n}{fullStackTrace}");
+ var n = Environment.NewLine;
+ Log.LogMessage(MessageImportance.Low, $"{displayName}: {fullErrorMessage}{n}Stack Trace:{n}{fullStackTrace}{n}Outputs:{n}{outputs}");
+ }
+ break;
+ case "test-failed3":
+ {
+ // 0 - display name
+ // 1 - error message
+ // 2 - outputs
+ var displayName = data[0];
+ var fullErrorMessage = data[1];
+ var outputs = data[2];
+
+ var singleLineError = JoinSingleLineAndShorten(displayName, fullErrorMessage);
+ Log.LogError(null, "VSTEST1", null, string.Empty, 0, 0, 0, 0, singleLineError);
+ // Write the full error to verbose log.
+ //
+ // Log without location information, because it will give better experience in binary log viewer. By default you will see the output shortened followed by "Space: view, Ctrl+C: copy).
+ // Pressing space will navigate to code, instead of showing the full output. To show full output you have to right-click and select View full text.
+ // So we avoid providing source info
+ // var n = Environment.NewLine;
+ // Log.LogMessage(null, "VSTEST1", null, string.Empty, 0, 0, 0, 0, MessageImportance.Low, $"{displayName}: {fullErrorMessage}");
+ var n = Environment.NewLine;
+ Log.LogMessage(MessageImportance.Low, $"{displayName}: {fullErrorMessage}{n}Outputs:{n}{outputs}");
+ }
+ break;
+ default:
+ // If we get other message, forward it to binary log. In the future we can ignore this or remove the prefix, but now I want to see it.
+ Log.LogMessage(MessageImportance.Low, $"Unhandled message: {singleLine}");
+ break;
}
}
- else if (singleLine.StartsWith(_testResultSplitter))
+ else
{
- var parts = singleLine.Split(_testResultSplitterArray, StringSplitOptions.None);
- if (parts.Length == 3)
- {
- var outcome = parts[1];
- var testName = parts[2];
+ // We will receive output, such as vstest version, forward it to msbuild log.
+
+ // DO NOT call the base, it parses out the output, and if it sees "error" in any place it will log it as error
+ // we don't want this, we only want to log errors from the text messages we receive that start error splitter.
+ // base.LogEventsFromTextOutput(singleLine, messageImportance);
- Log.LogMessage(MessageImportance.Low, $"{outcome} {testName}");
- return;
+ if (!StringUtils.IsNullOrWhiteSpace(singleLine))
+ {
+ Log.LogMessage(MessageImportance.Low, singleLine);
}
}
- else
+ }
+
+ private static string? JoinSingleLineAndShorten(string? first, string? second)
+ {
+ // Join them with space if both are not null,
+ // otherwise use the one that is not null.
+ return first != null && second != null
+ ? SingleLineAndShorten(first) + " " + SingleLineAndShorten(second)
+ : SingleLineAndShorten(first) ?? SingleLineAndShorten(second);
+ }
+
+ private static string AsForwardedMessage(string?[] data)
+ {
+ return string.Join("||||", data.Select(CleanSeperator));
+ }
+
+ private static string? CleanSeperator(string? text)
+ {
+ return text == null ? null : text.Replace("||||", "___");
+ }
+
+ private static string? SingleLineAndShorten(string? text)
+ {
+ if (text == null)
+ {
+ return null;
+ }
+
+ return text.Length <= 1000 ? text : text.Substring(0, 1000).Replace('\r', ' ').Replace('\n', ' ');
+ }
+
+ private bool TryGetMessage(string singleLine, out string name, out string?[] data)
+ {
+ if (singleLine.StartsWith(_messageSplitter))
{
- Log.LogMessage(MessageImportance.Low, singleLine);
+ var parts = singleLine.Split(_messageSplitterArray, StringSplitOptions.None);
+ name = parts[1];
+ data = parts.Skip(2).Take(parts.Length).Select(p => p == null ? null : p.Replace("~~~~", "\r").Replace("!!!!", "\n")).ToArray();
+ return true;
}
- // Do not call the base, it parses out the output, and if it sees "error" in any place it will log it as error
- // we don't want this, we only want to log errors from the text messages we receive that start error splitter.
- // base.LogEventsFromTextOutput(singleLine, messageImportance);
+ name = string.Empty;
+ data = Array.Empty();
+ return false;
}
protected override string? GenerateCommandLineCommands()
@@ -190,4 +344,50 @@ protected override void LogEventsFromTextOutput(string singleLine, MessageImport
return null;
}
+
+ ///
+ /// Converts the time span format to readable string.
+ ///
+ ///
+ ///
+ internal static string? GetFormattedDurationString(TimeSpan duration)
+ {
+ if (duration == default)
+ {
+ return null;
+ }
+
+ var time = new List();
+ if (duration.Days > 0)
+ {
+ time.Add("> 1d");
+ }
+ else
+ {
+ if (duration.Hours > 0)
+ {
+ time.Add(duration.Hours + "h");
+ }
+
+ if (duration.Minutes > 0)
+ {
+ time.Add(duration.Minutes + "m");
+ }
+
+ if (duration.Hours == 0)
+ {
+ if (duration.Seconds > 0)
+ {
+ time.Add(duration.Seconds + "s");
+ }
+
+ if (duration.Milliseconds > 0 && duration.Minutes == 0)
+ {
+ time.Add(duration.Milliseconds + "ms");
+ }
+ }
+ }
+
+ return time.Count == 0 ? "< 1ms" : string.Join(" ", time);
+ }
}
diff --git a/src/vstest.console/Internal/MSBuildLogger.cs b/src/vstest.console/Internal/MSBuildLogger.cs
index e5f053b99c..214010a248 100644
--- a/src/vstest.console/Internal/MSBuildLogger.cs
+++ b/src/vstest.console/Internal/MSBuildLogger.cs
@@ -3,10 +3,12 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
+using System.Text;
using System.Text.RegularExpressions;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
@@ -57,13 +59,9 @@ public void Initialize(TestLoggerEvents events, string testRunDirectory)
Output ??= ConsoleOutput.Instance;
// Register for the events.
- // events.TestRunMessage += TestMessageHandler;
+ events.TestRunMessage += TestMessageHandler;
events.TestResult += TestResultHandler;
events.TestRunComplete += TestRunCompleteHandler;
- // events.TestRunStart += TestRunStartHandler;
-
- // Register for the discovery events.
- // events.DiscoveryMessage += TestMessageHandler;
}
public void Initialize(TestLoggerEvents events, Dictionary parameters)
@@ -71,25 +69,66 @@ public void Initialize(TestLoggerEvents events, Dictionary para
Initialize(events, string.Empty);
}
+ private void TestMessageHandler(object? sender, TestRunMessageEventArgs e)
+ {
+ switch (e.Level)
+ {
+ case TestMessageLevel.Informational:
+ SendMessage($"output-info", e.Message);
+ break;
+ case TestMessageLevel.Warning:
+ SendMessage($"output-warning", e.Message);
+ break;
+ case TestMessageLevel.Error:
+ SendMessage($"output-error", e.Message);
+ break;
+ }
+ }
+
private void TestRunCompleteHandler(object? sender, TestRunCompleteEventArgs e)
{
TPDebug.Assert(Output != null, "Initialize should have been called");
if (e.IsCanceled)
{
- Output.Error(false, CommandLineResources.TestRunCanceled);
+ SendMessage("run-cancel", CommandLineResources.TestRunCanceled);
}
else if (e.IsAborted)
{
if (e.Error == null)
{
- Output.Error(false, CommandLineResources.TestRunAborted);
+ SendMessage("run-abort", CommandLineResources.TestRunAborted);
}
else
{
- Output.Error(false, CommandLineResources.TestRunAbortedWithError, e.Error);
+ SendMessage("run-abort", string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestRunAbortedWithError, e.Error));
}
}
+ else
+ {
+ var total = e.TestRunStatistics?.ExecutedTests ?? 0;
+ var passed = e.TestRunStatistics?[TestOutcome.Passed] ?? 0;
+ var skipped = e.TestRunStatistics?[TestOutcome.Skipped] ?? 0;
+ var failed = e.TestRunStatistics?[TestOutcome.Failed] ?? 0;
+ var time = e.ElapsedTimeInRunningTests.TotalMilliseconds;
+
+ var summary = string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestRunSummary,
+ (failed > 0 ? CommandLineResources.FailedTestIndicator : CommandLineResources.PassedTestIndicator) + "!",
+ failed,
+ passed,
+ skipped,
+ total,
+ $"[{GetFormattedDurationString(e.ElapsedTimeInRunningTests)}]"
+ );
+
+ SendMessage("run-finish",
+ summary,
+ total.ToString(CultureInfo.InvariantCulture),
+ passed.ToString(CultureInfo.InvariantCulture),
+ skipped.ToString(CultureInfo.InvariantCulture),
+ failed.ToString(CultureInfo.InvariantCulture),
+ time.ToString(CultureInfo.InvariantCulture));
+ }
}
private void TestResultHandler(object? sender, TestResultEventArgs e)
@@ -100,16 +139,17 @@ private void TestResultHandler(object? sender, TestResultEventArgs e)
switch (e.Result.Outcome)
{
case TestOutcome.Passed:
+ SendMessage("test-passed",
+ CommandLineResources.PassedTestIndicator,
+ e.Result.TestCase.DisplayName,
+ e.Result.Duration.TotalMilliseconds.ToString(CultureInfo.InvariantCulture),
+ FormatOutputs(e.Result));
+ break;
case TestOutcome.Skipped:
-
- var test = e.Result.TestCase.DisplayName;
- var outcome = e.Result.Outcome == TestOutcome.Passed
- ? CommandLineResources.PassedTestIndicator
- : CommandLineResources.SkippedTestIndicator;
- var info = $"++++{outcome}++++{ReplacePlusSeparator(test)}";
-
- Debug.WriteLine(">>>>MESSAGE:" + info);
- Output.Information(false, info);
+ SendMessage("test-skipped",
+ CommandLineResources.PassedTestIndicator,
+ e.Result.TestCase.DisplayName,
+ e.Result.Duration.TotalMilliseconds.ToString(CultureInfo.InvariantCulture));
break;
case TestOutcome.Failed:
var result = e.Result;
@@ -117,15 +157,6 @@ private void TestResultHandler(object? sender, TestResultEventArgs e)
Debug.WriteLine(">>>>STK:" + result.ErrorStackTrace);
if (!StringUtils.IsNullOrWhiteSpace(result.ErrorStackTrace))
{
- var maxLength = 1000;
- string? error = null;
- if (result.ErrorMessage != null)
- {
- // Do not use environment.newline here, we want to replace also \n on Windows.
- var oneLineMessage = result.ErrorMessage.Replace("\n", " ").Replace("\r", " ");
- error = oneLineMessage.Length > maxLength ? oneLineMessage.Substring(0, maxLength) : oneLineMessage;
- }
-
string? stackFrame = null;
var stackFrames = Regex.Split(result.ErrorStackTrace, Environment.NewLine);
string? line = null;
@@ -161,19 +192,13 @@ private void TestResultHandler(object? sender, TestResultEventArgs e)
}
}
- place = $"({result.TestCase.DisplayName}) {place}";
- var message = $"||||{ReplacePipeSeparator(file)}||||{line}||||{ReplacePipeSeparator(place)}||||{ReplacePipeSeparator(error)}";
-
- Debug.WriteLine(">>>>MESSAGE:" + message);
- Output.Error(false, message);
-
- var fullError = $"~~~~{ReplaceTildaSeparator(result.ErrorMessage)}~~~~{ReplaceTildaSeparator(result.ErrorStackTrace)}";
- Output.Information(false, fullError);
+ var outputs = FormatOutputs(result);
+ SendMessage("test-failed", result.DisplayName, result.ErrorMessage, result.ErrorStackTrace, outputs, file, line, place);
return;
}
else
{
- Output.Error(false, result.DisplayName?.Replace(Environment.NewLine, " ") ?? string.Empty);
+ SendMessage("test-failed", result.DisplayName, result.ErrorMessage);
}
break;
@@ -197,49 +222,157 @@ private static bool TryGetStackFrameLocation(string stackFrame, out string? line
line = match.Groups["line"].Value;
}
- Debug.WriteLine($">>>> {(match.Success ? "MATCH" : "NOMATCH")} {stackFrame}");
-
return match.Success;
}
+ ///
+ /// Writes message to standard output, with the name of the message followed by the number of
+ /// parameters. With each parameter delimited by '||||', and newlines replaced with ~~~~ and !!!!.
+ /// Such as:
+ /// ||||run-start1||||s:\t\mstest97\bin\Debug\net8.0\mstest97.dll
+ /// ||||test-failed6||||TestMethod5||||Assert.IsTrue failed. |||| at mstest97.UnitTest1.TestMethod5() in s:\t\mstest97\UnitTest1.cs:line 27~~~~!!!! at Syste...
+ ///
+ ///
+ ///
+ private static void SendMessage(string name, params string?[] data)
+ {
+ TPDebug.Assert(Output != null, "Initialize should have been called");
+
+ var message = FormatMessage(name, data);
+ Debug.WriteLine($"MSBUILDLOGGER: {message}");
+ Output.Information(appendPrefix: false, FormatMessage(name, data));
+ }
+
+ private static string FormatMessage(string name, params string?[] data)
+ {
+ return $"||||{name}{data.Length}||||{string.Join("||||", data.Select(Escape))}";
+ }
- private static string? ReplacePipeSeparator(string? text)
+ private static string? Escape(string? input)
{
- if (text == null)
+ if (input == null)
{
return null;
}
- // Remove any occurrence of message splitter.
- return text.Replace("||||", "____");
+ return input
+ // Cleanup characters that we are using ourselves to delimit the message
+ .Replace("||||", "____").Replace("~~~~", "____").Replace("!!!!", "____")
+ // Replace new line characters that would change how the message is consumed.
+ .Replace("\r", "~~~~").Replace("\n", "!!!!");
}
- private static string? ReplacePlusSeparator(string? text)
+ ///
+ /// Collects all the messages of a particular category(Standard Output/Standard Error/Debug Traces) and returns a collection.
+ ///
+ private static Collection GetTestMessages(Collection messages, string requiredCategory)
+ {
+ var selectedMessages = messages.Where(msg => msg.Category.Equals(requiredCategory, StringComparison.OrdinalIgnoreCase));
+ var requiredMessageCollection = new Collection(selectedMessages.ToList());
+ return requiredMessageCollection;
+ }
+
+ private static string FormatOutputs(TestResult result)
{
- if (text == null)
+ var stringBuilder = new StringBuilder();
+ var testResultPrefix = " ";
+ TPDebug.Assert(result != null, "a null result can not be displayed");
+
+ var stdOutMessagesCollection = GetTestMessages(result.Messages, TestResultMessage.StandardOutCategory);
+ if (stdOutMessagesCollection.Count > 0)
{
- return null;
+ stringBuilder.AppendLine(testResultPrefix + CommandLineResources.StdOutMessagesBanner);
+ AddFormattedOutput(stdOutMessagesCollection, stringBuilder);
+ }
+
+ var stdErrMessagesCollection = GetTestMessages(result.Messages, TestResultMessage.StandardErrorCategory);
+ if (stdErrMessagesCollection.Count > 0)
+ {
+ stringBuilder.AppendLine(testResultPrefix + CommandLineResources.StdErrMessagesBanner);
+ AddFormattedOutput(stdErrMessagesCollection, stringBuilder);
+
+ }
+
+ var dbgTrcMessagesCollection = GetTestMessages(result.Messages, TestResultMessage.DebugTraceCategory);
+ if (dbgTrcMessagesCollection.Count > 0)
+ {
+ stringBuilder.AppendLine(testResultPrefix + CommandLineResources.DbgTrcMessagesBanner);
+ AddFormattedOutput(dbgTrcMessagesCollection, stringBuilder);
}
- // Remove any occurrence of message splitter.
- return text.Replace("++++", "____");
+ var addnlInfoMessagesCollection = GetTestMessages(result.Messages, TestResultMessage.AdditionalInfoCategory);
+ if (addnlInfoMessagesCollection.Count > 0)
+ {
+ stringBuilder.AppendLine(testResultPrefix + CommandLineResources.AddnlInfoMessagesBanner);
+ AddFormattedOutput(addnlInfoMessagesCollection, stringBuilder);
+ }
+
+ return stringBuilder.ToString();
}
- private static string? ReplaceTildaSeparator(string? text)
+ private static void AddFormattedOutput(Collection testMessageCollection, StringBuilder stringBuilder)
{
- if (text == null)
+ string testMessageFormattingPrefix = " ";
+ if (testMessageCollection == null)
{
- return null;
+ return;
}
- // Remove any occurrence of message splitter.
- text = text.Replace("~~~~", "____");
- // Clean up any occurrence of newline splitter.
- text = text.Replace("!!!!", "____");
- // Replace newlines with newline splitter.
- text = text.Replace(Environment.NewLine, "!!!!");
+ foreach (var message in testMessageCollection)
+ {
+ var prefix = string.Format(CultureInfo.CurrentCulture, "{0}{1}", Environment.NewLine, testMessageFormattingPrefix);
+ var messageText = message.Text?.Replace(Environment.NewLine, prefix).TrimEnd(testMessageFormattingPrefix.ToCharArray());
- return text;
+ if (!messageText.IsNullOrWhiteSpace())
+ {
+ stringBuilder.AppendFormat(CultureInfo.CurrentCulture, "{0}{1}", testMessageFormattingPrefix, messageText);
+ }
+ }
}
+ ///
+ /// Converts the time span format to readable string.
+ ///
+ ///
+ ///
+ internal static string GetFormattedDurationString(TimeSpan duration)
+ {
+ if (duration == default)
+ {
+ return "< 1ms";
+ }
+
+ var time = new List();
+ if (duration.Days > 0)
+ {
+ time.Add("> 1d");
+ }
+ else
+ {
+ if (duration.Hours > 0)
+ {
+ time.Add(duration.Hours + "h");
+ }
+
+ if (duration.Minutes > 0)
+ {
+ time.Add(duration.Minutes + "m");
+ }
+
+ if (duration.Hours == 0)
+ {
+ if (duration.Seconds > 0)
+ {
+ time.Add(duration.Seconds + "s");
+ }
+
+ if (duration.Milliseconds > 0 && duration.Minutes == 0)
+ {
+ time.Add(duration.Milliseconds + "ms");
+ }
+ }
+ }
+
+ return time.Count == 0 ? "< 1ms" : string.Join(" ", time);
+ }
}
diff --git a/test/Microsoft.TestPlatform.Build.UnitTests/Microsoft.TestPlatform.Build.UnitTests.csproj b/test/Microsoft.TestPlatform.Build.UnitTests/Microsoft.TestPlatform.Build.UnitTests.csproj
index 29604a5388..1e5c859108 100644
--- a/test/Microsoft.TestPlatform.Build.UnitTests/Microsoft.TestPlatform.Build.UnitTests.csproj
+++ b/test/Microsoft.TestPlatform.Build.UnitTests/Microsoft.TestPlatform.Build.UnitTests.csproj
@@ -6,8 +6,8 @@
- net6.0;net48
- Exe
+ net8.0;net48
+ Exe
Microsoft.TestPlatform.Build.UnitTests