File: AnalyzeCiFailureVerdictMatrixTests.cs
Web Access
Project: src\tests\AnalyzeCiFailure.Tests\AnalyzeCiFailure.Tests.csproj (AnalyzeCiFailure.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using System.Text.Json;
using AnalyzeCiFailure;
using VerifyXunit;
using Xunit;
using static AnalyzeCiFailure.ValidationRules;
 
namespace Infrastructure.Tests;
 
/// <summary>Checks the verdict and publication policy across combinations of job and test classifications.</summary>
public sealed class AnalyzeCiFailureVerdictMatrixTests
{
    [Fact]
    public async Task CompleteRulesHaveOneAcceptedVerdictForEachSupportedDecisionShape()
    {
        var results = new SortedDictionary<string, string[]>(StringComparer.Ordinal);
        foreach (var scope in new[] { RunScope.Main, RunScope.PullRequest })
        {
            foreach (var first in Enum.GetValues<JobClassification>())
            {
                foreach (var second in Enum.GetValues<JobClassification>().Select(value => (JobClassification?)value).Prepend(element: null))
                {
                    foreach (var testClass in new[] { (TestClassification?)null, TestClassification.Flaky, TestClassification.CodeIssue })
                    {
                        foreach (var overlap in new[] { false, true })
                        {
                            var classifications = second is { } value ? new[] { first, value } : [first];
                            var jobs = classifications.Select((_, index) =>
                                new FailedJob(NormalizedJsonNumber.Parse((index + 1).ToString()), (index + 1).ToString(), $"Job {index + 1}")).ToImmutableArray();
                            var tests = testClass is { } classification
                                ? ImmutableArray.Create(new AnalysisTest
                                {
                                    Name = "Tests.Example",
                                    Job = "Job 1",
                                    JobId = JsonSerializer.SerializeToElement(value: 1),
                                    Classification = classification,
                                    Reason = "Decision",
                                })
                                : [];
                            var causes = classifications.Select((jobClass, index) => (jobClass, index))
                                .Where(item => item.jobClass != JobClassification.CodeIssue)
                                .Select(item => new FailureCause(
                                    $"cause-{item.index + 1}",
                                    $"cause-{item.index + 1}.json",
                                    item.jobClass switch
                                    {
                                        JobClassification.TransientInfrastructure => CauseType.InfrastructureFailure,
                                        JobClassification.FlakyTest => CauseType.FlakyTest,
                                        _ => CauseType.MainRepositoryBreakage,
                                    },
                                    [jobs[item.index].Id],
                                    item.jobClass == JobClassification.FlakyTest
                                        ? testClass == TestClassification.Flaky && item.index == 0
                                            ? [new TestObservation("Tests.Example", jobs[0].Id)] : []
                                        : default))
                                .ToList();
                            if (testClass == TestClassification.Flaky && first != JobClassification.FlakyTest)
                            {
                                causes.Add(new FailureCause(
                                    "test-cause",
                                    "test-cause.json",
                                    CauseType.FlakyTest,
                                    [jobs[0].Id],
                                    [new TestObservation("Tests.Example", jobs[0].Id)]));
                            }
                            if (overlap && causes.FirstOrDefault(cause => cause.Type == CauseType.FlakyTest) is { } shared)
                            {
                                causes.Add(shared with { Id = "overlapping-cause", FileName = "overlapping-cause.json" });
                            }
                            var accepted = new List<string>();
                            foreach (var verdict in Enum.GetValues<AnalysisVerdict>())
                            {
                                var report = new AnalysisReport
                                {
                                    Verdict = verdict,
                                    Jobs = classifications.Select((jobClass, index) => new AnalysisJob
                                    {
                                        Id = JsonSerializer.SerializeToElement(index + 1),
                                        Classification = jobClass,
                                        Reason = "Decision",
                                    }).ToImmutableArray(),
                                    Tests = tests,
                                    Causes = causes.Select(cause => cause.Id).ToImmutableArray(),
                                };
                                var run = new ValidatedRun(
                                    scope,
                                    "https://github.com/microsoft/aspire/actions/runs/123",
                                    scope == RunScope.Main ? "" : "42");
                                try
                                {
                                    AnalysisValidator.ValidateBundle(AnalyzeCiFailureFixtures.CreateEvidence(scope), "microsoft/aspire");
                                    RequirePermittedVerdict(report, run);
                                    RequireExactJobCoverage(report, jobs, run);
                                    RequireExactTestCoverage(report, tests.Select(test => new TestIdentity(test.Name!, test.Job!, test.NormalizedJobId)).ToImmutableArray(), jobs);
                                    foreach (var cause in causes)
                                    {
                                        RequireCauseFields(cause);
                                        ValidateCauseCompatibility(cause, report, jobs, run);
                                    }
                                    var immutableCauses = causes.ToImmutableArray();
                                    ValidateVerdict(report, immutableCauses, run);
                                    RequireCauseCoverage(report, immutableCauses);
                                    accepted.Add(CiFailureJson.EnumText(verdict));
                                }
                                catch (ValidationException)
                                {
                                    // An empty set records rejection by the complete production rule sequence.
                                }
                            }
                            Assert.True(accepted.Count <= 1, $"{scope}/{first}/{second}/{testClass}/{overlap}: {string.Join(", ", accepted)}");
                            results[$"{scope}/{first}/{second?.ToString() ?? "none"}/{testClass?.ToString() ?? "none"}/overlap={overlap}"] = accepted.ToArray();
                        }
                    }
                }
            }
        }
 
        Assert.Contains(results.Values, values => values.SequenceEqual(["mixed"]));
        Assert.Contains(results.Values, values => values.SequenceEqual(["transient-infra"]));
        Assert.Contains(results.Values, values => values.SequenceEqual(["flaky-test"]));
        Assert.Contains(results.Values, values => values.SequenceEqual(["code-issue"]));
        Assert.Contains(results.Values, values => values.SequenceEqual(["main-repository-breakage"]));
        await Verifier.Verify(JsonSerializer.Serialize(results, new JsonSerializerOptions { WriteIndented = true }), "json")
            .UseDirectory("Snapshots");
    }
}