File: Commands\Test\MTP\Terminal\TestProgressState.cs
Web Access
Project: src\src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using TestNodeInfoEntry = (int Passed, int Skipped, int Failed, int LastAttemptNumber);

namespace Microsoft.DotNet.Cli.Commands.Test.Terminal;

internal sealed class TestProgressState(long id, string assembly, string? targetFramework, string? architecture, IStopwatch stopwatch, bool isDiscovery)
{
    private readonly Dictionary<string, TestNodeInfoEntry> _testUidToResults = new();

    // In most cases, retries don't happen. So we start with a capacity of 1.
    // Resizes will be rare and will be okay with such small sizes.
    private readonly List<string> _orderedInstanceIds = new(capacity: 1);

    public string Assembly { get; } = assembly;

    public string AssemblyName { get; } = Path.GetFileName(assembly)!;

    public string? TargetFramework { get; } = targetFramework;

    public string? Architecture { get; } = architecture;

    public IStopwatch Stopwatch { get; } = stopwatch;

    public int DiscoveredTests { get; private set; }

    public int FailedTests { get; private set; }

    public int PassedTests { get; private set; }

    public int SkippedTests { get; private set; }

    public int TotalTests => IsDiscovery ? DiscoveredTests : PassedTests + SkippedTests + FailedTests;

    public int RetriedFailedTests { get; private set; }

    public TestNodeResultsState? TestNodeResultsState { get; internal set; }

    public int SlotIndex { get; internal set; }

    public long Id { get; internal set; } = id;

    public long Version { get; internal set; }

    public List<(string? DisplayName, string? UID)> DiscoveredTestNames { get; internal set; } = [];

    public bool Success { get; internal set; }

    public bool IsDiscovery = isDiscovery;

    public int TryCount { get; private set; }

    private void ReportGenericTestResult(
        string testNodeUid,
        string instanceId,
        Func<TestNodeInfoEntry, TestNodeInfoEntry> incrementTestNodeInfoEntry,
        Action<TestProgressState> incrementCountAction)
    {
        var currentAttemptNumber = GetAttemptNumberFromInstanceId(instanceId);

        if (_testUidToResults.TryGetValue(testNodeUid, out var value))
        {
            // We received a result for this test node uid before.
            if (value.LastAttemptNumber == currentAttemptNumber)
            {
                // We are getting a test result for the same attempt.
                // This means that the test framework is reporting multiple results for the same test node uid.
                // We will just increment the count of the result.
                _testUidToResults[testNodeUid] = incrementTestNodeInfoEntry(value);
            }
            else if (currentAttemptNumber > value.LastAttemptNumber)
            {
                // This is a retry!
                // We are getting a test result for a different instance id.
                // This means that the test was retried.
                // We discard the results from the previous instance id
                RetriedFailedTests += value.Failed;
                PassedTests -= value.Passed;
                SkippedTests -= value.Skipped;
                FailedTests -= value.Failed;
                _testUidToResults[testNodeUid] = incrementTestNodeInfoEntry((Passed: 0, Skipped: 0, Failed: 0, LastAttemptNumber: currentAttemptNumber));
            }
            else
            {
                // This is an unexpected case where we received a result for an instance id that is older than the last one we saw.
                throw new UnreachableException($"Unexpected test result for attempt '{currentAttemptNumber}' while the last attempt is '{value.LastAttemptNumber}'");
            }
        }
        else
        {
            // This is the first time we see this test node.
            _testUidToResults.Add(testNodeUid, incrementTestNodeInfoEntry((Passed: 0, Skipped: 0, Failed: 0, LastAttemptNumber: currentAttemptNumber)));
        }

        incrementCountAction(this);
    }

    public void ReportPassingTest(string testNodeUid, string instanceId)
    {
        ReportGenericTestResult(testNodeUid, instanceId, static entry =>
        {
            entry.Passed++;
            return entry;
        }, static @this => @this.PassedTests++);
    }

    public void ReportSkippedTest(string testNodeUid, string instanceId)
    {
        ReportGenericTestResult(testNodeUid, instanceId, static entry =>
        {
            entry.Skipped++;
            return entry;
        }, static @this => @this.SkippedTests++);
    }

    public void ReportFailedTest(string testNodeUid, string instanceId)
    {
        ReportGenericTestResult(testNodeUid, instanceId, static entry =>
        {
            entry.Failed++;
            return entry;
        }, static @this => @this.FailedTests++);
    }

    public void DiscoverTest(string? displayName, string? uid)
    {
        DiscoveredTests++;
        DiscoveredTestNames.Add(new(displayName, uid));
    }

    internal void NotifyHandshake(string instanceId)
    {
        var index = _orderedInstanceIds.IndexOf(instanceId);
        if (index < 0)
        {
            // New instanceId for a retry. We add it to _orderedInstanceIds.
            _orderedInstanceIds.Add(instanceId);
            TryCount++;
        }
        else if (index != _orderedInstanceIds.Count - 1)
        {
            // This is an unexpected case where we received a handshake for an instance id that is not the last one we saw.
            // This means that the test framework is trying to report results for an instance id that is not the last one.
            throw new UnreachableException($"Unexpected handshake for instance id '{instanceId}' at index '{index}' while the last index is '{_orderedInstanceIds.Count - 1}'");
        }
    }

    private int GetAttemptNumberFromInstanceId(string instanceId)
    {
        var index = _orderedInstanceIds.IndexOf(instanceId);
        if (index < 0)
        {
            throw new UnreachableException($"The instanceId '{instanceId}' not found.");
        }

        // Attempt numbers are 1-based, so we add 1 to the index.
        return index + 1;
    }
}