diff --git a/src/vstest.console/Internal/ConsoleLogger.cs b/src/vstest.console/Internal/ConsoleLogger.cs
index 4ef52ff60a..846ad79c7e 100644
--- a/src/vstest.console/Internal/ConsoleLogger.cs
+++ b/src/vstest.console/Internal/ConsoleLogger.cs
@@ -11,14 +11,13 @@ namespace Microsoft.VisualStudio.TestPlatform.CommandLine.Internal
using System.Globalization;
using System.Linq;
using System.Text;
-
+ using System.Xml.XPath;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
using Microsoft.VisualStudio.TestPlatform.Utilities;
-
+ using NuGet.Frameworks;
using CommandLineResources = Resources.Resources;
-
///
/// Logger for sending output to the console.
/// All the console logger messages prints to Standard Output with respective color, except OutputLevel.Error messages
@@ -81,6 +80,16 @@ internal class ConsoleLogger : ITestLoggerWithParameters
///
public const string ExecutionIdPropertyIdentifier = "ExecutionId";
+ // Figure out the longest result string (+1 for ! where applicable), so we don't
+ // get misaligned output on non-english systems
+ private static int LongestResultIndicator = new[]
+ {
+ CommandLineResources.FailedTestIndicator.Length + 1,
+ CommandLineResources.PassedTestIndicator.Length + 1,
+ CommandLineResources.SkippedTestIndicator.Length + 1,
+ CommandLineResources.None.Length
+ }.Max();
+
#endregion
internal enum Verbosity
@@ -104,7 +113,11 @@ internal enum Verbosity
#endif
private bool testRunHasErrorMessages = false;
- private ConcurrentDictionary leafExecutionIdAndTestOutcomePairDictionary = new ConcurrentDictionary();
+
+ ///
+ /// Framework on which the test runs.
+ ///
+ private string targetFramework;
#endregion
@@ -148,6 +161,12 @@ protected static IOutput Output
///
public Verbosity VerbosityLevel => verbosityLevel;
+ ///
+ /// Tracks leaf test outcomes per source. This is needed to correctly count hierarchical tests as well as
+ /// tracking counts per source for the minimal and quiet output.
+ ///
+ private ConcurrentDictionary leafTestResults { get; set; }
+
#endregion
#region ITestLoggerWithParameters
@@ -183,6 +202,7 @@ public void Initialize(TestLoggerEvents events, string testRunDirectory)
// Register for the discovery events.
events.DiscoveryMessage += this.TestMessageHandler;
+ this.leafTestResults = new ConcurrentDictionary();
// TODO Get changes from https://github.com/Microsoft/vstest/pull/1111/
// events.DiscoveredTests += DiscoveredTestsHandler;
@@ -218,6 +238,9 @@ public void Initialize(TestLoggerEvents events, Dictionary param
bool.TryParse(enableProgress, out EnableProgress);
}
+ parameters.TryGetValue(DefaultLoggerParameterNames.TargetFramework, out this.targetFramework);
+ this.targetFramework = !string.IsNullOrEmpty(this.targetFramework) ? NuGetFramework.Parse(this.targetFramework).GetShortFolderName() : this.targetFramework;
+
Initialize(events, String.Empty);
}
#endregion
@@ -410,7 +433,6 @@ private void TestRunStartHandler(object sender, TestRunStartEventArgs e)
ValidateArg.NotNull(e, "e");
// Print all test containers.
- Output.WriteLine(string.Empty, OutputLevel.Information);
Output.WriteLine(string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestSourcesDiscovered, CommandLineOptions.Instance.Sources.Count()), OutputLevel.Information);
if (verbosityLevel == Verbosity.Detailed)
{
@@ -510,12 +532,21 @@ private void TestResultHandler(object sender, TestResultEventArgs e)
var executionId = GetExecutionId(e.Result);
var parentExecutionId = GetParentExecutionId(e.Result);
- if (parentExecutionId != Guid.Empty && leafExecutionIdAndTestOutcomePairDictionary.ContainsKey(parentExecutionId))
+ if (parentExecutionId != Guid.Empty)
{
- leafExecutionIdAndTestOutcomePairDictionary.TryRemove(parentExecutionId, out _);
+ // Not checking the result value.
+ // This would return false if the id did not exist,
+ // or true if it did exist. In either case the id is not in the dictionary
+ // which is our goal.
+ leafTestResults.TryRemove(parentExecutionId, out _);
}
- leafExecutionIdAndTestOutcomePairDictionary.TryAdd(executionId, e.Result.Outcome);
+ if (!leafTestResults.TryAdd(executionId, e.Result))
+ {
+ // This would happen if the key already exists. This should not happen, because we are
+ // inserting by GUID key, so this would mean an error in our code.
+ throw new InvalidOperationException($"ExecutionId {executionId} already exists.");
+ };
switch (e.Result.Outcome)
{
@@ -618,28 +649,28 @@ private string GetFormattedDurationString(TimeSpan duration)
var time = new List();
if (duration.Hours > 0)
{
- time.Add(duration.Hours + "h");
+ time.Add(duration.Hours + " h");
}
if (duration.Minutes > 0)
{
- time.Add(duration.Minutes + "m");
+ time.Add(duration.Minutes + " m");
}
if (duration.Hours == 0)
{
if (duration.Seconds > 0)
{
- time.Add(duration.Seconds + "s");
+ time.Add(duration.Seconds + " s");
}
- if (duration.Milliseconds > 0 && duration.Minutes == 0)
+ if (duration.Milliseconds > 0 && duration.Minutes == 0 && duration.Seconds == 0)
{
- time.Add(duration.Milliseconds + "ms");
+ time.Add(duration.Milliseconds + " ms");
}
}
- return time.Count == 0 ? "< 1ms" : string.Join(" ", time);
+ return time.Count == 0 ? "< 1 ms" : string.Join(" ", time);
}
///
@@ -647,12 +678,12 @@ private string GetFormattedDurationString(TimeSpan duration)
///
private void TestRunCompleteHandler(object sender, TestRunCompleteEventArgs e)
{
- var testsTotal = 0;
- var testsPassed = 0;
- var testsFailed = 0;
- var testsSkipped = 0;
// Stop the progress indicator as we are about to print the summary
this.progressIndicator?.Stop();
+ var passedTests = 0;
+ var failedTests = 0;
+ var skippedTests = 0;
+ var totalTests = 0;
Output.WriteLine(string.Empty, OutputLevel.Information);
// Printing Run-level Attachments
@@ -670,23 +701,138 @@ private void TestRunCompleteHandler(object sender, TestRunCompleteEventArgs e)
}
}
- foreach (KeyValuePair entry in leafExecutionIdAndTestOutcomePairDictionary)
+ var leafTestResultsPerSource = this.leafTestResults.Select(p => p.Value).GroupBy(r => r.TestCase.Source);
+ foreach (var sd in leafTestResultsPerSource)
{
- testsTotal++;
- switch (entry.Value)
+ var source = sd.Key;
+ var sourceSummary = new SourceSummary();
+
+ foreach (var result in sd.ToArray())
{
- case TestOutcome.Failed:
- testsFailed++;
- break;
- case TestOutcome.Passed:
- testsPassed++;
- break;
- case TestOutcome.Skipped:
- testsSkipped++;
- break;
- default:
- break;
+ sourceSummary.Duration += result.Duration;
+ switch (result.Outcome)
+ {
+ case TestOutcome.Passed:
+ sourceSummary.TotalTests++;
+ sourceSummary.PassedTests++;
+ break;
+ case TestOutcome.Failed:
+ sourceSummary.TotalTests++;
+ sourceSummary.FailedTests++;
+ break;
+ case TestOutcome.Skipped:
+ sourceSummary.TotalTests++;
+ sourceSummary.SkippedTests++;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (verbosityLevel == Verbosity.Quiet || verbosityLevel == Verbosity.Minimal)
+ {
+ TestOutcome sourceOutcome = TestOutcome.None;
+ if (sourceSummary.FailedTests > 0)
+ {
+ sourceOutcome = TestOutcome.Failed;
+ }
+ else if (sourceSummary.PassedTests > 0)
+ {
+ sourceOutcome = TestOutcome.Passed;
+ }
+ else if (sourceSummary.SkippedTests > 0)
+ {
+ sourceOutcome = TestOutcome.Skipped;
+ }
+
+
+ string resultString;
+ switch (sourceOutcome)
+ {
+ case TestOutcome.Failed:
+ resultString = (CommandLineResources.FailedTestIndicator + "!").PadRight(LongestResultIndicator);
+ break;
+ case TestOutcome.Passed:
+ resultString = (CommandLineResources.PassedTestIndicator + "!").PadRight(LongestResultIndicator);
+ break;
+ case TestOutcome.Skipped:
+ resultString = (CommandLineResources.SkippedTestIndicator + "!").PadRight(LongestResultIndicator);
+ break;
+ default:
+ resultString = CommandLineResources.None.PadRight(LongestResultIndicator);
+ break;
+ };
+
+ var failed = sourceSummary.FailedTests.ToString().PadLeft(5);
+ var passed = sourceSummary.PassedTests.ToString().PadLeft(5);
+ var skipped = sourceSummary.SkippedTests.ToString().PadLeft(5);
+ var total = sourceSummary.TotalTests.ToString().PadLeft(5);
+
+
+ var frameworkString = string.IsNullOrEmpty(targetFramework)
+ ? string.Empty
+ : $"({targetFramework})";
+
+ var duration = GetFormattedDurationString(sourceSummary.Duration);
+ var sourceName = sd.Key.Split('\\').Last();
+
+ var outputLine = string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestRunSummary,
+ resultString,
+ failed,
+ passed,
+ skipped,
+ total,
+ duration,
+ sourceName,
+ frameworkString);
+
+
+ ConsoleColor? color = null;
+ if (sourceOutcome == TestOutcome.Failed)
+ {
+ color = ConsoleColor.Red;
+ }
+ else if (sourceOutcome == TestOutcome.Passed)
+ {
+ color = ConsoleColor.Green;
+ }
+ else if (sourceOutcome == TestOutcome.Skipped)
+ {
+ color = ConsoleColor.Yellow;
+ }
+
+ if (color != null)
+ {
+ Output.Write(outputLine, OutputLevel.Information, color.Value);
+ }
+ else
+ {
+ Output.Write(outputLine, OutputLevel.Information);
+ }
+
+ Output.Information(false, CommandLineResources.TestRunSummaryAssemblyAndFramework,
+ sourceName,
+ frameworkString);
}
+
+ passedTests += sourceSummary.PassedTests;
+ failedTests += sourceSummary.FailedTests;
+ skippedTests += sourceSummary.SkippedTests;
+ totalTests += sourceSummary.TotalTests;
+ }
+
+ if (verbosityLevel == Verbosity.Quiet || verbosityLevel == Verbosity.Minimal)
+ {
+ if (e.IsCanceled)
+ {
+ Output.Error(false, CommandLineResources.TestRunCanceled);
+ }
+ else if (e.IsAborted)
+ {
+ Output.Error(false, CommandLineResources.TestRunAborted);
+ }
+
+ return;
}
if (e.IsCanceled)
@@ -697,36 +843,36 @@ private void TestRunCompleteHandler(object sender, TestRunCompleteEventArgs e)
{
Output.Error(false, CommandLineResources.TestRunAborted);
}
- else if (testsFailed > 0 || this.testRunHasErrorMessages)
+ else if (failedTests > 0 || this.testRunHasErrorMessages)
{
Output.Error(false, CommandLineResources.TestRunFailed);
}
- else if (testsTotal > 0)
+ else if (totalTests > 0)
{
Output.Information(false, ConsoleColor.Green, CommandLineResources.TestRunSuccessful);
}
// Output a summary.
- if (testsTotal > 0)
+ if (totalTests > 0)
{
string totalTestsformat = (e.IsAborted || e.IsCanceled) ? CommandLineResources.TestRunSummaryForCanceledOrAbortedRun : CommandLineResources.TestRunSummaryTotalTests;
- Output.Information(false, string.Format(CultureInfo.CurrentCulture, totalTestsformat, testsTotal));
+ Output.Information(false, string.Format(CultureInfo.CurrentCulture, totalTestsformat, totalTests));
- if (testsPassed > 0)
+ if (passedTests > 0)
{
- Output.Information(false, ConsoleColor.Green, string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestRunSummaryPassedTests, testsPassed));
+ Output.Information(false, ConsoleColor.Green, string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestRunSummaryPassedTests, passedTests));
}
- if (testsFailed > 0)
+ if (failedTests > 0)
{
- Output.Information(false, ConsoleColor.Red, string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestRunSummaryFailedTests, testsFailed));
+ Output.Information(false, ConsoleColor.Red, string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestRunSummaryFailedTests, failedTests));
}
- if (testsSkipped > 0)
+ if (skippedTests > 0)
{
- Output.Information(false, ConsoleColor.Yellow, string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestRunSummarySkippedTests, testsSkipped));
+ Output.Information(false, ConsoleColor.Yellow, string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestRunSummarySkippedTests, skippedTests));
}
}
- if (testsTotal > 0)
+ if (totalTests > 0)
{
if (e.ElapsedTimeInRunningTests.Equals(TimeSpan.Zero))
{
diff --git a/src/vstest.console/Internal/SourceSummary.cs b/src/vstest.console/Internal/SourceSummary.cs
new file mode 100644
index 0000000000..d0f228b6de
--- /dev/null
+++ b/src/vstest.console/Internal/SourceSummary.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.VisualStudio.TestPlatform.CommandLine.Internal
+{
+ using System;
+
+ ///
+ /// Summary of test results per source.
+ ///
+ internal class SourceSummary
+ {
+ ///
+ /// Total tests of a test run.
+ ///
+ public int TotalTests { get; set; }
+
+ ///
+ /// Passed tests of a test run.
+ ///
+ public int PassedTests { get; set; }
+
+ ///
+ /// Failed tests of a test run.
+ ///
+ public int FailedTests { get; set; }
+
+ ///
+ /// Skipped tests of a test run.
+ ///
+ public int SkippedTests { get; set; }
+
+ ///
+ /// Duration of the test run.
+ ///
+ public TimeSpan Duration { get; set; }
+ }
+}
diff --git a/src/vstest.console/Resources/Resources.Designer.cs b/src/vstest.console/Resources/Resources.Designer.cs
index db78353506..d4db636f10 100644
--- a/src/vstest.console/Resources/Resources.Designer.cs
+++ b/src/vstest.console/Resources/Resources.Designer.cs
@@ -1028,6 +1028,15 @@ internal static string NonDefaultFrameworkAndOrArchDetected {
}
}
+ ///
+ /// Looks up a localized string similar to None.
+ ///
+ internal static string None {
+ get {
+ return ResourceManager.GetString("None", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to App package '{0}' does not has test executor entry point. For running unit tests for Windows Store apps, create app package using Windows Store app Unit Test Library project..
///
@@ -1567,6 +1576,24 @@ internal static string TestRunSuccessful {
}
}
+ ///
+ /// Looks up a localized string similar to {0} - Failed: {1}, Passed: {2}, Skipped: {3}, Total: {4}, Duration: {5}.
+ ///
+ internal static string TestRunSummary {
+ get {
+ return ResourceManager.GetString("TestRunSummary", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to - {0} {1}.
+ ///
+ internal static string TestRunSummaryAssemblyAndFramework {
+ get {
+ return ResourceManager.GetString("TestRunSummaryAssemblyAndFramework", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Failed: {0}.
///
diff --git a/src/vstest.console/Resources/Resources.resx b/src/vstest.console/Resources/Resources.resx
index 5fb7d82647..1e4808fbf4 100644
--- a/src/vstest.console/Resources/Resources.resx
+++ b/src/vstest.console/Resources/Resources.resx
@@ -734,10 +734,19 @@
A total of {0} test files matched the specified pattern.
+
+ {0} - Failed: {1}, Passed: {2}, Skipped: {3}, Total: {4}, Duration: {5}
+
The test run parameter argument '{0}' is invalid. Please use the format below.
Format: TestRunParameters.Parameter(name=\"<name>\", value=\"<value>\")
+
+ None
+
+
+ - {0} {1}
+
Collecting hang dumps by option CollectDump with TestTimeout for Blame is not supported for this platform.
diff --git a/src/vstest.console/Resources/xlf/Resources.cs.xlf b/src/vstest.console/Resources/xlf/Resources.cs.xlf
index 3cafe3c695..a9c537603d 100644
--- a/src/vstest.console/Resources/xlf/Resources.cs.xlf
+++ b/src/vstest.console/Resources/xlf/Resources.cs.xlf
@@ -1668,6 +1668,21 @@
Shromažďování výpisů stavu systému při zablokování pomocí možnosti CollectDump s TestTimeout pro Blame se pro tuto platformu nepodporuje.
+
+
+ {0} - Failed: {1}, Passed: {2}, Skipped: {3}, Total: {4}, Duration: {5}
+
+
+
+
+ None
+
+
+
+
+ - {0} {1}
+
+