File: AnalyzeCiFailureRenderingTests.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.Globalization;
using System.Text.Json.Nodes;
using AnalyzeCiFailure;
 
namespace Infrastructure.Tests;
 
/// <summary>Exercises production Markdown and metadata projections through shared scenario helpers.</summary>
public sealed partial class AnalyzeCiFailureWorkflowTests
{
    // The fixtures exercise the production functions, with file I/O kept here only
    // to preserve the representative inputs and independently asserted outputs.
    private static Task<CommandResult> RunFunctionAsync(Func<string> operation, int errorCode = 1)
    {
        try
        {
            return Task.FromResult(new CommandResult(ExitCode: 0, operation()));
        }
        catch (Exception ex) when (ex is ValidationException or System.Text.Json.JsonException or IOException or InvalidDataException)
        {
            return Task.FromResult(new CommandResult(errorCode, (errorCode == 2 ? "::warning::" : "::error::") + ex.Message + "\n"));
        }
    }
 
    private static async Task<CommandResult> RunFunctionAsync(Func<Task<string>> operation)
    {
        try
        {
            return new CommandResult(ExitCode: 0, await operation());
        }
        catch (Exception ex) when (ex is ValidationException or System.Text.Json.JsonException or IOException)
        {
            return new CommandResult(ExitCode: 1, "::error::" + ex.Message + "\n");
        }
    }
 
    private static int Int(IReadOnlyList<string> args, int index, int fallback)
        => args.Count > index ? int.Parse(args[index], CultureInfo.InvariantCulture) : fallback;
 
    private static long Long(IReadOnlyList<string> args, int index, long fallback)
        => args.Count > index ? long.Parse(args[index], CultureInfo.InvariantCulture) : fallback;
 
    private static Task<CommandResult> RunCommentRendererAsync(IReadOnlyList<string> args)
        => RunFunctionAsync(() => PrCommentRenderer.Render(
            CiFailureJson.ReadContract<AnalysisReport>(args[0])!,
            CiFailureJson.ReadContract<WorkflowJob[]>(args[1])!,
            args[2]));
 
    private static Task<CommandResult> RunIssueRendererAsync(IReadOnlyList<string> args)
        => RunFunctionAsync(
            () =>
        {
            static T? Optional<T>(string file) where T : class
                => File.Exists(file) ? CiFailureJson.ReadContract<T>(file) : null;
            var evidence = new PublicationEvidence(
                PullRequest: null,
                Optional<WorkflowRun>(args[2]),
                Optional<MergedPullRequest>(args[3]),
                Optional<CandidateHistoryStatus>(args[4])?.State ?? CandidateHistoryState.Unavailable,
                Candidates: null);
            var projection = CauseIssueRenderer.RenderCanonicalIssue(
                (JsonObject)CiFailureJson.Read(args[0])!,
                Optional<RunContext>(args[1]),
                evidence,
                args[5],
                args[6],
                args[7],
                args[8],
                args[9]);
            CiFailureJson.Write(args[11], new JsonObject { ["title"] = projection.Title, ["labels"] = projection.Labels });
            File.WriteAllText(args[10], projection.Body);
            if (!(CauseIssueRenderer.FitIssue(projection.Body!, []) is not null))
            {
                throw new ValidationException("Rendered cause issue exceeds the 65000-byte publication budget");
            }
            return "";
        },
            errorCode: 2);
 
    private static Task<CommandResult> RunExtractionAsync(IReadOnlyList<string> args)
        => RunFunctionAsync(() =>
        {
            TestResultArchiveExtractor.Extract(
                args[0],
                args[1],
                Int(args, index: 2, fallback: 10000),
                Long(args, index: 3, fallback: 1073741824),
                Long(args, index: 4, fallback: 104857600),
                args.Count > 5 && args[5].Length > 0 ? Long(args, index: 5, fallback: 0) : null,
                args.Count > 6 ? args[6] : "trx");
            return "";
        });
 
    private static Task<CommandResult> RunSelectionAsync(IReadOnlyList<string> args)
        => RunFunctionAsync(() => TestResultArtifacts.SerializeSelection(TestResultArtifacts.Select(
            TestResultArtifacts.ReadArtifacts((JsonArray)CiFailureJson.Read(args[0])!),
            ((JsonArray)CiFailureJson.Read(args[3])!).OfType<JsonObject>()
                .SelectMany(job => new AspireCiWorkflowConventions().DescribeTestArtifacts(job, Int(args, index: 7, fallback: 1)))
                .ToArray(),
            args[1],
            args[2],
            Int(args, index: 4, fallback: 20),
            Long(args, index: 5, fallback: 1073741824),
            Long(args, index: 6, fallback: 104857600))).ToJsonString() + "\n");
 
