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