File: PriorCauseCatalogue.cs
Web Access
Project: src\tools\AnalyzeCiFailure\AnalyzeCiFailure.csproj (AnalyzeCiFailure)
// 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 System.Text.RegularExpressions;
 
namespace AnalyzeCiFailure;
 
/// <summary>Builds a tolerant historical catalogue and current-evidence matches without I/O.</summary>
internal static partial class PriorCauseCatalogue
{
 
    internal static ImmutableArray<PriorCauseIndexEntry> BuildIndex(
        ImmutableDictionary<string, CollectedAttachment> attachments,
        ImmutableArray<CollectedTestFailure> tests)
    {
        var names = tests.Select(test => test.Name).ToHashSet(StringComparer.Ordinal);
        var diagnostics = tests.SelectMany(test => new[]
        {
            test.Diagnostics.Error, test.Diagnostics.StackTrace, test.Diagnostics.StandardOutput, test.Diagnostics.StandardError,
        }).Concat(attachments.Where(pair => pair.Key.StartsWith("diagnostics/job-", StringComparison.Ordinal) && pair.Key.EndsWith(".log", StringComparison.Ordinal))
            .Select(pair => pair.Value.Text)).ToArray();
        return attachments.Where(pair => pair.Key.StartsWith("history/causes/", StringComparison.Ordinal))
            .OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair =>
            {
                try
                {
                    var cause = CiFailureJson.ToContract<PriorCauseRecord>(System.Text.Json.Nodes.JsonNode.Parse(pair.Value.Text))
                        ?? throw new ValidationException("Prior cause is not an object");
                    if (cause.Id is not { } id || !CauseIdSlugRegex().IsMatch(id) ||
                        id + ".json" != Path.GetFileName(pair.Key))
                    {
                        throw new ValidationException("Prior cause ID does not match its filename");
                    }
                    cause.RequireReadableCollections();
                    var occurrences = cause.Occurrences ?? [];
                    var testNames = (cause.Tests ?? []).Concat(occurrences.SelectMany(occurrence => (occurrence!.Tests ?? []).Concat(occurrence.FailedTests ?? [])))
                        .Select(test => test!.Name).Append(cause.TestName);
                    return new PriorCauseIndexEntry
                    {
                        File = pair.Key,
                        Id = id,
                        Type = cause.Type,
                        Title = DiagnosticTextSanitizer.Sanitize(cause.Title ?? id, limit: 238),
                        TestName = cause.TestName is { } label ? DiagnosticTextSanitizer.Sanitize(label, limit: 500) : null,
                        IssueUrl = cause.IssueUrl is { } url ? DiagnosticTextSanitizer.Sanitize(url, limit: 500) : null,
                        ErrorPattern = DiagnosticTextSanitizer.Sanitize(cause.ErrorPattern ?? "", limit: 500, multiline: true),
                        OccurrenceCount = occurrences.Length,
                        LastSeen = occurrences.Select(occurrence => occurrence!.ObservedAt).Where(value => value is not null)
                            .Order(StringComparer.Ordinal).LastOrDefault() is { } last ? DiagnosticTextSanitizer.Sanitize(last, limit: 500) : null,
                        CurrentTestMatch = testNames.Any(name => name is not null && names.Contains(name)),
                        CurrentDiagnosticMatch = !string.IsNullOrEmpty(cause.ErrorPattern) && diagnostics.Any(value => value.Contains(cause.ErrorPattern, StringComparison.Ordinal)),
                    };
                }
                catch (Exception ex) when (ex is JsonException or ValidationException)
                {
                    return new PriorCauseIndexEntry
                    {
                        File = pair.Key,
                        ReadError = "Cannot summarize prior cause record: " + CiFailureJson.Display(DiagnosticTextSanitizer.Sanitize(ex.Message, limit: 500)),
                    };
                }
            }).ToImmutableArray();
    }
 
    [GeneratedRegex(@"^[a-z0-9]+(-[a-z0-9]+)*\z")]
    private static partial Regex CauseIdSlugRegex();
}