From e53ae55883d0199b7102eac027c19d2af53a06f8 Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Mon, 25 Nov 2024 18:49:38 +0100 Subject: [PATCH 01/18] Terminal progress --- samples/Playground/Tests.cs | 6 + .../OutputDevice/Terminal/AnsiTerminal.cs | 17 +- .../Terminal/AnsiTerminalTestProgressFrame.cs | 232 ++++++++++++------ .../OutputDevice/Terminal/NonAnsiTerminal.cs | 2 +- .../Terminal/TerminalTestReporter.cs | 25 +- .../OutputDevice/Terminal/TestDetailState.cs | 25 ++ .../Terminal/TestProgressState.cs | 9 +- .../TestProgressStateAwareTerminal.cs | 2 +- .../OutputDevice/TerminalOutputDevice.cs | 7 +- 9 files changed, 231 insertions(+), 94 deletions(-) create mode 100644 src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs diff --git a/samples/Playground/Tests.cs b/samples/Playground/Tests.cs index eb480d750f..a00c9eef69 100644 --- a/samples/Playground/Tests.cs +++ b/samples/Playground/Tests.cs @@ -24,4 +24,10 @@ public class TestClass public void Test3() { } + + [TestMethod] + public void Test4() + { + Thread.Sleep(15_000); + } } diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminal.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminal.cs index 9c9341357c..21a664fbb4 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminal.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminal.cs @@ -45,7 +45,7 @@ internal sealed class AnsiTerminal : ITerminal private readonly bool _useBusyIndicator; private readonly StringBuilder _stringBuilder = new(); private bool _isBatching; - private AnsiTerminalTestProgressFrame _currentFrame = new(Array.Empty(), 0, 0); + private AnsiTerminalTestProgressFrame _currentFrame = new(0, 0); public AnsiTerminal(IConsole console, string? baseDirectory) { @@ -274,27 +274,20 @@ public void SetCursorHorizontal(int position) /// public void EraseProgress() { - if (_currentFrame.ProgressCount == 0) + if (_currentFrame.RenderedLines == null || _currentFrame.RenderedLines.Count == 0) { return; } - AppendLine($"{AnsiCodes.CSI}{_currentFrame.ProgressCount + 2}{AnsiCodes.MoveUpToLineStart}"); + AppendLine($"{AnsiCodes.CSI}{_currentFrame.RenderedLines.Count + 2}{AnsiCodes.MoveUpToLineStart}"); Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInDisplay}"); _currentFrame.Clear(); } public void RenderProgress(TestProgressState?[] progress) { - AnsiTerminalTestProgressFrame newFrame = new(progress, Width, Height); - - // Do not render delta but clear everything if Terminal width or height have changed. - if (newFrame.Width != _currentFrame.Width || newFrame.Height != _currentFrame.Height) - { - EraseProgress(); - } - - newFrame.Render(_currentFrame, this); + AnsiTerminalTestProgressFrame newFrame = new(Width, Height); + newFrame.Render(_currentFrame, progress, terminal: this); _currentFrame = newFrame; } diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs index c2b3d7da46..481fc49458 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs @@ -12,43 +12,29 @@ internal sealed class AnsiTerminalTestProgressFrame { private const int MaxColumn = 250; - private readonly (TestProgressState TestProgressState, int DurationLength)[] _progressItems; - public int Width { get; } public int Height { get; } - public int ProgressCount { get; private set; } + public List? RenderedLines { get; set; } - public AnsiTerminalTestProgressFrame(TestProgressState?[] nodes, int width, int height) + public AnsiTerminalTestProgressFrame(int width, int height) { Width = Math.Min(width, MaxColumn); Height = height; - - _progressItems = new (TestProgressState, int)[nodes.Length]; - - foreach (TestProgressState? status in nodes) - { - if (status is not null) - { - _progressItems[ProgressCount++].TestProgressState = status; - } - } } - public void AppendTestWorkerProgress(int i, AnsiTerminal terminal) + public void AppendTestWorkerProgress(TestProgressState progress, RenderedProgressItem currentLine, AnsiTerminal terminal) { - TestProgressState p = _progressItems[i].TestProgressState; + string durationString = HumanReadableDurationFormatter.Render(progress.Stopwatch.Elapsed); - string durationString = HumanReadableDurationFormatter.Render(p.Stopwatch.Elapsed); - - _progressItems[i].DurationLength = durationString.Length; + currentLine.RenderedDurationLength = durationString.Length; int nonReservedWidth = Width - (durationString.Length + 2); - int passed = p.PassedTests; - int failed = p.FailedTests; - int skipped = p.SkippedTests; + int passed = progress.PassedTests; + int failed = progress.FailedTests; + int skipped = progress.SkippedTests; int charsTaken = 0; terminal.Append('['); @@ -85,28 +71,27 @@ public void AppendTestWorkerProgress(int i, AnsiTerminal terminal) terminal.Append(']'); charsTaken++; - // -5 because we want to output at least 1 char from the name, and ' ...' followed by duration terminal.Append(' '); charsTaken++; - AppendToWidth(terminal, p.AssemblyName, nonReservedWidth, ref charsTaken); + AppendToWidth(terminal, progress.AssemblyName, nonReservedWidth, ref charsTaken); - if (charsTaken < nonReservedWidth && (p.TargetFramework != null || p.Architecture != null)) + if (charsTaken < nonReservedWidth && (progress.TargetFramework != null || progress.Architecture != null)) { int lengthNeeded = 0; lengthNeeded++; // for '(' - if (p.TargetFramework != null) + if (progress.TargetFramework != null) { - lengthNeeded += p.TargetFramework.Length; - if (p.Architecture != null) + lengthNeeded += progress.TargetFramework.Length; + if (progress.Architecture != null) { lengthNeeded++; // for '|' } } - if (p.Architecture != null) + if (progress.Architecture != null) { - lengthNeeded += p.Architecture.Length; + lengthNeeded += progress.Architecture.Length; } lengthNeeded++; // for ')' @@ -114,29 +99,41 @@ public void AppendTestWorkerProgress(int i, AnsiTerminal terminal) if ((charsTaken + lengthNeeded) < nonReservedWidth) { terminal.Append(" ("); - if (p.TargetFramework != null) + if (progress.TargetFramework != null) { - terminal.Append(p.TargetFramework); - if (p.Architecture != null) + terminal.Append(progress.TargetFramework); + if (progress.Architecture != null) { terminal.Append('|'); } } - if (p.Architecture != null) + if (progress.Architecture != null) { - terminal.Append(p.Architecture); + terminal.Append(progress.Architecture); } terminal.Append(')'); } } - if (!RoslynString.IsNullOrWhiteSpace(p.Detail)) - { - terminal.Append(" - "); - terminal.Append(p.Detail); - } + terminal.SetCursorHorizontal(Width - durationString.Length); + terminal.Append(durationString); + } + + public void AppendTestWorkerDetail(TestDetailState detail, RenderedProgressItem currentLine, AnsiTerminal terminal) + { + string durationString = HumanReadableDurationFormatter.Render(detail.Stopwatch.Elapsed); + + currentLine.RenderedDurationLength = durationString.Length; + + int nonReservedWidth = Width - (durationString.Length + 2); + int charsTaken = 0; + + terminal.Append(" "); + charsTaken += 2; + + AppendToWidth(terminal, detail.Text, nonReservedWidth, ref charsTaken); terminal.SetCursorHorizontal(Width - durationString.Length); terminal.Append(durationString); @@ -166,60 +163,136 @@ private static void AppendToWidth(AnsiTerminal terminal, string text, int width, /// /// Render VT100 string to update from current to next frame. /// - public void Render(AnsiTerminalTestProgressFrame previousFrame, AnsiTerminal terminal) + public void Render(AnsiTerminalTestProgressFrame previousFrame, TestProgressState?[] progress, AnsiTerminal terminal) { - // Don't go up if we did not render progress in previous frame or we cleared it. - if (previousFrame.ProgressCount > 0) + // Clear everything if Terminal width or height have changed. + if (Width != previousFrame.Width || Height != previousFrame.Height) + { + terminal.EraseProgress(); + } + + // Don't go up if we did not render any lines in previous frame or we already cleared them. + if (previousFrame.RenderedLines != null && previousFrame.RenderedLines.Count > 0) { // Move cursor back to 1st line of progress. - // +2 because we prepend 1 empty line before the progress - // and new line after the progress indicator. - terminal.MoveCursorUp(previousFrame.ProgressCount + 2); + // + 2 because we output and empty line right below. + terminal.MoveCursorUp(previousFrame.RenderedLines.Count + 2); } // When there is nothing to render, don't write empty lines, e.g. when we start the test run, and then we kick off build // in dotnet test, there is a long pause where we have no assemblies and no test results (yet). - if (ProgressCount > 0) + // if (ProgressCount > 0) + // { + // terminal.AppendLine(); + // } + int i = 0; + RenderedLines = new List(progress.Length * 2); + var progresses = new List(progress.Length); + + foreach (TestProgressState? progressItem in progress) { - terminal.AppendLine(); + if (progressItem == null) + { + continue; + } + + progresses.Add(progressItem); + if (progressItem.Detail != null) + { + progresses.Add(progressItem.Detail); + } } - int i = 0; - for (; i < ProgressCount; i++) + foreach (object item in progresses) { - // Optimize the rendering. When we have previous frame to compare with, we can decide to rewrite only part of the screen, - // rather than deleting whole line and have the line flicker. Most commonly this will rewrite just the time part of the line. - if (previousFrame.ProgressCount > i) + if (previousFrame.RenderedLines != null && previousFrame.RenderedLines.Count > i) { - if (previousFrame._progressItems[i].TestProgressState.LastUpdate != _progressItems[i].TestProgressState.LastUpdate) + if (item is TestProgressState progressItem) { - // Same everything except time. - string durationString = HumanReadableDurationFormatter.Render(_progressItems[i].TestProgressState.Stopwatch.Elapsed); + var currentLine = new RenderedProgressItem(progressItem.Id, progressItem.Version); + RenderedLines.Add(currentLine); - if (previousFrame._progressItems[i].DurationLength == durationString.Length) + // We have a line that was rendered previously, compare it and decide how to render. + RenderedProgressItem previouslyRenderedLine = previousFrame.RenderedLines[i]; + if (previouslyRenderedLine.ProgressId == progressItem.Id && previouslyRenderedLine.ProgressVersion == progressItem.Version) { - terminal.SetCursorHorizontal(MaxColumn); - terminal.Append($"{AnsiCodes.SetCursorHorizontal(MaxColumn)}{AnsiCodes.MoveCursorBackward(durationString.Length)}{durationString}"); - _progressItems[i].DurationLength = durationString.Length; + // This is the same progress item and it was not updated since we rendered it, only update the timestamp if possible to avoid flicker. + string durationString = HumanReadableDurationFormatter.Render(progressItem.Stopwatch.Elapsed); + + if (previouslyRenderedLine.RenderedDurationLength == durationString.Length) + { + // Duration is the same length rewrite just it. + terminal.SetCursorHorizontal(MaxColumn); + terminal.Append($"{AnsiCodes.SetCursorHorizontal(MaxColumn)}{AnsiCodes.MoveCursorBackward(durationString.Length)}{durationString}"); + currentLine.RenderedDurationLength = durationString.Length; + } + else + { + // Duration is not the same length (it is longer because time moves only forward), we need to re-render the whole line + // to avoid writing the duration over the last portion of text: my.dll (1s) -> my.d (1m 1s) + terminal.Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInLine}"); + AppendTestWorkerProgress(progressItem, currentLine, terminal); + } } else { - // Render full line. + // These lines are different or the line was updated. Render the whole line. terminal.Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInLine}"); - AppendTestWorkerProgress(i, terminal); + AppendTestWorkerProgress(progressItem, currentLine, terminal); } } - else + + if (item is TestDetailState detailItem) { - // Render full line. - terminal.Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInLine}"); - AppendTestWorkerProgress(i, terminal); + var currentLine = new RenderedProgressItem(detailItem.Id, detailItem.Version); + RenderedLines.Add(currentLine); + + // We have a line that was rendered previously, compare it and decide how to render. + RenderedProgressItem previouslyRenderedLine = previousFrame.RenderedLines[i]; + if (previouslyRenderedLine.ProgressId == detailItem.Id && previouslyRenderedLine.ProgressVersion == detailItem.Version) + { + // This is the same progress item and it was not updated since we rendered it, only update the timestamp if possible to avoid flicker. + string durationString = HumanReadableDurationFormatter.Render(detailItem.Stopwatch.Elapsed); + + if (previouslyRenderedLine.RenderedDurationLength == durationString.Length) + { + // Duration is the same length rewrite just it. + terminal.SetCursorHorizontal(MaxColumn); + terminal.Append($"{AnsiCodes.SetCursorHorizontal(MaxColumn)}{AnsiCodes.MoveCursorBackward(durationString.Length)}{durationString}"); + currentLine.RenderedDurationLength = durationString.Length; + } + else + { + // Duration is not the same length (it is longer because time moves only forward), we need to re-render the whole line + // to avoid writing the duration over the last portion of text: my.dll (1s) -> my.d (1m 1s) + terminal.Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInLine}"); + AppendTestWorkerDetail(detailItem, currentLine, terminal); + } + } + else + { + // These lines are different or the line was updated. Render the whole line. + terminal.Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInLine}"); + AppendTestWorkerDetail(detailItem, currentLine, terminal); + } } } else { - // From now on we have to simply WriteLine - AppendTestWorkerProgress(i, terminal); + // We are rendering more lines than we rendered in previous frame + if (item is TestProgressState progressItem) + { + var currentLine = new RenderedProgressItem(progressItem.Id, progressItem.Version); + RenderedLines.Add(currentLine); + AppendTestWorkerProgress(progressItem, currentLine, terminal); + } + + if (item is TestDetailState detailItem) + { + var currentLine = new RenderedProgressItem(detailItem.Id, detailItem.Version); + RenderedLines.Add(currentLine); + AppendTestWorkerDetail(detailItem, currentLine, terminal); + } } // This makes the progress not stick to the last line on the command line, which is @@ -228,12 +301,29 @@ public void Render(AnsiTerminalTestProgressFrame previousFrame, AnsiTerminal ter terminal.AppendLine(); } - // clear no longer used lines - if (i < previousFrame.ProgressCount) + // We rendered more lines in previous frame. Clear them. + if (previousFrame.RenderedLines != null && i < previousFrame.RenderedLines.Count) { terminal.Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInDisplay}"); } } - public void Clear() => ProgressCount = 0; + public void Clear() => RenderedLines?.Clear(); + + internal class RenderedProgressItem + { + public RenderedProgressItem(long id, long version) + { + ProgressId = id; + ProgressVersion = version; + } + + public long ProgressId { get; } + + public long ProgressVersion { get; } + + public int RenderedHeight { get; set; } + + public int RenderedDurationLength { get; set; } + } } diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/NonAnsiTerminal.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/NonAnsiTerminal.cs index e0af85e4fc..7cdb995138 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/NonAnsiTerminal.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/NonAnsiTerminal.cs @@ -182,7 +182,7 @@ public void RenderProgress(TestProgressState?[] progress) // Use just ascii here, so we don't put too many restrictions on fonts needing to // properly show unicode, or logs being saved in particular encoding. - string? detail = !RoslynString.IsNullOrWhiteSpace(p.Detail) ? $"- {p.Detail}" : null; + string? detail = !RoslynString.IsNullOrWhiteSpace(p.Detail?.Text) ? $"- {p.Detail.Text}" : null; Append('['); SetColor(TerminalColor.DarkGreen); Append('+'); diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs index afcc850879..0d98982826 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs @@ -101,10 +101,12 @@ private static Regex GetFrameRegex() } #endif + private int _counter; + /// /// Initializes a new instance of the class with custom terminal and manual refresh for testing. /// - internal TerminalTestReporter(IConsole console, TerminalTestReporterOptions options) + public TerminalTestReporter(IConsole console, TerminalTestReporterOptions options) { _options = options; @@ -164,7 +166,7 @@ private TestProgressState GetOrAddAssemblyRun(string assembly, string? targetFra } IStopwatch sw = CreateStopwatch(); - var assemblyRun = new TestProgressState(assembly, targetFramework, architecture, sw); + var assemblyRun = new TestProgressState(_counter++, assembly, targetFramework, architecture, sw); int slotIndex = _terminalWithProgress.AddWorker(assemblyRun); assemblyRun.SlotIndex = slotIndex; @@ -173,7 +175,7 @@ private TestProgressState GetOrAddAssemblyRun(string assembly, string? targetFra return assemblyRun; } - internal void TestExecutionCompleted(DateTimeOffset endTime) + public void TestExecutionCompleted(DateTimeOffset endTime) { _testExecutionEndTime = endTime; _terminalWithProgress.StopShowingProgress(); @@ -798,7 +800,7 @@ public void ArtifactAdded(bool outOfProcess, string? assembly, string? targetFra /// /// Let the user know that cancellation was triggered. /// - internal void StartCancelling() + public void StartCancelling() { _wasCancelled = true; _terminalWithProgress.WriteToTerminal(terminal => @@ -853,7 +855,7 @@ internal void WriteWarningMessage(string assembly, string? targetFramework, stri internal void WriteErrorMessage(string assembly, string? targetFramework, string? architecture, string? executionId, Exception exception) => WriteErrorMessage(assembly, targetFramework, architecture, executionId, exception.ToString(), padding: null); - internal void WriteMessage(string text, SystemConsoleColor? color = null, int? padding = null) + public void WriteMessage(string text, SystemConsoleColor? color = null, int? padding = null) { if (color != null) { @@ -976,4 +978,17 @@ private static TerminalColor ToTerminalColor(ConsoleColor consoleColor) ConsoleColor.White => TerminalColor.White, _ => TerminalColor.Default, }; + + public void TestInProgress( + string assembly, + string? targetFramework, + string? architecture, + string displayName, + string? executionId) + { + TestProgressState asm = _assemblies[$"{assembly}|{targetFramework}|{architecture}|{executionId}"]; + + asm.Detail = new(_counter++, version: 0, CreateStopwatch(), displayName); + _terminalWithProgress.UpdateWorker(asm.SlotIndex); + } } diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs new file mode 100644 index 0000000000..7c18375a8f --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Helpers; + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +internal sealed class TestDetailState +{ + public TestDetailState(long id, long version, IStopwatch stopwatch, string text) + { + Id = id; + Version = version; + Stopwatch = stopwatch; + Text = text; + } + + public long Id { get; } + + public long Version { get; } + + public IStopwatch Stopwatch { get; } + + public string Text { get; } +} diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs index d117e2df4f..d8091e38f6 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs @@ -7,8 +7,9 @@ namespace Microsoft.Testing.Platform.OutputDevice.Terminal; internal sealed class TestProgressState { - public TestProgressState(string assembly, string? targetFramework, string? architecture, IStopwatch stopwatch) + public TestProgressState(long id, string assembly, string? targetFramework, string? architecture, IStopwatch stopwatch) { + Id = id; Assembly = assembly; TargetFramework = targetFramework; Architecture = architecture; @@ -38,11 +39,13 @@ public TestProgressState(string assembly, string? targetFramework, string? archi public int TotalTests { get; internal set; } - public string? Detail { get; internal set; } + public TestDetailState? Detail { get; internal set; } public int SlotIndex { get; internal set; } - public long LastUpdate { get; internal set; } + public long Id { get; internal set; } + + public long Version { get; internal set; } public List<(string? DisplayName, string? UID)> DiscoveredTests { get; internal set; } = new(); diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressStateAwareTerminal.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressStateAwareTerminal.cs index f1b1f32c62..c3a622df72 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressStateAwareTerminal.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressStateAwareTerminal.cs @@ -164,7 +164,7 @@ internal void UpdateWorker(int slotIndex) TestProgressState? progress = _progressItems[slotIndex]; if (progress != null) { - progress.LastUpdate = _counter; + progress.Version = _counter; } } } diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs index a0601b4491..f12c3086f4 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs @@ -434,7 +434,12 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella switch (testNodeStateChanged.TestNode.Properties.SingleOrDefault()) { case InProgressTestNodeStateProperty: - // do nothing. + _terminalTestReporter.TestInProgress( + _assemblyName, + _targetFramework, + _shortArchitecture, + testNodeStateChanged.TestNode.DisplayName, + executionId: null); break; case ErrorTestNodeStateProperty errorState: From 046c3285c4d431ed3984238d5453fbd7ccfc0e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Fri, 23 Aug 2024 16:45:06 +0200 Subject: [PATCH 02/18] Fixes --- .../Terminal/TerminalTestReporter.cs | 2 +- .../OutputDevice/Terminal/TestDetailState.cs | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs index 0d98982826..909e7dfabd 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs @@ -988,7 +988,7 @@ public void TestInProgress( { TestProgressState asm = _assemblies[$"{assembly}|{targetFramework}|{architecture}|{executionId}"]; - asm.Detail = new(_counter++, version: 0, CreateStopwatch(), displayName); + asm.Detail = new(_counter++, CreateStopwatch(), displayName); _terminalWithProgress.UpdateWorker(asm.SlotIndex); } } diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs index 7c18375a8f..5e369ba82e 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs @@ -7,19 +7,28 @@ namespace Microsoft.Testing.Platform.OutputDevice.Terminal; internal sealed class TestDetailState { - public TestDetailState(long id, long version, IStopwatch stopwatch, string text) + private string _text; + + public TestDetailState(long id, IStopwatch stopwatch, string text) { Id = id; - Version = version; Stopwatch = stopwatch; - Text = text; + _text = text; } public long Id { get; } - public long Version { get; } + public long Version { get; set; } public IStopwatch Stopwatch { get; } - public string Text { get; } + public string Text + { + get => _text; + set + { + Version++; + _text = value; + } + } } From f8d84dc7d2ce9af79d27601ab6564d6fcefbceea Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Mon, 2 Dec 2024 18:59:19 +0100 Subject: [PATCH 03/18] Update terminal reporting to include the active tests in progress --- .../Terminal/AnsiTerminalTestProgressFrame.cs | 33 +++++++--- .../HumanReadableDurationFormatter.cs | 27 +++++---- .../OutputDevice/Terminal/NonAnsiTerminal.cs | 3 +- .../Terminal/TerminalTestReporter.cs | 11 +++- .../OutputDevice/Terminal/TestDetailState.cs | 4 +- .../Terminal/TestNodeResultsState.cs | 60 +++++++++++++++++++ .../Terminal/TestProgressState.cs | 2 +- .../OutputDevice/TerminalOutputDevice.cs | 7 +++ .../Terminal/TerminalTestReporterTests.cs | 10 ++-- 9 files changed, 127 insertions(+), 30 deletions(-) create mode 100644 src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs index 481fc49458..b564e995af 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs @@ -123,7 +123,7 @@ public void AppendTestWorkerProgress(TestProgressState progress, RenderedProgres public void AppendTestWorkerDetail(TestDetailState detail, RenderedProgressItem currentLine, AnsiTerminal terminal) { - string durationString = HumanReadableDurationFormatter.Render(detail.Stopwatch.Elapsed); + string durationString = HumanReadableDurationFormatter.Render(detail.Stopwatch?.Elapsed); currentLine.RenderedDurationLength = durationString.Length; @@ -171,6 +171,14 @@ public void Render(AnsiTerminalTestProgressFrame previousFrame, TestProgressStat terminal.EraseProgress(); } + // At the end of the terminal we're going to print the live progress. + // We re-render this progress by moving the cursor to the beginning of the previous progress + // and then overwriting the lines that have changed. + // The assumption we do here is that: + // - Each rendered line is a single line, i.e. a single detail cannot span multiple lines. + // - Each rendered detail can be tracked via a unique ID and version, so that we can + // quickly determine if the detail has changed since the last render. + // Don't go up if we did not render any lines in previous frame or we already cleared them. if (previousFrame.RenderedLines != null && previousFrame.RenderedLines.Count > 0) { @@ -181,10 +189,11 @@ public void Render(AnsiTerminalTestProgressFrame previousFrame, TestProgressStat // When there is nothing to render, don't write empty lines, e.g. when we start the test run, and then we kick off build // in dotnet test, there is a long pause where we have no assemblies and no test results (yet). - // if (ProgressCount > 0) - // { - // terminal.AppendLine(); - // } + if (progress.Length > 0) + { + terminal.AppendLine(); + } + int i = 0; RenderedLines = new List(progress.Length * 2); var progresses = new List(progress.Length); @@ -197,9 +206,15 @@ public void Render(AnsiTerminalTestProgressFrame previousFrame, TestProgressStat } progresses.Add(progressItem); - if (progressItem.Detail != null) + + IEnumerable? runningTasks + = progressItem.TestNodeResultsState?.GetRunningTasks(); + if (runningTasks is not null) { - progresses.Add(progressItem.Detail); + foreach (TestDetailState testDetail in runningTasks) + { + progresses.Add(testDetail); + } } } @@ -214,7 +229,7 @@ public void Render(AnsiTerminalTestProgressFrame previousFrame, TestProgressStat // We have a line that was rendered previously, compare it and decide how to render. RenderedProgressItem previouslyRenderedLine = previousFrame.RenderedLines[i]; - if (previouslyRenderedLine.ProgressId == progressItem.Id && previouslyRenderedLine.ProgressVersion == progressItem.Version) + if (previouslyRenderedLine.ProgressId == progressItem.Id && false) { // This is the same progress item and it was not updated since we rendered it, only update the timestamp if possible to avoid flicker. string durationString = HumanReadableDurationFormatter.Render(progressItem.Stopwatch.Elapsed); @@ -252,7 +267,7 @@ public void Render(AnsiTerminalTestProgressFrame previousFrame, TestProgressStat if (previouslyRenderedLine.ProgressId == detailItem.Id && previouslyRenderedLine.ProgressVersion == detailItem.Version) { // This is the same progress item and it was not updated since we rendered it, only update the timestamp if possible to avoid flicker. - string durationString = HumanReadableDurationFormatter.Render(detailItem.Stopwatch.Elapsed); + string durationString = HumanReadableDurationFormatter.Render(detailItem.Stopwatch?.Elapsed); if (previouslyRenderedLine.RenderedDurationLength == durationString.Length) { diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/HumanReadableDurationFormatter.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/HumanReadableDurationFormatter.cs index 6d07e8d56b..27aa9af5d1 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/HumanReadableDurationFormatter.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/HumanReadableDurationFormatter.cs @@ -55,8 +55,13 @@ public static void Append(ITerminal terminal, TimeSpan duration, bool wrapInPare private static string GetFormattedPart(int value, bool hasParentValue, string suffix, int paddingWitdh = 2) => $"{(hasParentValue ? " " : string.Empty)}{(hasParentValue ? value.ToString(CultureInfo.InvariantCulture).PadLeft(paddingWitdh, '0') : value.ToString(CultureInfo.InvariantCulture))}{suffix}"; - public static string Render(TimeSpan duration, bool wrapInParentheses = true, bool showMilliseconds = false) + public static string Render(TimeSpan? duration, bool wrapInParentheses = true, bool showMilliseconds = false) { + if (duration is null) + { + return string.Empty; + } + bool hasParentValue = false; var stringBuilder = new StringBuilder(); @@ -66,35 +71,35 @@ public static string Render(TimeSpan duration, bool wrapInParentheses = true, bo stringBuilder.Append('('); } - if (duration.Days > 0) + if (duration.Value.Days > 0) { - stringBuilder.Append(CultureInfo.CurrentCulture, $"{duration.Days}d"); + stringBuilder.Append(CultureInfo.CurrentCulture, $"{duration.Value.Days}d"); hasParentValue = true; } - if (duration.Hours > 0 || hasParentValue) + if (duration.Value.Hours > 0 || hasParentValue) { - stringBuilder.Append(GetFormattedPart(duration.Hours, hasParentValue, "h")); + stringBuilder.Append(GetFormattedPart(duration.Value.Hours, hasParentValue, "h")); hasParentValue = true; } - if (duration.Minutes > 0 || hasParentValue) + if (duration.Value.Minutes > 0 || hasParentValue) { - stringBuilder.Append(GetFormattedPart(duration.Minutes, hasParentValue, "m")); + stringBuilder.Append(GetFormattedPart(duration.Value.Minutes, hasParentValue, "m")); hasParentValue = true; } - if (duration.Seconds > 0 || hasParentValue || !showMilliseconds) + if (duration.Value.Seconds > 0 || hasParentValue || !showMilliseconds) { - stringBuilder.Append(GetFormattedPart(duration.Seconds, hasParentValue, "s")); + stringBuilder.Append(GetFormattedPart(duration.Value.Seconds, hasParentValue, "s")); hasParentValue = true; } if (showMilliseconds) { - if (duration.Milliseconds >= 0 || hasParentValue) + if (duration.Value.Milliseconds >= 0 || hasParentValue) { - stringBuilder.Append(GetFormattedPart(duration.Milliseconds, hasParentValue, "ms", paddingWitdh: 3)); + stringBuilder.Append(GetFormattedPart(duration.Value.Milliseconds, hasParentValue, "ms", paddingWitdh: 3)); } } diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/NonAnsiTerminal.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/NonAnsiTerminal.cs index 7cdb995138..1b6707600f 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/NonAnsiTerminal.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/NonAnsiTerminal.cs @@ -182,7 +182,8 @@ public void RenderProgress(TestProgressState?[] progress) // Use just ascii here, so we don't put too many restrictions on fonts needing to // properly show unicode, or logs being saved in particular encoding. - string? detail = !RoslynString.IsNullOrWhiteSpace(p.Detail?.Text) ? $"- {p.Detail.Text}" : null; + TestDetailState? activeTest = p.TestNodeResultsState?.GetRunningTasks().FirstOrDefault(); + string? detail = !RoslynString.IsNullOrWhiteSpace(activeTest?.Text) ? $"- {activeTest.Text}" : null; Append('['); SetColor(TerminalColor.DarkGreen); Append('+'); diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs index 909e7dfabd..7b7dcbf4f8 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs @@ -11,6 +11,7 @@ using Microsoft.Testing.Platform.Helpers; using Microsoft.Testing.Platform.Resources; +using Microsoft.Testing.Platform.Extensions.Messages; namespace Microsoft.Testing.Platform.OutputDevice.Terminal; @@ -374,6 +375,7 @@ internal void TestCompleted( string? targetFramework, string? architecture, string? executionId, + string testNodeUid, string displayName, TestOutcome outcome, TimeSpan duration, @@ -390,6 +392,7 @@ internal void TestCompleted( targetFramework, architecture, executionId, + testNodeUid, displayName, outcome, duration, @@ -405,6 +408,7 @@ internal void TestCompleted( string? targetFramework, string? architecture, string? executionId, + string testNodeUid, string displayName, TestOutcome outcome, TimeSpan duration, @@ -416,6 +420,8 @@ internal void TestCompleted( { TestProgressState asm = _assemblies[$"{assembly}|{targetFramework}|{architecture}|{executionId}"]; + asm.TestNodeResultsState?.RemoveRunningTestNode(testNodeUid); + switch (outcome) { case TestOutcome.Error: @@ -983,12 +989,15 @@ public void TestInProgress( string assembly, string? targetFramework, string? architecture, + string testNodeUid, string displayName, string? executionId) { TestProgressState asm = _assemblies[$"{assembly}|{targetFramework}|{architecture}|{executionId}"]; - asm.Detail = new(_counter++, CreateStopwatch(), displayName); + asm.TestNodeResultsState ??= new(_counter++); + asm.TestNodeResultsState.AddRunningTestNode( + _counter++, testNodeUid, $"{displayName}", CreateStopwatch()); _terminalWithProgress.UpdateWorker(asm.SlotIndex); } } diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs index 5e369ba82e..bd806bb36f 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs @@ -9,7 +9,7 @@ internal sealed class TestDetailState { private string _text; - public TestDetailState(long id, IStopwatch stopwatch, string text) + public TestDetailState(long id, IStopwatch? stopwatch, string text) { Id = id; Stopwatch = stopwatch; @@ -20,7 +20,7 @@ public TestDetailState(long id, IStopwatch stopwatch, string text) public long Version { get; set; } - public IStopwatch Stopwatch { get; } + public IStopwatch? Stopwatch { get; } public string Text { diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs new file mode 100644 index 0000000000..ce43713550 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Concurrent; + +using Microsoft.Testing.Platform.Helpers; + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +internal sealed class TestNodeResultsState +{ + public TestNodeResultsState(long id) + { + Id = id; + _summaryDetail = new(id, stopwatch: null, text: string.Empty); + } + + public long Id { get; } + + private readonly TestDetailState _summaryDetail; + + public ConcurrentDictionary TestNodeProgressStates { get; internal set; } + = new(); + + public void AddRunningTestNode(int id, string uid, string name, IStopwatch stopwatch) + { + TestNodeProgressStates[uid] = new TestDetailState(id, stopwatch, name); + UpdateSummaryDetail(); + } + + public void RemoveRunningTestNode(string uid) + { + TestNodeProgressStates.TryRemove(uid, out _); + UpdateSummaryDetail(); + } + + private void UpdateSummaryDetail() + => _summaryDetail.Text = TestNodeProgressStates.Count > 5 + ? $"... {TestNodeProgressStates.Count} more tests" + : string.Empty; + + // Note: Show up to 5 long running tests per project. + public IEnumerable GetRunningTasks() + { + var sortedDetails = TestNodeProgressStates + .Select(d => d.Value) + .OrderBy(d => d.Stopwatch?.Elapsed ?? TimeSpan.Zero) + .Take(5); + + foreach (TestDetailState? detail in sortedDetails) + { + yield return detail; + } + + if (!RoslynString.IsNullOrEmpty(_summaryDetail.Text)) + { + yield return _summaryDetail; + } + } +} diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs index d8091e38f6..7824e12bc0 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs @@ -39,7 +39,7 @@ public TestProgressState(long id, string assembly, string? targetFramework, stri public int TotalTests { get; internal set; } - public TestDetailState? Detail { get; internal set; } + public TestNodeResultsState? TestNodeResultsState { get; internal set; } public int SlotIndex { get; internal set; } diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs index f12c3086f4..99d1eb97ee 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs @@ -438,6 +438,7 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella _assemblyName, _targetFramework, _shortArchitecture, + testNodeStateChanged.TestNode.Uid.Value, testNodeStateChanged.TestNode.DisplayName, executionId: null); break; @@ -448,6 +449,7 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella _targetFramework, _shortArchitecture, executionId: null, + testNodeStateChanged.TestNode.Uid.Value, testNodeStateChanged.TestNode.DisplayName, TestOutcome.Error, duration, @@ -465,6 +467,7 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella _targetFramework, _shortArchitecture, executionId: null, + testNodeStateChanged.TestNode.Uid.Value, testNodeStateChanged.TestNode.DisplayName, TestOutcome.Fail, duration, @@ -482,6 +485,7 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella _targetFramework, _shortArchitecture, executionId: null, + testNodeStateChanged.TestNode.Uid.Value, testNodeStateChanged.TestNode.DisplayName, TestOutcome.Timeout, duration, @@ -499,6 +503,7 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella _targetFramework, _shortArchitecture, executionId: null, + testNodeStateChanged.TestNode.Uid.Value, testNodeStateChanged.TestNode.DisplayName, TestOutcome.Canceled, duration, @@ -516,6 +521,7 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella _targetFramework, _shortArchitecture, executionId: null, + testNodeStateChanged.TestNode.Uid.Value, testNodeStateChanged.TestNode.DisplayName, outcome: TestOutcome.Passed, duration: duration, @@ -533,6 +539,7 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella _targetFramework, _shortArchitecture, executionId: null, + testNodeStateChanged.TestNode.Uid.Value, testNodeStateChanged.TestNode.DisplayName, TestOutcome.Skipped, duration, diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs index 58ad5b0aad..cf590f5e4c 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs @@ -72,16 +72,16 @@ public void OutputFormattingIsCorrect() string standardOutput = "Hello!"; string errorOutput = "Oh no!"; - terminalReporter.TestCompleted(assembly, targetFramework, architecture, executionId: null, "PassedTest1", TestOutcome.Passed, TimeSpan.FromSeconds(10), + terminalReporter.TestCompleted(assembly, targetFramework, architecture, executionId: null, testNodeUid: "PassedTest1", "PassedTest1", TestOutcome.Passed, TimeSpan.FromSeconds(10), errorMessage: null, exception: null, expected: null, actual: null, standardOutput, errorOutput); - terminalReporter.TestCompleted(assembly, targetFramework, architecture, executionId: null, "SkippedTest1", TestOutcome.Skipped, TimeSpan.FromSeconds(10), + terminalReporter.TestCompleted(assembly, targetFramework, architecture, executionId: null, testNodeUid: "SkippedTest1", "SkippedTest1", TestOutcome.Skipped, TimeSpan.FromSeconds(10), errorMessage: null, exception: null, expected: null, actual: null, standardOutput, errorOutput); // timed out + canceled + failed should all report as failed in summary - terminalReporter.TestCompleted(assembly, targetFramework, architecture, executionId: null, "TimedoutTest1", TestOutcome.Timeout, TimeSpan.FromSeconds(10), + terminalReporter.TestCompleted(assembly, targetFramework, architecture, executionId: null, testNodeUid: "TimedoutTest1", "TimedoutTest1", TestOutcome.Timeout, TimeSpan.FromSeconds(10), errorMessage: null, exception: null, expected: null, actual: null, standardOutput, errorOutput); - terminalReporter.TestCompleted(assembly, targetFramework, architecture, executionId: null, "CanceledTest1", TestOutcome.Canceled, TimeSpan.FromSeconds(10), + terminalReporter.TestCompleted(assembly, targetFramework, architecture, executionId: null, testNodeUid: "CanceledTest1", "CanceledTest1", TestOutcome.Canceled, TimeSpan.FromSeconds(10), errorMessage: null, exception: null, expected: null, actual: null, standardOutput, errorOutput); - terminalReporter.TestCompleted(assembly, targetFramework, architecture, executionId: null, "FailedTest1", TestOutcome.Fail, TimeSpan.FromSeconds(10), + terminalReporter.TestCompleted(assembly, targetFramework, architecture, executionId: null, testNodeUid: "FailedTest1", "FailedTest1", TestOutcome.Fail, TimeSpan.FromSeconds(10), errorMessage: "Tests failed", exception: new StackTraceException(@$" at FailingTest() in {folder}codefile.cs:line 10"), expected: "ABC", actual: "DEF", standardOutput, errorOutput); terminalReporter.ArtifactAdded(outOfProcess: true, assembly, targetFramework, architecture, executionId: null, testName: null, @$"{folder}artifact1.txt"); terminalReporter.ArtifactAdded(outOfProcess: false, assembly, targetFramework, architecture, executionId: null, testName: null, @$"{folder}artifact2.txt"); From 82e00bff2e94519509ad5b8d031d27b0220cd4bb Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Mon, 2 Dec 2024 19:09:46 +0100 Subject: [PATCH 04/18] Add resource strings --- .../OutputDevice/Terminal/TestNodeResultsState.cs | 4 +++- .../Resources/PlatformResources.resx | 3 +++ .../Resources/xlf/PlatformResources.cs.xlf | 5 +++++ .../Resources/xlf/PlatformResources.de.xlf | 5 +++++ .../Resources/xlf/PlatformResources.es.xlf | 5 +++++ .../Resources/xlf/PlatformResources.fr.xlf | 5 +++++ .../Resources/xlf/PlatformResources.it.xlf | 5 +++++ .../Resources/xlf/PlatformResources.ja.xlf | 5 +++++ .../Resources/xlf/PlatformResources.ko.xlf | 5 +++++ .../Resources/xlf/PlatformResources.pl.xlf | 5 +++++ .../Resources/xlf/PlatformResources.pt-BR.xlf | 5 +++++ .../Resources/xlf/PlatformResources.ru.xlf | 5 +++++ .../Resources/xlf/PlatformResources.tr.xlf | 5 +++++ .../Resources/xlf/PlatformResources.zh-Hans.xlf | 5 +++++ .../Resources/xlf/PlatformResources.zh-Hant.xlf | 5 +++++ 15 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs index ce43713550..56d9e5405b 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs @@ -2,8 +2,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Concurrent; +using System.Globalization; using Microsoft.Testing.Platform.Helpers; +using Microsoft.Testing.Platform.Resources; namespace Microsoft.Testing.Platform.OutputDevice.Terminal; @@ -36,7 +38,7 @@ public void RemoveRunningTestNode(string uid) private void UpdateSummaryDetail() => _summaryDetail.Text = TestNodeProgressStates.Count > 5 - ? $"... {TestNodeProgressStates.Count} more tests" + ? $"... {string.Format(CultureInfo.CurrentCulture, PlatformResources.MoreTestsRunning, TestNodeProgressStates.Count)}" : string.Empty; // Note: Show up to 5 long running tests per project. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx b/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx index 51d20861e7..604bec623b 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx +++ b/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx @@ -665,4 +665,7 @@ Takes one argument as string in the format <value>[h|m|s] where 'value' is Specifies the minimum number of tests that are expected to run. + + and {0} more + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf index 59c6383447..ca44f781ac 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf @@ -371,6 +371,11 @@ Při použití protokolu jsonRpc byl očekáván parametr --client-port. + + and {0} more + and {0} more + + No serializer registered with ID '{0}' Není zaregistrovaný žádný serializátor s ID {0}. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf index 93010da43a..13aa942fcd 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf @@ -371,6 +371,11 @@ "--client-port" wird erwartet, wenn das jsonRpc-Protokoll verwendet wird. + + and {0} more + and {0} more + + No serializer registered with ID '{0}' Es ist kein Serialisierungsmodul mit der ID "{0}" registriert diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf index 91955cb0c2..aef3609a7e 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf @@ -371,6 +371,11 @@ Se esperaba --client-port cuando se usa el protocolo jsonRpc. + + and {0} more + and {0} more + + No serializer registered with ID '{0}' No hay ningún serializador registrado con el id. '{0}' diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf index a52c16c4c7..6f3e8bb761 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf @@ -371,6 +371,11 @@ Attendu --client-port attendu lorsque le protocole jsonRpc est utilisé. + + and {0} more + and {0} more + + No serializer registered with ID '{0}' Aucun sérialiseur inscrit avec l’ID « {0} » diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf index ffc131d874..626e8a19c1 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf @@ -371,6 +371,11 @@ Previsto --client-port quando viene usato il protocollo jsonRpc. + + and {0} more + and {0} more + + No serializer registered with ID '{0}' Nessun serializzatore registrato con ID '{0}' diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf index 93c55f9f9e..30d1be32ed 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf @@ -371,6 +371,11 @@ jsonRpc プロトコルを使用する場合、--client-port が必要です。 + + and {0} more + and {0} more + + No serializer registered with ID '{0}' ID '{0}' で登録されたシリアライザーがありません diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf index 02feafb53a..5ad579b0a7 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf @@ -371,6 +371,11 @@ jsonRpc 프로토콜을 사용하는 경우 --client-port가 필요합니다. + + and {0} more + and {0} more + + No serializer registered with ID '{0}' ID '{0}'(으)로 등록된 직렬 변환기가 없습니다. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf index 63a7e36fe1..14d9ccc051 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf @@ -371,6 +371,11 @@ Oczekiwano parametru --client-port, gdy jest używany protokół jsonRpc. + + and {0} more + and {0} more + + No serializer registered with ID '{0}' Nie zarejestrowano serializatora z identyfikatorem „{0}” diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf index a397aabdb9..e2f5cbf1ed 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf @@ -371,6 +371,11 @@ Esperado --client-port quando o protocolo jsonRpc é usado. + + and {0} more + and {0} more + + No serializer registered with ID '{0}' Nenhum serializador registrado com a ID “{0}” diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf index b478eea875..e4e10bcaf2 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf @@ -371,6 +371,11 @@ Ожидается --client-port при использовании протокола jsonRpc. + + and {0} more + and {0} more + + No serializer registered with ID '{0}' Не зарегистрирован сериализатор с ИД "{0}" diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf index 4d0ad0f21e..e1f63d8e4c 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf @@ -371,6 +371,11 @@ JsonRpc protokolü kullanıldığında --client-port bekleniyordu. + + and {0} more + and {0} more + + No serializer registered with ID '{0}' '{0}' kimliğiyle kayıtlı seri hale getirici yok diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf index 381bffaf18..238de85fcf 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf @@ -371,6 +371,11 @@ 使用 jsonRpc 协议时应为 --client-port。 + + and {0} more + and {0} more + + No serializer registered with ID '{0}' 没有使用 ID“{0}”注册序列化程序 diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf index 6a801d838c..daf53c0b2c 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf @@ -371,6 +371,11 @@ 使用 jsonRpc 通訊協定時,必須是 --client-port。 + + and {0} more + and {0} more + + No serializer registered with ID '{0}' 沒有使用識別碼 '{0}' 註冊的序列化程式 From 3805a59fca30c0af47d6c2370fa21ab590a159f0 Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Mon, 2 Dec 2024 19:11:30 +0100 Subject: [PATCH 05/18] Fixup small code style issues --- .../OutputDevice/Terminal/TestNodeResultsState.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs index 56d9e5405b..ce62ac21ea 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs @@ -20,31 +20,29 @@ public TestNodeResultsState(long id) public long Id { get; } private readonly TestDetailState _summaryDetail; - - public ConcurrentDictionary TestNodeProgressStates { get; internal set; } - = new(); + private readonly ConcurrentDictionary _testNodeProgressStates = new(); public void AddRunningTestNode(int id, string uid, string name, IStopwatch stopwatch) { - TestNodeProgressStates[uid] = new TestDetailState(id, stopwatch, name); + _testNodeProgressStates[uid] = new TestDetailState(id, stopwatch, name); UpdateSummaryDetail(); } public void RemoveRunningTestNode(string uid) { - TestNodeProgressStates.TryRemove(uid, out _); + _testNodeProgressStates.TryRemove(uid, out _); UpdateSummaryDetail(); } private void UpdateSummaryDetail() - => _summaryDetail.Text = TestNodeProgressStates.Count > 5 - ? $"... {string.Format(CultureInfo.CurrentCulture, PlatformResources.MoreTestsRunning, TestNodeProgressStates.Count)}" + => _summaryDetail.Text = _testNodeProgressStates.Count > 5 + ? $"... {string.Format(CultureInfo.CurrentCulture, PlatformResources.MoreTestsRunning, _testNodeProgressStates.Count)}" : string.Empty; // Note: Show up to 5 long running tests per project. public IEnumerable GetRunningTasks() { - var sortedDetails = TestNodeProgressStates + IEnumerable sortedDetails = _testNodeProgressStates .Select(d => d.Value) .OrderBy(d => d.Stopwatch?.Elapsed ?? TimeSpan.Zero) .Take(5); From 4bc39040e66835751ab427fc0fb02b430ad587db Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Tue, 3 Dec 2024 13:33:17 +0100 Subject: [PATCH 06/18] Remove long running test from playground --- samples/Playground/Tests.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/samples/Playground/Tests.cs b/samples/Playground/Tests.cs index a00c9eef69..eb480d750f 100644 --- a/samples/Playground/Tests.cs +++ b/samples/Playground/Tests.cs @@ -24,10 +24,4 @@ public class TestClass public void Test3() { } - - [TestMethod] - public void Test4() - { - Thread.Sleep(15_000); - } } From 5af15db8bc71ecf7708a810e78a4fcf90949308c Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Wed, 4 Dec 2024 14:45:22 +0100 Subject: [PATCH 07/18] Improve distribution of the additional lines to render Make the number of active tests to render based on the size of the terminal. --- .../Terminal/AnsiTerminalTestProgressFrame.cs | 50 +++++++++++-------- .../OutputDevice/Terminal/NonAnsiTerminal.cs | 2 +- .../OutputDevice/Terminal/TestDetailState.cs | 7 ++- .../Terminal/TestNodeResultsState.cs | 29 +++++------ 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs index b564e995af..957f934bd5 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs @@ -196,27 +196,7 @@ public void Render(AnsiTerminalTestProgressFrame previousFrame, TestProgressStat int i = 0; RenderedLines = new List(progress.Length * 2); - var progresses = new List(progress.Length); - - foreach (TestProgressState? progressItem in progress) - { - if (progressItem == null) - { - continue; - } - - progresses.Add(progressItem); - - IEnumerable? runningTasks - = progressItem.TestNodeResultsState?.GetRunningTasks(); - if (runningTasks is not null) - { - foreach (TestDetailState testDetail in runningTasks) - { - progresses.Add(testDetail); - } - } - } + List progresses = GenerateLinesToRender(progress); foreach (object item in progresses) { @@ -323,6 +303,34 @@ public void Render(AnsiTerminalTestProgressFrame previousFrame, TestProgressStat } } + private List GenerateLinesToRender(TestProgressState?[] progress) + { + var linesToRender = new List(progress.Length); + + // Note: We want to render the list of active tests, but this can easily fill up the full screen. + // As such, we should balance the number of active tests shown per project. + // We do this by distributing the remaining lines for each projects. + var progressItems = progress.OfType().ToList(); + int linesToDistribute = (int)(Height * 0.7) - 1 - progressItems.Count; + var detailItems = new IEnumerable[progressItems.Count]; + var sortedItemsI = Enumerable.Range(0, progressItems.Count).OrderBy(i => progressItems[i].TestNodeResultsState?.Count ?? 0).ToList(); + + foreach (int sortedItemIndex in sortedItemsI) + { + detailItems[sortedItemIndex] = progressItems[sortedItemIndex].TestNodeResultsState?.GetRunningTasks( + linesToDistribute / progressItems.Count) + ?? Array.Empty(); + } + + for (int progressI = 0; progressI < progressItems.Count; progressI++) + { + linesToRender.Add(progressItems[progressI]); + linesToRender.AddRange(detailItems[progressI]); + } + + return linesToRender; + } + public void Clear() => RenderedLines?.Clear(); internal class RenderedProgressItem diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/NonAnsiTerminal.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/NonAnsiTerminal.cs index 842e20f83c..2da9ab982c 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/NonAnsiTerminal.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/NonAnsiTerminal.cs @@ -183,7 +183,7 @@ public void RenderProgress(TestProgressState?[] progress) // Use just ascii here, so we don't put too many restrictions on fonts needing to // properly show unicode, or logs being saved in particular encoding. - TestDetailState? activeTest = p.TestNodeResultsState?.GetRunningTasks().FirstOrDefault(); + TestDetailState? activeTest = p.TestNodeResultsState?.GetFirstRunningTask(); string? detail = !RoslynString.IsNullOrWhiteSpace(activeTest?.Text) ? $"- {activeTest.Text}" : null; Append('['); SetColor(TerminalColor.DarkGreen); diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs index bd806bb36f..ad00e3ee42 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestDetailState.cs @@ -27,8 +27,11 @@ public string Text get => _text; set { - Version++; - _text = value; + if (!_text.Equals(value, StringComparison.Ordinal)) + { + Version++; + _text = value; + } } } } diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs index ce62ac21ea..7d97e6b6f7 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs @@ -22,30 +22,27 @@ public TestNodeResultsState(long id) private readonly TestDetailState _summaryDetail; private readonly ConcurrentDictionary _testNodeProgressStates = new(); - public void AddRunningTestNode(int id, string uid, string name, IStopwatch stopwatch) - { - _testNodeProgressStates[uid] = new TestDetailState(id, stopwatch, name); - UpdateSummaryDetail(); - } + public int Count => _testNodeProgressStates.Count; - public void RemoveRunningTestNode(string uid) - { - _testNodeProgressStates.TryRemove(uid, out _); - UpdateSummaryDetail(); - } + public void AddRunningTestNode(int id, string uid, string name, IStopwatch stopwatch) => _testNodeProgressStates[uid] = new TestDetailState(id, stopwatch, name); - private void UpdateSummaryDetail() - => _summaryDetail.Text = _testNodeProgressStates.Count > 5 - ? $"... {string.Format(CultureInfo.CurrentCulture, PlatformResources.MoreTestsRunning, _testNodeProgressStates.Count)}" + public void RemoveRunningTestNode(string uid) => _testNodeProgressStates.TryRemove(uid, out _); + + private void UpdateSummaryDetail(int maxCount) + => _summaryDetail.Text = _testNodeProgressStates.Count > maxCount + ? $"... {string.Format(CultureInfo.CurrentCulture, PlatformResources.MoreTestsRunning, _testNodeProgressStates.Count - maxCount)}" : string.Empty; - // Note: Show up to 5 long running tests per project. - public IEnumerable GetRunningTasks() + public TestDetailState? GetFirstRunningTask() => _testNodeProgressStates.FirstOrDefault().Value; + + public IEnumerable GetRunningTasks(int maxCount) { + UpdateSummaryDetail(maxCount); + IEnumerable sortedDetails = _testNodeProgressStates .Select(d => d.Value) .OrderBy(d => d.Stopwatch?.Elapsed ?? TimeSpan.Zero) - .Take(5); + .Take(maxCount); foreach (TestDetailState? detail in sortedDetails) { From f3f846984880a8041fc2bcec9c4dad355767ff69 Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Wed, 4 Dec 2024 21:36:43 +0100 Subject: [PATCH 08/18] Do not show the "... and 1 more test" message --- .../Terminal/TestNodeResultsState.cs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs index 7d97e6b6f7..3afbee9478 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs @@ -28,28 +28,33 @@ public TestNodeResultsState(long id) public void RemoveRunningTestNode(string uid) => _testNodeProgressStates.TryRemove(uid, out _); - private void UpdateSummaryDetail(int maxCount) - => _summaryDetail.Text = _testNodeProgressStates.Count > maxCount - ? $"... {string.Format(CultureInfo.CurrentCulture, PlatformResources.MoreTestsRunning, _testNodeProgressStates.Count - maxCount)}" - : string.Empty; - public TestDetailState? GetFirstRunningTask() => _testNodeProgressStates.FirstOrDefault().Value; public IEnumerable GetRunningTasks(int maxCount) { - UpdateSummaryDetail(maxCount); + + bool tooManyItems = _testNodeProgressStates.Count > maxCount; IEnumerable sortedDetails = _testNodeProgressStates .Select(d => d.Value) - .OrderBy(d => d.Stopwatch?.Elapsed ?? TimeSpan.Zero) - .Take(maxCount); + .OrderBy(d => d.Stopwatch?.Elapsed ?? TimeSpan.Zero); + + if (tooManyItems) + { + // Note: If there's too many items to display, the summary will take up one line. + // As such, we can only take maxCount - 1 items. + int itemsToTake = maxCount - 1; + _summaryDetail.Text = + $"... {string.Format(CultureInfo.CurrentCulture, PlatformResources.MoreTestsRunning, _testNodeProgressStates.Count - itemsToTake)}"; + sortedDetails = sortedDetails.Take(itemsToTake); + } foreach (TestDetailState? detail in sortedDetails) { yield return detail; } - if (!RoslynString.IsNullOrEmpty(_summaryDetail.Text)) + if (tooManyItems) { yield return _summaryDetail; } From 7fb23408bd025c275865fb8cdddac3c15aea69d7 Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Thu, 5 Dec 2024 00:46:21 +0100 Subject: [PATCH 09/18] Only show tests longer than 1s --- .../Terminal/TestNodeResultsState.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs index 3afbee9478..3d7f379aca 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs @@ -32,12 +32,17 @@ public TestNodeResultsState(long id) public IEnumerable GetRunningTasks(int maxCount) { + // Note: Do not show tasks that complete in less than 1s, to avoid refreshing the UI too often. + // Tests that took more than 1s to complete are much more likely to be longer running. + var minimumTime = TimeSpan.FromSeconds(1); - bool tooManyItems = _testNodeProgressStates.Count > maxCount; - - IEnumerable sortedDetails = _testNodeProgressStates + var sortedDetails = _testNodeProgressStates .Select(d => d.Value) - .OrderBy(d => d.Stopwatch?.Elapsed ?? TimeSpan.Zero); + .Where(d => d.Stopwatch?.Elapsed > minimumTime) + .OrderBy(d => d.Stopwatch?.Elapsed ?? TimeSpan.Zero) + .ToList(); + + bool tooManyItems = sortedDetails.Count > maxCount; if (tooManyItems) { @@ -45,8 +50,8 @@ public IEnumerable GetRunningTasks(int maxCount) // As such, we can only take maxCount - 1 items. int itemsToTake = maxCount - 1; _summaryDetail.Text = - $"... {string.Format(CultureInfo.CurrentCulture, PlatformResources.MoreTestsRunning, _testNodeProgressStates.Count - itemsToTake)}"; - sortedDetails = sortedDetails.Take(itemsToTake); + $"... {string.Format(CultureInfo.CurrentCulture, PlatformResources.MoreTestsRunning, sortedDetails.Count - itemsToTake)}"; + sortedDetails = sortedDetails.Take(itemsToTake).ToList(); } foreach (TestDetailState? detail in sortedDetails) From 5aed36c5a68eea3862819a4378f045662d3f1682 Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Thu, 5 Dec 2024 16:45:12 +0100 Subject: [PATCH 10/18] Remove 1s limit when showing active tests --- .../OutputDevice/Terminal/TestNodeResultsState.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs index 3d7f379aca..f03eb442ec 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs @@ -32,13 +32,8 @@ public TestNodeResultsState(long id) public IEnumerable GetRunningTasks(int maxCount) { - // Note: Do not show tasks that complete in less than 1s, to avoid refreshing the UI too often. - // Tests that took more than 1s to complete are much more likely to be longer running. - var minimumTime = TimeSpan.FromSeconds(1); - var sortedDetails = _testNodeProgressStates .Select(d => d.Value) - .Where(d => d.Stopwatch?.Elapsed > minimumTime) .OrderBy(d => d.Stopwatch?.Elapsed ?? TimeSpan.Zero) .ToList(); From 7d1305c1ccd1b57082855aa64c7a300e5a643503 Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Thu, 5 Dec 2024 16:45:21 +0100 Subject: [PATCH 11/18] Add an option to disable display of active tests --- .../OutputDevice/Terminal/TerminalTestReporter.cs | 15 +++++++++++---- .../Terminal/TerminalTestReporterOptions.cs | 5 +++++ .../OutputDevice/TerminalOutputDevice.cs | 1 + 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs index f8ee33c668..7b25155f19 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs @@ -422,7 +422,10 @@ internal void TestCompleted( { TestProgressState asm = _assemblies[$"{assembly}|{targetFramework}|{architecture}|{executionId}"]; - asm.TestNodeResultsState?.RemoveRunningTestNode(testNodeUid); + if (_options.ShowActiveTests) + { + asm.TestNodeResultsState?.RemoveRunningTestNode(testNodeUid); + } switch (outcome) { @@ -998,9 +1001,13 @@ public void TestInProgress( { TestProgressState asm = _assemblies[$"{assembly}|{targetFramework}|{architecture}|{executionId}"]; - asm.TestNodeResultsState ??= new(_counter++); - asm.TestNodeResultsState.AddRunningTestNode( - _counter++, testNodeUid, $"{displayName}", CreateStopwatch()); + if (_options.ShowActiveTests) + { + asm.TestNodeResultsState ??= new(_counter++); + asm.TestNodeResultsState.AddRunningTestNode( + _counter++, testNodeUid, $"{displayName}", CreateStopwatch()); + } + _terminalWithProgress.UpdateWorker(asm.SlotIndex); } } diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporterOptions.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporterOptions.cs index d2e84bc340..478453758d 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporterOptions.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporterOptions.cs @@ -37,6 +37,11 @@ internal sealed class TerminalTestReporterOptions /// public Func ShowProgress { get; init; } = () => true; + /// + /// Gets a value indicating whether the active tests should be visible when the progress is shown. + /// + public bool ShowActiveTests { get; init; } + /// /// Gets a value indicating whether we should use ANSI escape codes or disable them. When true the capabilities of the console are autodetected. /// diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs index b27a9c56ec..81cf00fdd1 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs @@ -160,6 +160,7 @@ public Task InitializeAsync() ShowPassedTests = showPassed, MinimumExpectedTests = PlatformCommandLineProvider.GetMinimumExpectedTests(_commandLineOptions), UseAnsi = !noAnsi, + ShowActiveTests = true, ShowProgress = shouldShowProgress, }); From eb814446ed35686786f3d3fc640e992b383c35ea Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Thu, 5 Dec 2024 16:46:32 +0100 Subject: [PATCH 12/18] Small refactorings --- .../Terminal/AnsiTerminalTestProgressFrame.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs index 957f934bd5..35cd1f7e38 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs @@ -310,19 +310,19 @@ private List GenerateLinesToRender(TestProgressState?[] progress) // Note: We want to render the list of active tests, but this can easily fill up the full screen. // As such, we should balance the number of active tests shown per project. // We do this by distributing the remaining lines for each projects. - var progressItems = progress.OfType().ToList(); - int linesToDistribute = (int)(Height * 0.7) - 1 - progressItems.Count; - var detailItems = new IEnumerable[progressItems.Count]; - var sortedItemsI = Enumerable.Range(0, progressItems.Count).OrderBy(i => progressItems[i].TestNodeResultsState?.Count ?? 0).ToList(); + TestProgressState[] progressItems = progress.OfType().ToArray(); + int linesToDistribute = (int)(Height * 0.7) - 1 - progressItems.Length; + var detailItems = new IEnumerable[progressItems.Length]; + IEnumerable sortedItemsIndices = Enumerable.Range(0, progressItems.Length).OrderBy(i => progressItems[i].TestNodeResultsState?.Count ?? 0); - foreach (int sortedItemIndex in sortedItemsI) + foreach (int sortedItemIndex in sortedItemsIndices) { detailItems[sortedItemIndex] = progressItems[sortedItemIndex].TestNodeResultsState?.GetRunningTasks( - linesToDistribute / progressItems.Count) + linesToDistribute / progressItems.Length) ?? Array.Empty(); } - for (int progressI = 0; progressI < progressItems.Count; progressI++) + for (int progressI = 0; progressI < progressItems.Length; progressI++) { linesToRender.Add(progressItems[progressI]); linesToRender.AddRange(detailItems[progressI]); @@ -333,7 +333,7 @@ private List GenerateLinesToRender(TestProgressState?[] progress) public void Clear() => RenderedLines?.Clear(); - internal class RenderedProgressItem + internal sealed class RenderedProgressItem { public RenderedProgressItem(long id, long version) { From d2b9c822aebe6d70942af80c4116900580f5296e Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Thu, 5 Dec 2024 17:02:12 +0100 Subject: [PATCH 13/18] Show "{0} tests running" if no active tests can fit on the screen --- .../OutputDevice/Terminal/TestNodeResultsState.cs | 6 +++++- .../Resources/PlatformResources.resx | 7 +++++-- .../Resources/xlf/PlatformResources.cs.xlf | 7 ++++++- .../Resources/xlf/PlatformResources.de.xlf | 7 ++++++- .../Resources/xlf/PlatformResources.es.xlf | 7 ++++++- .../Resources/xlf/PlatformResources.fr.xlf | 7 ++++++- .../Resources/xlf/PlatformResources.it.xlf | 7 ++++++- .../Resources/xlf/PlatformResources.ja.xlf | 7 ++++++- .../Resources/xlf/PlatformResources.ko.xlf | 7 ++++++- .../Resources/xlf/PlatformResources.pl.xlf | 7 ++++++- .../Resources/xlf/PlatformResources.pt-BR.xlf | 7 ++++++- .../Resources/xlf/PlatformResources.ru.xlf | 7 ++++++- .../Resources/xlf/PlatformResources.tr.xlf | 7 ++++++- .../Resources/xlf/PlatformResources.zh-Hans.xlf | 7 ++++++- .../Resources/xlf/PlatformResources.zh-Hant.xlf | 7 ++++++- 15 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs index f03eb442ec..95d16d4a9d 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs @@ -45,7 +45,11 @@ public IEnumerable GetRunningTasks(int maxCount) // As such, we can only take maxCount - 1 items. int itemsToTake = maxCount - 1; _summaryDetail.Text = - $"... {string.Format(CultureInfo.CurrentCulture, PlatformResources.MoreTestsRunning, sortedDetails.Count - itemsToTake)}"; + itemsToTake == 0 + // Note: If itemsToTake is 0, then we only show two lines, the project summary and the number of running tests. + ? string.Format(CultureInfo.CurrentCulture, PlatformResources.ActiveTestsRunning_FullTestsCount, sortedDetails.Count) + // If itemsToTake is larger, then we show the project summary, active tests, and the number of active tests that are not shown. + : $"... {string.Format(CultureInfo.CurrentCulture, PlatformResources.ActiveTestsRunning_MoreTestsCount, sortedDetails.Count - itemsToTake)}"; sortedDetails = sortedDetails.Take(itemsToTake).ToList(); } diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx b/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx index b1cceae944..8a899ed676 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx +++ b/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx @@ -671,9 +671,12 @@ Takes one argument as string in the format <value>[h|m|s] where 'value' is The configuration file '{0}' specified with '--config-file' could not be found. - + and {0} more + + {0} tests running + Starting test session. @@ -697,4 +700,4 @@ Takes one argument as string in the format <value>[h|m|s] where 'value' is Exception during the cancellation of request id '{0}' {0} is the request id - + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf index 27a81dccce..feb185a3da 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf @@ -386,11 +386,16 @@ Při použití protokolu jsonRpc byl očekáván parametr --client-port. - + and {0} more and {0} more + + {0} tests running + {0} tests running + + No serializer registered with ID '{0}' Není zaregistrovaný žádný serializátor s ID {0}. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf index 8ded22aff2..65f739dfd4 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf @@ -386,11 +386,16 @@ "--client-port" wird erwartet, wenn das jsonRpc-Protokoll verwendet wird. - + and {0} more and {0} more + + {0} tests running + {0} tests running + + No serializer registered with ID '{0}' Es ist kein Serialisierungsmodul mit der ID "{0}" registriert diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf index 520509c6f6..87532d09a3 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf @@ -386,11 +386,16 @@ Se esperaba --client-port cuando se usa el protocolo jsonRpc. - + and {0} more and {0} more + + {0} tests running + {0} tests running + + No serializer registered with ID '{0}' No hay ningún serializador registrado con el id. '{0}' diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf index a7774711f4..351c2180a9 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf @@ -386,11 +386,16 @@ Attendu --client-port attendu lorsque le protocole jsonRpc est utilisé. - + and {0} more and {0} more + + {0} tests running + {0} tests running + + No serializer registered with ID '{0}' Aucun sérialiseur inscrit avec l’ID « {0} » diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf index 850d53503d..30cac5a9e0 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf @@ -386,11 +386,16 @@ Previsto --client-port quando viene usato il protocollo jsonRpc. - + and {0} more and {0} more + + {0} tests running + {0} tests running + + No serializer registered with ID '{0}' Nessun serializzatore registrato con ID '{0}' diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf index f47194fbbf..5ddb7afd0e 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf @@ -386,11 +386,16 @@ jsonRpc プロトコルを使用する場合、--client-port が必要です。 - + and {0} more and {0} more + + {0} tests running + {0} tests running + + No serializer registered with ID '{0}' ID '{0}' で登録されたシリアライザーがありません diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf index 45586d5ba6..a5ec870434 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf @@ -386,11 +386,16 @@ jsonRpc 프로토콜을 사용하는 경우 --client-port가 필요합니다. - + and {0} more and {0} more + + {0} tests running + {0} tests running + + No serializer registered with ID '{0}' ID '{0}'(으)로 등록된 직렬 변환기가 없습니다. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf index dfe65de958..f9644eeb4e 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf @@ -386,11 +386,16 @@ Oczekiwano parametru --client-port, gdy jest używany protokół jsonRpc. - + and {0} more and {0} more + + {0} tests running + {0} tests running + + No serializer registered with ID '{0}' Nie zarejestrowano serializatora z identyfikatorem „{0}” diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf index 0279335ff1..2da937a42a 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf @@ -386,11 +386,16 @@ Esperado --client-port quando o protocolo jsonRpc é usado. - + and {0} more and {0} more + + {0} tests running + {0} tests running + + No serializer registered with ID '{0}' Nenhum serializador registrado com a ID “{0}” diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf index 60e5b3f2d5..48ade07606 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf @@ -386,11 +386,16 @@ Ожидается --client-port при использовании протокола jsonRpc. - + and {0} more and {0} more + + {0} tests running + {0} tests running + + No serializer registered with ID '{0}' Не зарегистрирован сериализатор с ИД "{0}" diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf index 3be23e03d0..fd52fb7ad5 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf @@ -386,11 +386,16 @@ JsonRpc protokolü kullanıldığında --client-port bekleniyordu. - + and {0} more and {0} more + + {0} tests running + {0} tests running + + No serializer registered with ID '{0}' '{0}' kimliğiyle kayıtlı seri hale getirici yok diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf index acf271381f..835629671c 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf @@ -386,11 +386,16 @@ 使用 jsonRpc 协议时应为 --client-port。 - + and {0} more and {0} more + + {0} tests running + {0} tests running + + No serializer registered with ID '{0}' 没有使用 ID“{0}”注册序列化程序 diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf index ff49887861..682a4fac66 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf @@ -386,11 +386,16 @@ 使用 jsonRpc 通訊協定時,必須是 --client-port。 - + and {0} more and {0} more + + {0} tests running + {0} tests running + + No serializer registered with ID '{0}' 沒有使用識別碼 '{0}' 註冊的序列化程式 From 7df3907ed5185acb480e64c96513f8d5a361d0af Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Fri, 6 Dec 2024 15:19:02 +0100 Subject: [PATCH 14/18] Add a progress indicator test --- .../Terminal/TerminalTestReporter.cs | 12 ++ .../Terminal/TestNodeResultsState.cs | 2 +- .../TestProgressStateAwareTerminal.cs | 6 + .../Terminal/TerminalTestReporterTests.cs | 113 +++++++++++++++++- 4 files changed, 131 insertions(+), 2 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs index 7b25155f19..9239e05274 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs @@ -31,6 +31,18 @@ internal sealed partial class TerminalTestReporter : IDisposable internal Func CreateStopwatch { get; set; } = SystemStopwatch.StartNew; + internal event EventHandler OnProgressStartUpdate + { + add => _terminalWithProgress.OnProgressStartUpdate += value; + remove => _terminalWithProgress.OnProgressStartUpdate -= value; + } + + internal event EventHandler OnProgressStopUpdate + { + add => _terminalWithProgress.OnProgressStopUpdate += value; + remove => _terminalWithProgress.OnProgressStopUpdate -= value; + } + private readonly ConcurrentDictionary _assemblies = new(); private readonly List _artifacts = new(); diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs index 95d16d4a9d..d2532fdcd7 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs @@ -34,7 +34,7 @@ public IEnumerable GetRunningTasks(int maxCount) { var sortedDetails = _testNodeProgressStates .Select(d => d.Value) - .OrderBy(d => d.Stopwatch?.Elapsed ?? TimeSpan.Zero) + .OrderByDescending(d => d.Stopwatch?.Elapsed ?? TimeSpan.Zero) .ToList(); bool tooManyItems = sortedDetails.Count > maxCount; diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressStateAwareTerminal.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressStateAwareTerminal.cs index 3ce26d97ab..d369d99059 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressStateAwareTerminal.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressStateAwareTerminal.cs @@ -31,6 +31,10 @@ internal sealed partial class TestProgressStateAwareTerminal : IDisposable private Thread? _refresher; private long _counter; + public event EventHandler? OnProgressStartUpdate; + + public event EventHandler? OnProgressStopUpdate; + /// /// The thread proc. /// @@ -42,6 +46,7 @@ private void ThreadProc() { lock (_lock) { + OnProgressStartUpdate?.Invoke(this, EventArgs.Empty); _terminal.StartUpdate(); try { @@ -50,6 +55,7 @@ private void ThreadProc() finally { _terminal.StopUpdate(); + OnProgressStopUpdate?.Invoke(this, EventArgs.Empty); } } } diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs index f822211466..bfbd067a8a 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs @@ -7,6 +7,8 @@ using Microsoft.Testing.Platform.Helpers; using Microsoft.Testing.Platform.OutputDevice.Terminal; +using Moq; + namespace Microsoft.Testing.Platform.UnitTests; [TestGroup] @@ -167,12 +169,121 @@ Oh no! Assert.AreEqual(expected, ShowEscape(output)); } + public void OutputProgressFrameIsCorrect() + { + var stringBuilderConsole = new StringBuilderConsole(); + var stopwatchFactory = new StopwatchFactory(); + var terminalReporter = new TerminalTestReporter(stringBuilderConsole, new TerminalTestReporterOptions + { + ShowPassedTests = () => true, + UseAnsi = true, + ForceAnsi = true, + + ShowActiveTests = true, + ShowAssembly = false, + ShowAssemblyStartAndComplete = false, + ShowProgress = () => true, + }) + { + CreateStopwatch = stopwatchFactory.CreateStopwatch, + }; + + var startHandle = new AutoResetEvent(initialState: false); + var stopHandle = new AutoResetEvent(initialState: false); + + // Note: Ensure that we disable the timer updates, so that we can have deterministic output. + terminalReporter.OnProgressStartUpdate += (sender, args) => startHandle.WaitOne(); + terminalReporter.OnProgressStopUpdate += (sender, args) => stopHandle.Set(); + + DateTimeOffset startTime = DateTimeOffset.MinValue; + DateTimeOffset endTime = DateTimeOffset.MaxValue; + terminalReporter.TestExecutionStarted(startTime, 1, isDiscovery: false); + + string targetFramework = "net8.0"; + string architecture = "x64"; + string assembly = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\work\assembly.dll" : "/mnt/work/assembly.dll"; + string folder = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\work\" : "/mnt/work/"; + string folderNoSlash = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\work" : "/mnt/work"; + string folderLink = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:/work/" : "mnt/work/"; + string folderLinkNoSlash = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:/work" : "mnt/work"; + + terminalReporter.AssemblyRunStarted(assembly, targetFramework, architecture, executionId: null); + string standardOutput = "Hello!"; + string errorOutput = "Oh no!"; + + // Note: Add 1ms to make the order of the progress frame deterministic. + // Otherwise all tests that run for 1m31s could show in any order. + terminalReporter.TestInProgress(assembly, targetFramework, architecture, testNodeUid: "PassedTest1", displayName: "PassedTest1", executionId: null); + stopwatchFactory.AddTime(TimeSpan.FromMilliseconds(1)); + terminalReporter.TestInProgress(assembly, targetFramework, architecture, testNodeUid: "SkippedTest1", displayName: "SkippedTest1", executionId: null); + stopwatchFactory.AddTime(TimeSpan.FromMilliseconds(1)); + terminalReporter.TestInProgress(assembly, targetFramework, architecture, testNodeUid: "InProgressTest1", displayName: "InProgressTest1", executionId: null); + stopwatchFactory.AddTime(TimeSpan.FromMinutes(1)); + terminalReporter.TestInProgress(assembly, targetFramework, architecture, testNodeUid: "InProgressTest2", displayName: "InProgressTest2", executionId: null); + stopwatchFactory.AddTime(TimeSpan.FromSeconds(30)); + terminalReporter.TestInProgress(assembly, targetFramework, architecture, testNodeUid: "InProgressTest3", displayName: "InProgressTest3", executionId: null); + stopwatchFactory.AddTime(TimeSpan.FromSeconds(1)); + + terminalReporter.TestCompleted(assembly, targetFramework, architecture, executionId: null, testNodeUid: "PassedTest1", "PassedTest1", TestOutcome.Passed, TimeSpan.FromSeconds(10), + errorMessage: null, exception: null, expected: null, actual: null, standardOutput, errorOutput); + terminalReporter.TestCompleted(assembly, targetFramework, architecture, executionId: null, testNodeUid: "SkippedTest1", "SkippedTest1", TestOutcome.Skipped, TimeSpan.FromSeconds(10), + errorMessage: null, exception: null, expected: null, actual: null, standardOutput, errorOutput); + + string output = stringBuilderConsole.Output; + startHandle.Set(); + stopHandle.WaitOne(); + + // Note: The progress is drawn after each completed event. + string expected = $""" + ␛]9;4;3;␛\␛[?25l␛[92mpassed␛[m PassedTest1␛[90m ␛[90m(10s 000ms)␛[m + ␛[90m Standard output + Hello! + Error output + Oh no! + ␛[m + [␛[92m✓1␛[m/␛[91mx0␛[m/␛[93m↓0␛[m] assembly.dll (net8.0|x64)␛[2147483640G(1m 31s) + SkippedTest1␛[2147483640G(1m 31s) + InProgressTest1␛[2147483640G(1m 31s) + InProgressTest2␛[2147483643G(31s) + InProgressTest3␛[2147483644G(1s) + ␛[7F + ␛[J␛[93mskipped␛[m SkippedTest1␛[90m ␛[90m(10s 000ms)␛[m + ␛[90m Standard output + Hello! + Error output + Oh no! + ␛[m + [␛[92m✓1␛[m/␛[91mx0␛[m/␛[93m↓1␛[m] assembly.dll (net8.0|x64)␛[2147483640G(1m 31s) + InProgressTest1␛[2147483640G(1m 31s) + InProgressTest2␛[2147483643G(31s) + InProgressTest3␛[2147483644G(1s) + + """; + + Assert.AreEqual(expected, ShowEscape(output)); + } + private static string? ShowEscape(string? text) { string visibleEsc = "\x241b"; return text?.Replace(AnsiCodes.Esc, visibleEsc); } + internal sealed class StopwatchFactory + { + private TimeSpan _currentTime = TimeSpan.Zero; + + public void AddTime(TimeSpan time) => _currentTime += time; + + public IStopwatch CreateStopwatch() + { + TimeSpan createTime = _currentTime; + var stopwatch = new Mock(); + stopwatch.SetupGet(s => s.Elapsed).Returns(() => _currentTime - createTime); + return stopwatch.Object; + } + } + internal class StringBuilderConsole : IConsole { private readonly StringBuilder _output = new(); @@ -181,7 +292,7 @@ internal class StringBuilderConsole : IConsole public int BufferWidth => int.MinValue; - public bool IsOutputRedirected => throw new NotImplementedException(); + public bool IsOutputRedirected => false; public string Output => _output.ToString(); From 97134093eadcf1c2e4ce845da636cca3f396bb33 Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Fri, 6 Dec 2024 15:30:19 +0100 Subject: [PATCH 15/18] Update src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Amaury Levé --- .../OutputDevice/Terminal/TerminalTestReporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs index 9239e05274..a862a9b9f2 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs @@ -1017,7 +1017,7 @@ public void TestInProgress( { asm.TestNodeResultsState ??= new(_counter++); asm.TestNodeResultsState.AddRunningTestNode( - _counter++, testNodeUid, $"{displayName}", CreateStopwatch()); + _counter++, testNodeUid, displayName, CreateStopwatch()); } _terminalWithProgress.UpdateWorker(asm.SlotIndex); From 366b4281cb5c9da6e89a8b37c2814d8c7463db27 Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Fri, 6 Dec 2024 16:17:57 +0100 Subject: [PATCH 16/18] Fix terminal logger reporter test for MacOS --- .../OutputDevice/Terminal/TerminalTestReporterTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs index bfbd067a8a..4172bfbfd6 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs @@ -233,9 +233,13 @@ public void OutputProgressFrameIsCorrect() startHandle.Set(); stopHandle.WaitOne(); + // Note: On MacOS the busy indicator is not rendered. + bool useBusyIndicator = !RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + string busyIndicatorString = useBusyIndicator ? "␛]9;4;3;␛\\" : string.Empty; + // Note: The progress is drawn after each completed event. string expected = $""" - ␛]9;4;3;␛\␛[?25l␛[92mpassed␛[m PassedTest1␛[90m ␛[90m(10s 000ms)␛[m + {busyIndicatorString}␛[?25l␛[92mpassed␛[m PassedTest1␛[90m ␛[90m(10s 000ms)␛[m ␛[90m Standard output Hello! Error output From e2fa5d6fef9761df594c58c8e5e3a16b402d14b9 Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Mon, 9 Dec 2024 12:47:53 +0100 Subject: [PATCH 17/18] Use Interlocked for _counter updates --- .../OutputDevice/Terminal/TerminalTestReporter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs index a862a9b9f2..44b067a99c 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs @@ -182,7 +182,7 @@ private TestProgressState GetOrAddAssemblyRun(string assembly, string? targetFra return _assemblies.GetOrAdd(key, _ => { IStopwatch sw = CreateStopwatch(); - var assemblyRun = new TestProgressState(_counter++, assembly, targetFramework, architecture, sw); + var assemblyRun = new TestProgressState(Interlocked.Increment(ref _counter), assembly, targetFramework, architecture, sw); int slotIndex = _terminalWithProgress.AddWorker(assemblyRun); assemblyRun.SlotIndex = slotIndex; @@ -1015,9 +1015,9 @@ public void TestInProgress( if (_options.ShowActiveTests) { - asm.TestNodeResultsState ??= new(_counter++); + asm.TestNodeResultsState ??= new(Interlocked.Increment(ref _counter)); asm.TestNodeResultsState.AddRunningTestNode( - _counter++, testNodeUid, displayName, CreateStopwatch()); + Interlocked.Increment(ref _counter), testNodeUid, displayName, CreateStopwatch()); } _terminalWithProgress.UpdateWorker(asm.SlotIndex); From b3e5528e3adcbb64887311b3a8507be05142b3f7 Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Mon, 9 Dec 2024 12:59:29 +0100 Subject: [PATCH 18/18] Implement the stopwatch directly as a class --- .../Terminal/TerminalTestReporterTests.cs | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs index 4172bfbfd6..d7bbe0f104 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs @@ -7,8 +7,6 @@ using Microsoft.Testing.Platform.Helpers; using Microsoft.Testing.Platform.OutputDevice.Terminal; -using Moq; - namespace Microsoft.Testing.Platform.UnitTests; [TestGroup] @@ -279,12 +277,29 @@ internal sealed class StopwatchFactory public void AddTime(TimeSpan time) => _currentTime += time; - public IStopwatch CreateStopwatch() + public IStopwatch CreateStopwatch() => new MockStopwatch(this, _currentTime); + + internal sealed class MockStopwatch : IStopwatch { - TimeSpan createTime = _currentTime; - var stopwatch = new Mock(); - stopwatch.SetupGet(s => s.Elapsed).Returns(() => _currentTime - createTime); - return stopwatch.Object; + private readonly StopwatchFactory _factory; + + public MockStopwatch(StopwatchFactory factory, TimeSpan startTime) + { + _factory = factory; + StartTime = startTime; + } + + private TimeSpan StartTime { get; } + + public TimeSpan Elapsed => _factory._currentTime - StartTime; + + public void Start() + { + } + + public void Stop() + { + } } }