Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,27 +123,27 @@ private static void FormatStandardAndErrorOutput(ITerminal terminal, string? sta
terminal.ResetColor();
}

private void AppendAssemblyLinkTargetFrameworkAndArchitecture(ITerminal terminal)
private static void AppendAssemblyLinkTargetFrameworkAndArchitecture(ITerminal terminal, TestProgressState assemblyRun)
{
terminal.AppendLink(_assembly, lineNumber: null);
if (_targetFramework == null && _architecture == null)
terminal.AppendLink(assemblyRun.Assembly, lineNumber: null);
if (assemblyRun.TargetFramework == null && assemblyRun.Architecture == null)
{
return;
}

terminal.Append(" (");
if (_targetFramework != null)
if (assemblyRun.TargetFramework != null)
{
terminal.Append(_targetFramework);
if (_architecture != null)
terminal.Append(assemblyRun.TargetFramework);
if (assemblyRun.Architecture != null)
{
terminal.Append('|');
}
}

if (_architecture != null)
if (assemblyRun.Architecture != null)
{
terminal.Append(_architecture);
terminal.Append(assemblyRun.Architecture);
}

terminal.Append(')');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,57 +8,76 @@ namespace Microsoft.Testing.Platform.OutputDevice.Terminal;
[UnsupportedOSPlatform("browser")]
internal sealed partial class TerminalTestReporter
{
public void TestExecutionStarted(DateTimeOffset testStartTime, int workerCount, bool isDiscovery)
private bool _isHelp;

public void TestExecutionStarted(DateTimeOffset testStartTime, int workerCount, bool isDiscovery, bool isHelp, bool isRetry)
{
_isDiscovery = isDiscovery;
_isHelp = isHelp;
_testExecutionStartTime = testStartTime;
_terminalWithProgress.StartShowingProgress(workerCount);
}

public void AssemblyRunStarted()
=> GetOrAddAssemblyRun();
public void AssemblyRunStarted(string assembly, string? targetFramework, string? architecture, string executionId, string instanceId)
Comment thread
Evangelink marked this conversation as resolved.
// instanceId: reserved for the SDK orchestrator's instanceId-based retry-counting logic (follow-up PR).
// It is not used by the in-process host path, which registers a single assembly per run.
=> GetOrAddAssemblyRun(assembly, targetFramework, architecture, executionId);

private TestProgressState GetOrAddAssemblyRun()
private TestProgressState GetOrAddAssemblyRun(string assembly, string? targetFramework, string? architecture, string executionId)
{
if (_testProgressState is not null)
// NOTE: we intentionally do not use ConcurrentDictionary.GetOrAdd with a value factory here. GetOrAdd may
// invoke the factory more than once under contention and discard all but one result; that would allocate a
// worker slot (and bump _counter) for every losing race, leaking a fixed-size progress slot that is never
// RemoveWorker'd. Double-checked locking around AddWorker guarantees exactly one worker per executionId.
if (_assemblies.TryGetValue(executionId, out TestProgressState? result))
{
return _testProgressState;
return result;
}

lock (_lock)
{
if (_testProgressState is not null)
if (_assemblies.TryGetValue(executionId, out result))
{
Comment thread
Evangelink marked this conversation as resolved.
return _testProgressState;
return result;
}

IStopwatch sw = CreateStopwatch();
var assemblyRun = new TestProgressState(Interlocked.Increment(ref _counter), _assembly, _targetFramework, _architecture, sw, _isDiscovery);
int slotIndex = _terminalWithProgress.AddWorker(assemblyRun);
assemblyRun.SlotIndex = slotIndex;
_testProgressState = assemblyRun;
return assemblyRun;
result = new TestProgressState(Interlocked.Increment(ref _counter), assembly, targetFramework, architecture, sw, _isDiscovery);
int slotIndex = _terminalWithProgress.AddWorker(result);
result.SlotIndex = slotIndex;
_assemblies[executionId] = result;
return result;
}
}

internal void AssemblyRunCompleted()
internal void AssemblyRunCompleted(string executionId)
{
TestProgressState assemblyRun = GetOrAddAssemblyRun();
assemblyRun.Stopwatch.Stop();
if (!_assemblies.TryGetValue(executionId, out TestProgressState? assemblyRun))
{
return;
}

assemblyRun.Stopwatch.Stop();
_terminalWithProgress.RemoveWorker(assemblyRun.SlotIndex);
}

public void TestExecutionCompleted(DateTimeOffset endTime)
public void TestExecutionCompleted(DateTimeOffset endTime, int? exitCode)
{
_testExecutionEndTime = endTime;
_terminalWithProgress.StopShowingProgress();
Comment thread
Evangelink marked this conversation as resolved.

_terminalWithProgress.WriteToTerminal(_isDiscovery ? AppendTestDiscoverySummary : AppendTestRunSummary);
if (!_isHelp)
{
_terminalWithProgress.WriteToTerminal(_isDiscovery ? AppendTestDiscoverySummary : AppendTestRunSummary);
}

// This is relevant for HotReload scenarios. We want the next test sessions to start
// on a new TestProgressState
_testProgressState = null;
// This is relevant for HotReload scenarios. We want the next test sessions to start fresh, so we reset all
// per-run state here (after the summary above has consumed it): the per-assembly runs, the collected
// artifacts, and the cancellation flag. Otherwise a later session would re-print the previous session's
// artifacts or stay stuck in the aborted state.
_assemblies.Clear();
_artifacts.Clear();
WasCancelled = false;

_testExecutionStartTime = null;
_testExecutionEndTime = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ private void WriteMessage(string text, TerminalColor? color = null, int? padding
});

public void TestInProgress(
string executionId,
string testNodeUid,
string displayName)
{
if (_testProgressState is null)
if (!_assemblies.TryGetValue(executionId, out TestProgressState? asm))
{
throw ApplicationStateGuard.Unreachable();
}

TestProgressState asm = _testProgressState;
if (_options.ShowActiveTests)
{
asm.TestNodeResultsState ??= new(Interlocked.Increment(ref _counter));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ private void AppendTestRunSummary(ITerminal terminal)

terminal.AppendLine();

int totalTests = _testProgressState?.TotalTests ?? 0;
int totalFailedTests = _testProgressState?.FailedTests ?? 0;
int totalSkippedTests = _testProgressState?.SkippedTests ?? 0;
List<TestProgressState> assemblies = [.. _assemblies.Values.OrderBy(static a => a.Id)];

int totalTests = assemblies.Sum(static a => a.TotalTests);
int totalFailedTests = assemblies.Sum(static a => a.FailedTests);
int totalSkippedTests = assemblies.Sum(static a => a.SkippedTests);
int totalPassedTests = assemblies.Sum(static a => a.PassedTests);

// DESIGN: `allTestsWereSkipped` is intentionally treated as a failed run. Skipped tests don't count as
// "ran", so an all-skipped (or zero-test) run is reported in red as "Zero tests ran". This is the strict
Expand All @@ -60,17 +63,23 @@ private void AppendTestRunSummary(ITerminal terminal)
terminal.Append(' ');
terminal.Append(TestRunSummaryHelper.GetVerdictText(totalTests, totalFailedTests, totalSkippedTests, WasCancelled, _options.MinimumExpectedTests));

terminal.SetColor(TerminalColor.DarkGray);
terminal.Append(" - ");
terminal.ResetColor();
AppendAssemblyLinkTargetFrameworkAndArchitecture(terminal);
// For a single assembly (the in-process host) the verdict is followed by the assembly link, exactly as
// before. For multiple assemblies (the dotnet test orchestrator) the per-assembly identity is rendered in
// the progress area, so we keep the run-level verdict line link-free.
if (assemblies.Count == 1)
{
terminal.SetColor(TerminalColor.DarkGray);
terminal.Append(" - ");
terminal.ResetColor();
AppendAssemblyLinkTargetFrameworkAndArchitecture(terminal, assemblies[0]);
}

terminal.AppendLine();

int total = _testProgressState?.TotalTests ?? 0;
int failed = _testProgressState?.FailedTests ?? 0;
int passed = _testProgressState?.PassedTests ?? 0;
int skipped = _testProgressState?.SkippedTests ?? 0;
int total = totalTests;
int failed = totalFailedTests;
int passed = totalPassedTests;
int skipped = totalSkippedTests;
TimeSpan runDuration = _testExecutionStartTime != null && _testExecutionEndTime != null ? (_testExecutionEndTime - _testExecutionStartTime).Value : TimeSpan.Zero;

bool colorizeFailed = failed > 0;
Expand Down Expand Up @@ -126,14 +135,13 @@ private void AppendTestRunSummary(ITerminal terminal)
terminal.AppendLine();
}

internal void TestDiscovered(string displayName)
internal void TestDiscovered(string executionId, string displayName)
{
if (_testProgressState is null)
if (!_assemblies.TryGetValue(executionId, out TestProgressState? asm))
{
throw ApplicationStateGuard.Unreachable();
}

TestProgressState asm = _testProgressState;
asm.DiscoveredTests++;

if (_isDiscovery)
Expand All @@ -150,13 +158,13 @@ internal void TestDiscovered(string displayName)

public void AppendTestDiscoverySummary(ITerminal terminal)
{
TestProgressState? assembly = _testProgressState;
List<TestProgressState> assemblies = [.. _assemblies.Values.OrderBy(static a => a.Id)];
terminal.AppendLine();

int totalTests = assembly?.TotalTests ?? 0;
int totalTests = assemblies.Sum(static a => a.TotalTests);
bool runFailed = WasCancelled || totalTests < 1;

if (assembly is not null)
foreach (TestProgressState assembly in assemblies)
{
foreach (string displayName in assembly.DiscoveredTestDisplayNames)
{
Expand All @@ -170,10 +178,13 @@ public void AppendTestDiscoverySummary(ITerminal terminal)
terminal.SetColor(runFailed ? TerminalColor.DarkRed : TerminalColor.DarkGreen);
terminal.Append(string.Format(CultureInfo.CurrentCulture, TerminalResources.TestDiscoverySummarySingular, totalTests));

terminal.SetColor(TerminalColor.DarkGray);
terminal.Append(" - ");
terminal.ResetColor();
AppendAssemblyLinkTargetFrameworkAndArchitecture(terminal);
if (assemblies.Count == 1)
{
terminal.SetColor(TerminalColor.DarkGray);
terminal.Append(" - ");
terminal.ResetColor();
AppendAssemblyLinkTargetFrameworkAndArchitecture(terminal, assemblies[0]);
}

terminal.ResetColor();
terminal.AppendLine();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Microsoft.Testing.Platform.OutputDevice.Terminal;
internal sealed partial class TerminalTestReporter
{
internal void TestCompleted(
string executionId,
string testNodeUid,
string displayName,
TestOutcome outcome,
Expand All @@ -23,6 +24,7 @@ internal void TestCompleted(
{
FlatException[] flatExceptions = ExceptionFlattener.Flatten(errorMessage, exception);
TestCompleted(
executionId,
testNodeUid,
displayName,
outcome,
Expand All @@ -36,6 +38,7 @@ internal void TestCompleted(
}

private void TestCompleted(
string executionId,
string testNodeUid,
string displayName,
TestOutcome outcome,
Expand All @@ -47,13 +50,11 @@ private void TestCompleted(
string? standardOutput,
string? errorOutput)
{
if (_testProgressState is null)
if (!_assemblies.TryGetValue(executionId, out TestProgressState? asm))
{
throw ApplicationStateGuard.Unreachable();
}

TestProgressState asm = _testProgressState;

if (_options.ShowActiveTests)
{
asm.TestNodeResultsState?.RemoveRunningTestNode(testNodeUid);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,6 @@ internal event EventHandler OnProgressStopUpdate
remove => _terminalWithProgress.OnProgressStopUpdate -= value;
}

private readonly string _assembly;
private readonly string? _targetFramework;
private readonly string? _architecture;

/// <summary>
/// Returns whether external cancellation (e.g. Ctrl+C) has been requested. Injected as a delegate
/// rather than taking a concrete platform service so the reporter can be shared outside of
/// Microsoft.Testing.Platform (e.g. by the <c>dotnet test</c> multi-assembly orchestrator).
/// </summary>
private readonly Func<bool> _isCancellationRequested;

private readonly List<TestRunArtifact> _artifacts = [];
Expand All @@ -61,13 +52,21 @@ internal event EventHandler OnProgressStopUpdate

private readonly uint? _originalConsoleMode;

private TestProgressState? _testProgressState;
/// <summary>
/// Per-assembly run state, keyed by the caller-provided execution id. The in-process Microsoft.Testing.Platform
/// host registers a single assembly; the <c>dotnet test</c> orchestrator registers one entry per child test
/// assembly. Progress rendering already supports N workers (slots), so this only generalizes the bookkeeping.
/// </summary>
private readonly ConcurrentDictionary<string, TestProgressState> _assemblies = new();

private bool _isDiscovery;
private DateTimeOffset? _testExecutionStartTime;

private DateTimeOffset? _testExecutionEndTime;

/// <summary>Gets the total number of tests across all registered assemblies.</summary>
public int TotalTests => _assemblies.Values.Sum(static a => a.TotalTests);

private bool WasCancelled
{
get => field || _isCancellationRequested();
Expand All @@ -82,16 +81,10 @@ private bool WasCancelled
/// Initializes a new instance of the <see cref="TerminalTestReporter"/> class with custom terminal and manual refresh for testing.
/// </summary>
public TerminalTestReporter(
string assembly,
string? targetFramework,
string? architecture,
IConsole console,
Func<bool> isCancellationRequested,
TerminalTestReporterOptions options)
{
_assembly = assembly;
_targetFramework = targetFramework;
_architecture = architecture;
_isCancellationRequested = isCancellationRequested;
_options = options;

Expand Down Expand Up @@ -168,8 +161,8 @@ public void Dispose()
NativeMethods.RestoreConsoleMode(_originalConsoleMode);
}

public void ArtifactAdded(bool outOfProcess, string? testName, string path)
=> _artifacts.Add(new TestRunArtifact(outOfProcess, testName, path));
public void ArtifactAdded(bool outOfProcess, string? assembly, string? targetFramework, string? architecture, string? executionId, string? testName, string path)
=> _artifacts.Add(new TestRunArtifact(outOfProcess, assembly, targetFramework, architecture, executionId, testName, path));

/// <summary>
/// Let the user know that cancellation was triggered.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ public TestProgressState(long id, string assembly, string? targetFramework, stri
TargetFramework = targetFramework;
Architecture = architecture;
Stopwatch = stopwatch;
Assembly = assembly;
AssemblyName = Path.GetFileName(assembly);
IsDiscovery = isDiscovery;
}

/// <summary>Gets the assembly path or display name as provided by the caller (used for the summary link).</summary>
public string Assembly { get; }

public string AssemblyName { get; }

public string? TargetFramework { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ namespace Microsoft.Testing.Platform.OutputDevice.Terminal;
/// An artifact / attachment that was reported during run.
/// </summary>
[Embedded]
internal sealed record TestRunArtifact(bool OutOfProcess, string? TestName, string Path);
internal sealed record TestRunArtifact(bool OutOfProcess, string? Assembly, string? TargetFramework, string? Architecture, string? ExecutionId, string? TestName, string Path);
Loading
Loading