File: AnalyzeCiFailureProgressTests.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.Text.Json.Nodes;
using System.Text.RegularExpressions;
using AnalyzeCiFailure;
using Aspire.TestUtilities;
using VerifyXunit;
using Xunit;
 
namespace Infrastructure.Tests;
 
/// <summary>Checks live progress visibility, safe diagnostics and separation of stdout path outputs.</summary>
public sealed partial class AnalyzeCiFailureWorkflowTests
{
    [Fact]
    public async Task ProgressIsReadableBeforeDiagnosticFinalizationAndDoesNotDumpPayloads()
    {
        using var invocation = new AnalysisInvocation();
        _invocationDirectories.Add(invocation.RootDirectory);
        var path = Path.Combine(invocation.DiagnosticsDirectory, "progress.log");
 
        Assert.True(File.Exists(path));
        invocation.Progress.Diagnostics.StartCurrentStage();
        invocation.Progress.Stage = "jobs";
        invocation.Progress.Diagnostics.Record("job-log", "partial", new { job_id = "77", exit_code = 23, payload = "private-payload" });
        invocation.Progress.RecordProblem(AnalysisProblemCategory.Fatal, "jobs", "token=private-value;\n::warning::injected");
        var text = await File.ReadAllTextAsync(path);
        text = Regex.Replace(text, @"(?m)^\[[^\]]+\] ", "[time] ");
        text = Regex.Replace(text, @"duration_ms=\d+", "duration_ms=<elapsed>");
 
        Assert.False(File.Exists(Path.Combine(invocation.DiagnosticsDirectory, "summary.json")));
        await Verifier.Verify(text, "log").UseDirectory("Snapshots");
    }
 
    [Fact]
    [RequiresTools(["bash", "git"])]
    public async Task CollectionStreamsProgressToStderrWithoutChangingStdoutPathProtocol()
    {
        var result = await RunCollectionAsync();
 
        Assert.Equal(0, result.ExitCode);
        Assert.Contains("[identity] started", _lastStandardError, StringComparison.Ordinal);
        Assert.Contains("[jobs] started", _lastStandardError, StringComparison.Ordinal);
        Assert.Collection(
            _lastStandardOutput.Split('\n', StringSplitOptions.RemoveEmptyEntries),
            line => Assert.StartsWith("workspace_dir=", line),
            line => Assert.StartsWith("evidence_dir=", line),
            line => Assert.StartsWith("replay_dir=", line),
            line => Assert.StartsWith("recovery_dir=", line),
            line => Assert.StartsWith("diagnostics_dir=", line));
        Assert.Equal(
            _lastStandardError,
            await File.ReadAllTextAsync(Path.Combine(Path.GetDirectoryName(DiagnosticSummary)!, "progress.log")));
    }
 
    [Fact]
    public async Task ProgressFileFailureIsReportedWithoutReplacingOriginalFailure()
    {
        using var invocation = new AnalysisInvocation();
        _invocationDirectories.Add(invocation.RootDirectory);
        var path = Path.Combine(invocation.DiagnosticsDirectory, "progress.log");
        Assert.True(File.Exists(path));
        File.Delete(path);
        Directory.CreateDirectory(path);
 
        invocation.Progress.Stage = "jobs";
        invocation.Capture.Failure = new ValidationException("original acquisition error");
        invocation.Progress.RecordProblem(AnalysisProblemCategory.Fatal, "jobs", invocation.Capture.Failure.Message);
        await invocation.WriteDiagnosticsAsync("failed");
 
        var summary = JsonNode.Parse(await File.ReadAllTextAsync(Path.Combine(invocation.DiagnosticsDirectory, "summary.json")))!;
        Assert.Equal("original acquisition error", summary["exception"]!["message"]!.GetValue<string>());
        Assert.Contains(
            summary["events"]!.AsArray(),
            item => item!["action"]!.GetValue<string>() == "progress-log" && item["result"]!.GetValue<string>() == "disabled");
    }
 
    [Fact]
    public async Task ProgressFileIsBoundedAndReportsTruncation()
    {
        using var invocation = new AnalysisInvocation();
        _invocationDirectories.Add(invocation.RootDirectory);
        var message = new string('x', count: 1000);
        for (var i = 0; i < 3 * AnalysisDiagnostics.MaximumManifestEntries; i++)
        {
            invocation.Progress.Diagnostics.WriteProgress(message);
        }
 
        await invocation.WriteDiagnosticsAsync("ready");
 
        var path = Path.Combine(invocation.DiagnosticsDirectory, "progress.log");
        Assert.True(new FileInfo(path).Length <= AnalysisDiagnostics.MaximumFileBytes);
        Assert.EndsWith(
            $"Progress log file reached its size limit; progress continues on stderr.{Environment.NewLine}",
            await File.ReadAllTextAsync(path));
        var summary = JsonNode.Parse(await File.ReadAllTextAsync(Path.Combine(invocation.DiagnosticsDirectory, "summary.json")))!;
        Assert.Contains(
            summary["events"]!.AsArray(),
            item => item!["action"]!.GetValue<string>() == "progress-log" && item["result"]!.GetValue<string>() == "truncated");
    }
 
    [Fact]
    [RequiresTools(["bash", "git"])]
    public async Task JobListingFailureNamesOperationAndDoesNotReachTestParsing()
    {
        var result = await RunCollectionAsync("job-list-api-failure");
 
        Assert.Equal(1, result.ExitCode);
        Assert.Contains("::error::Listing workflow jobs failed: GET /repos/microsoft/aspire/actions/runs/123/attempts/2/jobs: HTTP 502.", _lastStandardError, StringComparison.Ordinal);
        var summary = JsonNode.Parse(await File.ReadAllTextAsync(DiagnosticSummary))!;
        Assert.Equal(
            ["identity", "jobs"],
            summary["events"]!.AsArray()
                .Where(item => item!["action"]!.GetValue<string>() == "stage")
                .Select(item => item!["stage"]!.GetValue<string>()));
        Assert.Null(summary["outcome"]!["has_work"]);
        var text = await File.ReadAllTextAsync(Path.Combine(Path.GetDirectoryName(DiagnosticSummary)!, "progress.log"));
        Assert.Contains("[jobs] started: Listing workflow jobs", text, StringComparison.Ordinal);
        Assert.Contains("[jobs] failed: Listing workflow jobs", text, StringComparison.Ordinal);
    }
 
    [Fact]
    [RequiresTools(["bash", "git"])]
    public async Task MalformedTestResultsAreIdentifiedAsParsingProblems()
    {
        var result = await RunCollectionAsync("malformed-trx");
 
        Assert.Equal(0, result.ExitCode);
        Assert.Contains("[test-evidence] started: Collecting and parsing test results", _lastStandardError, StringComparison.Ordinal);
        Assert.Contains("stage=parsing: Cannot parse test result", _lastStandardError, StringComparison.Ordinal);
        var summary = JsonNode.Parse(await File.ReadAllTextAsync(DiagnosticSummary))!;
        Assert.Equal("degraded", summary["status"]!.GetValue<string>());
    }
}