Skip to content

Commit 3d61873

Browse files
authored
Add ux for retry in dotnet test (#48075)
1 parent 33d6c5f commit 3d61873

20 files changed

+174
-43
lines changed

src/Cli/dotnet/Commands/Test/LocalizableStrings.resx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,4 +534,8 @@ dotnet.config is a name don't translate.</comment>
534534
<data name="CmdNumberName" xml:space="preserve">
535535
<value>NUMBER</value>
536536
</data>
537+
<data name="Try" xml:space="preserve">
538+
<value>try {0}</value>
539+
<comment>number or tries of the current test assembly when test assembly is being retried. {0} is number that starts at 1</comment>
540+
</data>
537541
</root>

src/Cli/dotnet/Commands/Test/Terminal/AnsiTerminalTestProgressFrame.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public void AppendTestWorkerProgress(TestProgressState progress, RenderedProgres
2929
int passed = progress.PassedTests;
3030
int failed = progress.FailedTests;
3131
int skipped = progress.SkippedTests;
32+
int retried = progress.RetriedFailedTests;
3233
int charsTaken = 0;
3334

3435
terminal.Append('[');
@@ -62,6 +63,20 @@ public void AppendTestWorkerProgress(TestProgressState progress, RenderedProgres
6263
terminal.Append(skippedText);
6364
charsTaken += skippedText.Length;
6465
terminal.ResetColor();
66+
67+
if (retried > 0)
68+
{
69+
terminal.Append('/');
70+
charsTaken++;
71+
terminal.SetColor(TerminalColor.Gray);
72+
terminal.Append('r');
73+
charsTaken++;
74+
string retriedText = retried.ToString(CultureInfo.CurrentCulture);
75+
terminal.Append(retriedText);
76+
charsTaken += retriedText.Length;
77+
terminal.ResetColor();
78+
}
79+
6580
terminal.Append(']');
6681
charsTaken++;
6782

src/Cli/dotnet/Commands/Test/Terminal/NonAnsiTerminal.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ public void RenderProgress(TestProgressState?[] progress)
177177
int passed = p.PassedTests;
178178
int failed = p.FailedTests;
179179
int skipped = p.SkippedTests;
180+
int retried = p.RetriedFailedTests;
180181

181182
// Use just ascii here, so we don't put too many restrictions on fonts needing to
182183
// properly show unicode, or logs being saved in particular encoding.
@@ -199,6 +200,15 @@ public void RenderProgress(TestProgressState?[] progress)
199200
Append('?');
200201
Append(skipped.ToString(CultureInfo.CurrentCulture));
201202
ResetColor();
203+
204+
if (retried > 0)
205+
{
206+
SetColor(TerminalColor.Gray);
207+
Append('r');
208+
Append(retried.ToString(CultureInfo.CurrentCulture));
209+
ResetColor();
210+
}
211+
202212
Append(']');
203213

204214
Append(' ');

src/Cli/dotnet/Commands/Test/Terminal/TerminalTestReporter.cs

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.CommandLine.Help;
66
using System.Globalization;
77
using System.Text.RegularExpressions;
8+
using Microsoft.CodeAnalysis;
89
using Microsoft.DotNet.Cli.Commands.Test;
910
using Microsoft.DotNet.Cli.Commands.Test.Terminal;
1011
using FlatException = Microsoft.DotNet.Cli.Commands.Test.Terminal.FlatException;
@@ -51,6 +52,7 @@ internal event EventHandler OnProgressStopUpdate
5152
private readonly uint? _originalConsoleMode;
5253
private bool _isDiscovery;
5354
private bool _isHelp;
55+
private bool _isRetry;
5456
private DateTimeOffset? _testExecutionStartTime;
5557

5658
private DateTimeOffset? _testExecutionEndTime;
@@ -152,28 +154,52 @@ public TerminalTestReporter(IConsole console, TerminalTestReporterOptions option
152154
_terminalWithProgress = terminalWithProgress;
153155
}
154156

155-
public void TestExecutionStarted(DateTimeOffset testStartTime, int workerCount, bool isDiscovery, bool isHelp)
157+
public void TestExecutionStarted(DateTimeOffset testStartTime, int workerCount, bool isDiscovery, bool isHelp, bool isRetry)
156158
{
157159
_isDiscovery = isDiscovery;
158160
_isHelp = isHelp;
161+
_isRetry = isRetry;
159162
_testExecutionStartTime = testStartTime;
160163
_terminalWithProgress.StartShowingProgress(workerCount);
161164
}
162165

163-
public void AssemblyRunStarted(string assembly, string? targetFramework, string? architecture, string? executionId)
166+
public void AssemblyRunStarted(string assembly, string? targetFramework, string? architecture, string? executionId, string? instanceId)
164167
{
168+
var assemblyRun = GetOrAddAssemblyRun(assembly, targetFramework, architecture, executionId);
169+
assemblyRun.Tries.Add(instanceId);
170+
171+
if (_isRetry)
172+
{
173+
// When we are retrying the new assembly run should ignore all previously failed tests and
174+
// clear all errors. We restarted the run and will retry all failed tests.
175+
//
176+
// In case of folded dynamic data tests we do not know how many tests we will get in each run,
177+
// if more or less, or the same amount as before,and we also will rerun tests that passed previously
178+
// because we are unable to run just a single test from that dynamic data source.
179+
// This will cause the total number of tests to differ between runs, and there is nothing we can do about it.
180+
assemblyRun.TotalTests -= assemblyRun.FailedTests;
181+
assemblyRun.RetriedFailedTests += assemblyRun.FailedTests;
182+
assemblyRun.FailedTests = 0;
183+
assemblyRun.ClearAllMessages();
184+
}
185+
165186
if (_options.ShowAssembly && _options.ShowAssemblyStartAndComplete)
166187
{
167188
_terminalWithProgress.WriteToTerminal(terminal =>
168189
{
190+
if (_isRetry)
191+
{
192+
terminal.SetColor(TerminalColor.DarkGray);
193+
terminal.Append($"({string.Format(LocalizableStrings.Try, assemblyRun.Tries.Count)}) ");
194+
terminal.ResetColor();
195+
}
196+
169197
terminal.Append(_isDiscovery ? LocalizableStrings.DiscoveringTestsFrom : LocalizableStrings.RunningTestsFrom);
170198
terminal.Append(' ');
171199
AppendAssemblyLinkTargetFrameworkAndArchitecture(terminal, assembly, targetFramework, architecture);
172200
terminal.AppendLine();
173201
});
174202
}
175-
176-
GetOrAddAssemblyRun(assembly, targetFramework, architecture, executionId);
177203
}
178204

179205
private TestProgressState GetOrAddAssemblyRun(string assembly, string? targetFramework, string? architecture, string? executionId)
@@ -306,6 +332,7 @@ private void AppendTestRunSummary(ITerminal terminal)
306332
int failed = _assemblies.Values.Sum(t => t.FailedTests);
307333
int passed = _assemblies.Values.Sum(t => t.PassedTests);
308334
int skipped = _assemblies.Values.Sum(t => t.SkippedTests);
335+
int retried = _assemblies.Values.Sum(t => t.RetriedFailedTests);
309336
int error = _assemblies.Values.Sum(t => !t.Success && (t.TotalTests == 0 || t.FailedTests == 0) ? 1 : 0);
310337
TimeSpan runDuration = _testExecutionStartTime != null && _testExecutionEndTime != null ? (_testExecutionEndTime - _testExecutionStartTime).Value : TimeSpan.Zero;
311338

@@ -316,6 +343,7 @@ private void AppendTestRunSummary(ITerminal terminal)
316343

317344
string errorText = $"{SingleIndentation}error: {error}";
318345
string totalText = $"{SingleIndentation}total: {total}";
346+
string retriedText = $" (+{retried} retried)";
319347
string failedText = $"{SingleIndentation}failed: {failed}";
320348
string passedText = $"{SingleIndentation}succeeded: {passed}";
321349
string skippedText = $"{SingleIndentation}skipped: {skipped}";
@@ -330,7 +358,15 @@ private void AppendTestRunSummary(ITerminal terminal)
330358
}
331359

332360
terminal.ResetColor();
333-
terminal.AppendLine(totalText);
361+
terminal.Append(totalText);
362+
if (retried > 0)
363+
{
364+
terminal.SetColor(TerminalColor.DarkGray);
365+
terminal.Append(retriedText);
366+
terminal.ResetColor();
367+
}
368+
terminal.AppendLine();
369+
334370
if (colorizeFailed)
335371
{
336372
terminal.SetColor(TerminalColor.Red);
@@ -405,44 +441,12 @@ private static void AppendAssemblyResult(ITerminal terminal, bool succeeded, int
405441
}
406442
}
407443

408-
internal void TestCompleted(
409-
string assembly,
410-
string? targetFramework,
411-
string? architecture,
412-
string? executionId,
413-
string testNodeUid,
414-
string displayName,
415-
TestOutcome outcome,
416-
TimeSpan duration,
417-
string? errorMessage,
418-
Exception? exception,
419-
string? expected,
420-
string? actual,
421-
string? standardOutput,
422-
string? errorOutput)
423-
{
424-
FlatException[] flatExceptions = ExceptionFlattener.Flatten(errorMessage, exception);
425-
TestCompleted(
426-
assembly,
427-
targetFramework,
428-
architecture,
429-
executionId,
430-
testNodeUid,
431-
displayName,
432-
outcome,
433-
duration,
434-
flatExceptions,
435-
expected,
436-
actual,
437-
standardOutput,
438-
errorOutput);
439-
}
440-
441444
internal void TestCompleted(
442445
string assembly,
443446
string? targetFramework,
444447
string? architecture,
445448
string? executionId,
449+
string instanceId,
446450
string testNodeUid,
447451
string displayName,
448452
TestOutcome outcome,
@@ -454,6 +458,7 @@ internal void TestCompleted(
454458
string? errorOutput)
455459
{
456460
TestProgressState asm = _assemblies[$"{assembly}|{targetFramework}|{architecture}|{executionId}"];
461+
var attempt = asm.Tries.Count;
457462

458463
if (_options.ShowActiveTests)
459464
{
@@ -479,12 +484,21 @@ internal void TestCompleted(
479484
break;
480485
}
481486

487+
if (_isRetry && asm.Tries.Count > 1 && outcome == TestOutcome.Passed)
488+
{
489+
// This is a retry of a test, and the test succeeded, so these tests are potentially flaky.
490+
// Tests that come from dynamic data sources and previously succeeded will also run on the second attempt,
491+
// and most likely will succeed as well, so we will get them here, even though they are probably not flaky.
492+
asm.FlakyTests.Add(testNodeUid);
493+
494+
}
482495
_terminalWithProgress.UpdateWorker(asm.SlotIndex);
483496
if (outcome != TestOutcome.Passed || GetShowPassedTests())
484497
{
485498
_terminalWithProgress.WriteToTerminal(terminal => RenderTestCompleted(
486499
terminal,
487500
assembly,
501+
attempt,
488502
targetFramework,
489503
architecture,
490504
displayName,
@@ -507,6 +521,7 @@ private bool GetShowPassedTests()
507521
internal /* for testing */ void RenderTestCompleted(
508522
ITerminal terminal,
509523
string assembly,
524+
int attempt,
510525
string? targetFramework,
511526
string? architecture,
512527
string displayName,
@@ -541,6 +556,11 @@ private bool GetShowPassedTests()
541556

542557
terminal.SetColor(color);
543558
terminal.Append(outcomeText);
559+
if (_isRetry)
560+
{
561+
terminal.SetColor(TerminalColor.DarkGray);
562+
terminal.Append($" ({string.Format(LocalizableStrings.Try, attempt)})");
563+
}
544564
terminal.ResetColor();
545565
terminal.Append(' ');
546566
terminal.Append(displayName);
@@ -1033,6 +1053,7 @@ public void TestInProgress(
10331053
string? targetFramework,
10341054
string? architecture,
10351055
string testNodeUid,
1056+
string instanceId,
10361057
string displayName,
10371058
string? executionId)
10381059
{

src/Cli/dotnet/Commands/Test/Terminal/TestProgressState.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ internal sealed class TestProgressState(long id, string assembly, string? target
2727

2828
public int TotalTests { get; internal set; }
2929

30+
public int RetriedFailedTests { get; internal set; }
31+
3032
public TestNodeResultsState? TestNodeResultsState { get; internal set; }
3133

3234
public int SlotIndex { get; internal set; }
@@ -39,9 +41,17 @@ internal sealed class TestProgressState(long id, string assembly, string? target
3941
public int? ExitCode { get; internal set; }
4042
public bool Success { get; internal set; }
4143

44+
public List<string> Tries { get; } = [];
45+
public HashSet<string> FlakyTests { get; } = [];
46+
4247
internal void AddError(string text)
4348
=> Messages.Add(new ErrorMessage(text));
4449

4550
internal void AddWarning(string text)
4651
=> Messages.Add(new WarningMessage(text));
52+
53+
internal void ClearAllMessages()
54+
{
55+
Messages.Clear();
56+
}
4757
}

src/Cli/dotnet/Commands/Test/TestApplicationEventHandlers.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,19 @@ namespace Microsoft.DotNet.Cli.Commands.Test;
99

1010
internal sealed class TestApplicationsEventHandlers(TerminalTestReporter output) : IDisposable
1111
{
12-
private readonly ConcurrentDictionary<TestApplication, (string ModulePath, string TargetFramework, string Architecture, string ExecutionId)> _executions = new();
12+
private readonly ConcurrentDictionary<TestApplication, (string ModulePath, string TargetFramework, string Architecture, string ExecutionId, string InstanceId)> _executions = new();
1313
private readonly TerminalTestReporter _output = output;
1414

1515
public void OnHandshakeReceived(object sender, HandshakeArgs args)
1616
{
1717
var testApplication = (TestApplication)sender;
1818
var executionId = args.Handshake.Properties[HandshakeMessagePropertyNames.ExecutionId];
19+
var instanceId = args.Handshake.Properties[HandshakeMessagePropertyNames.InstanceId];
1920
var arch = args.Handshake.Properties[HandshakeMessagePropertyNames.Architecture]?.ToLower();
2021
var tfm = TargetFrameworkParser.GetShortTargetFramework(args.Handshake.Properties[HandshakeMessagePropertyNames.Framework]);
21-
(string ModulePath, string TargetFramework, string Architecture, string ExecutionId) appInfo = new(testApplication.Module.RunProperties.RunCommand, tfm, arch, executionId);
22+
(string ModulePath, string TargetFramework, string Architecture, string ExecutionId, string InstanceId) appInfo = new(testApplication.Module.RunProperties.RunCommand, tfm, arch, executionId, instanceId);
2223
_executions[testApplication] = appInfo;
23-
_output.AssemblyRunStarted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId);
24+
_output.AssemblyRunStarted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, appInfo.InstanceId);
2425

2526
LogHandshake(args);
2627
}
@@ -63,6 +64,7 @@ public void OnTestResultsReceived(object sender, TestResultEventArgs args)
6364
foreach (var testResult in args.SuccessfulTestResults)
6465
{
6566
_output.TestCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId,
67+
args.InstanceId,
6668
testResult.Uid,
6769
testResult.DisplayName,
6870
ToOutcome(testResult.State),
@@ -76,7 +78,7 @@ public void OnTestResultsReceived(object sender, TestResultEventArgs args)
7678

7779
foreach (var testResult in args.FailedTestResults)
7880
{
79-
_output.TestCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId,
81+
_output.TestCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, args.InstanceId,
8082
testResult.Uid,
8183
testResult.DisplayName,
8284
ToOutcome(testResult.State),

src/Cli/dotnet/Commands/Test/TestingPlatformCommand.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal partial class TestingPlatformCommand : CliCommand, ICustomHelp
1919

2020
private byte _cancelled;
2121
private bool _isDiscovery;
22+
private bool _isRetry;
2223

2324
public TestingPlatformCommand(string name, string description = null) : base(name, description)
2425
{
@@ -88,6 +89,9 @@ private void PrepareEnvironment(ParseResult parseResult, out TestOptions testOpt
8889
testOptions = GetTestOptions(parseResult, filterModeEnabled, isHelp: ContainsHelpOption(arguments));
8990

9091
_isDiscovery = ContainsListTestsOption(arguments);
92+
93+
// This is ugly, and we need to replace it by passing out some info from testing platform to inform us that some process level retry plugin is active.
94+
_isRetry = arguments.Contains("--retry-failed-tests");
9195
}
9296

9397
private void InitializeActionQueue(int degreeOfParallelism, TestOptions testOptions, bool isHelp)
@@ -126,7 +130,7 @@ private void InitializeOutput(int degreeOfParallelism, ParseResult parseResult,
126130
ShowAssemblyStartAndComplete = true,
127131
});
128132

129-
_output.TestExecutionStarted(DateTimeOffset.Now, degreeOfParallelism, _isDiscovery, isHelp);
133+
_output.TestExecutionStarted(DateTimeOffset.Now, degreeOfParallelism, _isDiscovery, isHelp, _isRetry);
130134
}
131135

132136
private void InitializeHelpActionQueue(int degreeOfParallelism, TestOptions testOptions)

src/Cli/dotnet/Commands/Test/xlf/LocalizableStrings.cs.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/Test/xlf/LocalizableStrings.de.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)