    private static Task<CommandResult> RunJsonRenderingAsync(IReadOnlyList<string> args)
        => RunFunctionAsync(() => "    " + DiagnosticTextSanitizer.SanitizeJson(
            CiFailureJson.Read(args[0]),
            Int(args, index: 1, fallback: 500),
            args.Count > 2 && args[2] == "multiline")!.ToJsonString() + "\n");
 
    private static Task<CommandResult> RunTextRenderingAsync(IReadOnlyList<string> args)
        => RunFunctionAsync(() => MarkdownFormatting.Indent(DiagnosticTextSanitizer.Sanitize(File.ReadAllText(args[0]), Int(args, index: 1, fallback: 65536), multiline: true)) + "\n");
 
    private static Task<CommandResult> RunTextSanitizingAsync(IReadOnlyList<string> args)
        => RunFunctionAsync(() =>
        {
            File.WriteAllText(args[1], DiagnosticTextSanitizer.Sanitize(File.ReadAllText(args[0]), Int(args, index: 2, fallback: 10485760), multiline: true) + "\n");
            return "";
        });
 
    private static Task<CommandResult> RunOccurrencesAsync(IReadOnlyList<string> args)
        => RunFunctionAsync(
            () =>
        {
            var body = CauseIssueRenderer.AppendOccurrence(File.ReadAllText(args[0]), args[1], Int(args, index: 2, fallback: 1));
            var fitted = CauseIssueRenderer.FitIssue(body, [], Int(args, index: 4, fallback: 65000));
            if (!(fitted is not null))
            {
                throw new ValidationException("occurrence section cannot fit within the publication budget");
            }
            File.WriteAllText(args[3], fitted);
            return "";
        },
            errorCode: 2);
 
    private static Task<CommandResult> RunMainMigrationAsync(IReadOnlyList<string> args)
        => RunFunctionAsync(
            () =>
        {
            var body = CauseIssueRenderer.MigrateMainBody(File.ReadAllText(args[0]), File.ReadAllText(args[1]));
            var fitted = CauseIssueRenderer.FitIssue(body, [], Int(args, index: 3, fallback: 65000));
            if (!(fitted is not null))
            {
                throw new ValidationException("migrated issue body exceeds the publication budget");
            }
            File.WriteAllText(args[2], fitted);
            return "";
        },
            errorCode: 2);
 
    private static Task<CommandResult> RunJobNamesAsync(IReadOnlyList<string> args)
        => RunFunctionAsync(() =>
        {
            var cause = CiFailureJson.Read(args[0])!;
            var jobs = CiFailureJson.ToContract<System.Collections.Immutable.ImmutableArray<WorkflowJob>>(CiFailureJson.Read(args[1]));
            var names = ((JsonArray)cause["job_ids"]!).Select(id =>
                jobs.First(job => CiFailureJson.AsNormalizedNumber(job.Id) == CiFailureJson.ReadNormalizedNumber(id)).Name!);
            return CauseIssueRenderer.JobNames(names, args[2]) + "\n";
        });
 
    private Task<CommandResult> RunIssueInventoryAsync(IReadOnlyList<string> args, IReadOnlyDictionary<string, string> environment)
        => RunFunctionAsync(async () =>
        {
            File.Delete(args[1]);
            File.Delete(args[2]);
            var (open, closed) = await new AnalysisPublicationQueries(new AnalysisGitHubClient(CreateGitHubClient(environment), CancellationToken.None)).GetCauseIssuesAsync(args[0]);
            File.WriteAllText(args[1], open.ToJsonString() + "\n");
            File.WriteAllText(args[2], closed.ToJsonString() + "\n");
            return "";
        });
 
    private Task<CommandResult> RunActionabilityAsync(IReadOnlyList<string> args, IReadOnlyDictionary<string, string> environment)
        => RunFunctionAsync(async () => (await new AnalysisPublicationQueries(new AnalysisGitHubClient(CreateGitHubClient(environment), CancellationToken.None)).IsPullRequestOpenAndUnlockedAsync(args[0], args[1]))
            ? "true\n" : "false\n");
 
    private Task<CommandResult> RunCommentLookupAsync(IReadOnlyList<string> args, IReadOnlyDictionary<string, string> environment)
        => RunFunctionAsync(async () => await new AnalysisPublicationQueries(new AnalysisGitHubClient(CreateGitHubClient(environment), CancellationToken.None)).FindAnalysisCommentIdAsync(args[0], args[1]) + "\n");
}