| File: parent\dotnet\Commands\Test\MTP\Terminal\SilenceDrivenHeartbeatRenderer.cs | Web Access |
| Project: dotnet-aot.csproj (dotnet-aot) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // Copyright (c) Microsoft Corporation. All rights reserved. // Adapted from https://github.com/microsoft/testfx/blob/f4d8d35bf45ce0ece75a66e1b197c96684767e48/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/SilenceDrivenHeartbeatRenderer.cs using System.Globalization; namespace Microsoft.DotNet.Cli.Commands.Test.Terminal; internal sealed class SilenceDrivenHeartbeatRenderer( TimeSpan silenceThreshold, TimeSpan slowTestThreshold, Func<IStopwatch> createStopwatch, Func<IEnumerable<TestProgressState>>? getTestRuns = null) : IProgressRenderer { private readonly Dictionary<long, SlowTestThresholdState> _slowTestThresholdStates = []; private StringBuilder? _slowTestDescriptionBuilder; private IStopwatch? _clock; private long _lastVisibleOutputTicks; private long _lastCompletionTicks = -1; public TimeSpan TickInterval => TimeSpan.FromSeconds(1); private long NowTicks => Volatile.Read(ref _clock)?.Elapsed.Ticks ?? 0; public void OnStart() { _slowTestThresholdStates.Clear(); IStopwatch clock = createStopwatch(); Volatile.Write(ref _clock, clock); _lastVisibleOutputTicks = clock.Elapsed.Ticks; Interlocked.Exchange(ref _lastCompletionTicks, -1); } public void OnTestCompleted() => Interlocked.Exchange(ref _lastCompletionTicks, NowTicks); public void OnWrite(ITerminal terminal, TestProgressState?[] progressItems, Action<ITerminal> write) { write(terminal); _lastVisibleOutputTicks = NowTicks; } public void OnTick(ITerminal terminal, TestProgressState?[] progressItems) { EmitSilenceHeartbeat(terminal, progressItems); EmitSlowTests(terminal, progressItems); } private void EmitSilenceHeartbeat(ITerminal terminal, TestProgressState?[] progressItems) { if (silenceThreshold <= TimeSpan.Zero) { return; } long now = NowTicks; if (now - _lastVisibleOutputTicks < silenceThreshold.Ticks) { return; } _lastVisibleOutputTicks = now; int completed = 0; int failed = 0; // Finished modules remain in the reporter after their worker slots have been reused. IEnumerable<TestProgressState?> testRuns = progressItems; if (getTestRuns is not null) { testRuns = getTestRuns(); } foreach (TestProgressState? item in testRuns) { if (item is not null) { completed += item.TotalTests; failed += item.FailedTests; } } int activeModules = 0; string? activeTest = null; foreach (TestProgressState? item in progressItems) { if (item is null) { continue; } activeModules++; if (activeTest is null) { TestDetailState? active = item.TestNodeResultsState?.GetRunningTasks(1).FirstOrDefault(); if (!string.IsNullOrWhiteSpace(active?.Text)) { activeTest = active.Text; } } } string line = string.Format(CultureInfo.CurrentCulture, CliCommandStrings.TerminalProgressHeartbeat, completed, failed, activeModules); long lastCompletion = Interlocked.Read(ref _lastCompletionTicks); line += lastCompletion < 0 ? CliCommandStrings.TerminalProgressHeartbeatNoCompletionSuffix : string.Format( CultureInfo.CurrentCulture, CliCommandStrings.TerminalProgressHeartbeatLastCompletionSuffix, HumanReadableDurationFormatter.Render(TimeSpan.FromTicks(Math.Max(0, now - lastCompletion)), wrapInParentheses: false)); if (activeTest is not null) { line += string.Format(CultureInfo.CurrentCulture, CliCommandStrings.TerminalProgressHeartbeatActiveSuffix, activeTest); } terminal.AppendLine(line); } private void EmitSlowTests(ITerminal terminal, TestProgressState?[] progressItems) { if (slowTestThreshold <= TimeSpan.Zero) { return; } foreach (TestProgressState? item in progressItems) { TestNodeResultsState? results = item?.TestNodeResultsState; if (item is null || results is null) { continue; } foreach (TestDetailState detail in results.GetRunningTasks(int.MaxValue)) { if (detail.Stopwatch is null) { continue; } TimeSpan elapsed = detail.Stopwatch.Elapsed; if (!_slowTestThresholdStates.TryGetValue(detail.Id, out SlowTestThresholdState? thresholdState)) { if (elapsed < slowTestThreshold) { continue; } thresholdState = new(slowTestThreshold); } if (!thresholdState.IsDue(elapsed)) { continue; } _slowTestThresholdStates[detail.Id] = thresholdState; string duration = HumanReadableDurationFormatter.Render(elapsed, wrapInParentheses: false); terminal.AppendLine(string.Format( CultureInfo.CurrentCulture, CliCommandStrings.TerminalProgressSlowTest, duration, BuildSlowTestDescription(item, detail))); _lastVisibleOutputTicks = NowTicks; } } } private string BuildSlowTestDescription(TestProgressState item, TestDetailState detail) { StringBuilder builder = _slowTestDescriptionBuilder ??= new StringBuilder(); builder.Clear(); builder.Append(detail.Text); builder.Append(" ("); builder.Append(item.AssemblyName); if (item.TargetFramework is not null) { builder.Append('|'); builder.Append(item.TargetFramework); } if (item.Architecture is not null) { builder.Append('|'); builder.Append(item.Architecture); } builder.Append(')'); return builder.ToString(); } }