Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions src/Xping.Cli/Report/Indexes/SignatureIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ internal sealed record SignatureOccurrence(
/// <param name="Signature">The signature, readable components and all.</param>
/// <param name="Fingerprints">Distinct tests that failed with it, in ordinal order.</param>
/// <param name="Failures">Every failure carrying it, newest first.</param>
/// <param name="FailuresByFingerprint">
/// How many of those failures each test contributed. Counted here rather than by whoever needs the
/// number, so that the count of a member's failures and the decision that it is a member come from
/// one walk of one list and cannot disagree.
/// </param>
/// <param name="MaxTestsInOneSession">
/// The most distinct tests it hit within a single session — the measurement the shared-failure
/// threshold is applied to.
Expand All @@ -46,6 +51,7 @@ internal sealed record SignatureGroup(
FailureSignature Signature,
IReadOnlyList<string> Fingerprints,
IReadOnlyList<ExecutionRef> Failures,
IReadOnlyDictionary<string, int> FailuresByFingerprint,
int MaxTestsInOneSession,
int SessionCount,
int OldestSessionIndex,
Expand Down Expand Up @@ -254,11 +260,20 @@ private static Dictionary<string, SignatureGroup> BuildPerSignature(
.ThenBy(f => f.Execution.ExecutionId.ToString("N", CultureInfo.InvariantCulture),
StringComparer.Ordinal)];

var fingerprints = failures
.Select(f => f.Execution.Identity.TestFingerprint)
.Distinct(StringComparer.Ordinal)
.OrderBy(f => f, StringComparer.Ordinal)
.ToList();
// Membership and each member's share of the failures come out of the same walk. Counting
// them separately later would be quadratic in the size of the cluster, and a cluster is
// widest exactly when the finding matters most: one broken fixture takes the whole
// assembly with it, so the members and the failures grow together.
var failuresByFingerprint = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (ExecutionRef failure in failures)
{
string fingerprint = failure.Execution.Identity.TestFingerprint;
failuresByFingerprint[fingerprint] =
failuresByFingerprint.TryGetValue(fingerprint, out int seen) ? seen + 1 : 1;
}

List<string> fingerprints =
[.. failuresByFingerprint.Keys.OrderBy(f => f, StringComparer.Ordinal)];

// The shared-failure threshold is about one run: three tests failing the same way in the
// same session share something. Three tests failing the same way in three different
Expand All @@ -277,6 +292,7 @@ private static Dictionary<string, SignatureGroup> BuildPerSignature(
signatures[entry.Key],
fingerprints,
failures,
failuresByFingerprint,
maxTestsInOneSession,
sessionCount,
failures.Max(f => f.SessionIndex),
Expand Down
109 changes: 89 additions & 20 deletions src/Xping.Cli/Report/Providers/FailureModeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -447,9 +447,7 @@ private static FindingCandidate SharedFailure(AnalysisContext context, Signature

references.Add(reference);

int failures = cluster.Failures.Count(
f => string.Equals(
f.Execution.Identity.TestFingerprint, fingerprint, StringComparison.Ordinal));
cluster.FailuresByFingerprint.TryGetValue(fingerprint, out int failures);

members.Add(new ClusterMember(fingerprint, reference.FullyQualifiedName, failures));

Expand Down Expand Up @@ -985,36 +983,107 @@ private static Func<ExecutionRef, string> SignatureHashOf(AnalysisContext contex
private static List<ExecutionRef> Spread(
IReadOnlyList<ExecutionRef> failures, Func<ExecutionRef, string> key)
{
List<ExecutionRef> ordered = [.. failures
.OrderBy(f => f.SessionIndex)
.ThenBy(f => f.Execution.Retry?.AttemptNumber ?? 1)
.ThenBy(f => f.Execution.Identity.TestFingerprint, StringComparer.Ordinal)
.ThenBy(f => f.Execution.ExecutionId.ToString("N", CultureInfo.InvariantCulture),
StringComparer.Ordinal)];

var seen = new HashSet<string>(StringComparer.Ordinal);
List<ExecutionRef> chosen = [];
// Selected rather than sorted. A cluster's failure list is the largest collection the report
// handles — every test in a broken assembly, once per run — and ordering all of it to read
// three entries off the front was the expensive half of assembling that finding.
var earliestPerKey = new Dictionary<string, ExecutionRef>(StringComparer.Ordinal);

foreach (ExecutionRef reference in ordered)
foreach (ExecutionRef reference in failures)
{
if (chosen.Count == MaxExemplars)
break;

if (seen.Add(key(reference)))
chosen.Add(reference);
if (!earliestPerKey.TryGetValue(key(reference), out ExecutionRef? held) ||
ByExemplarOrder(reference, held) < 0)
{
earliestPerKey[key(reference)] = reference;
}
}

// The entries of the ordered list that introduce a new key are exactly the earliest failure
// under each key, so ordering those answers the same question the ordered walk did. No two
// of them can tie: a tie needs the same session, attempt, test and execution id, which is
// one execution and so one key.
List<ExecutionRef> candidates = [.. earliestPerKey.Values];
candidates.Sort(ByExemplarOrder);

List<ExecutionRef> chosen = [.. candidates.Take(MaxExemplars)];

// Then top up in the same order, so a test with one failure mode still gets three exemplars.
foreach (ExecutionRef reference in ordered)
if (chosen.Count < MaxExemplars)
TopUp(failures, chosen);
Comment thread
xping-admin marked this conversation as resolved.

return chosen;
}

/// <summary>
/// Fills the remaining exemplar slots from the earliest failures overall, repeating a key.
/// </summary>
/// <remarks>
/// Reached only when the failures span fewer keys than there are exemplars to fill, so fewer
/// than <see cref="MaxExemplars"/> entries are already spoken for. Holding twice that many
/// leaves the shortlist unable to run dry before <paramref name="chosen"/> is full.
/// </remarks>
private static void TopUp(IReadOnlyList<ExecutionRef> failures, List<ExecutionRef> chosen)
{
List<ExecutionRef> shortlist = new(MaxExemplars * 2);

foreach (ExecutionRef reference in failures)
Offer(shortlist, reference, MaxExemplars * 2);

foreach (ExecutionRef reference in shortlist)
{
if (chosen.Count == MaxExemplars)
break;

if (!chosen.Contains(reference))
chosen.Add(reference);
}
}

return chosen;
/// <summary>
/// Keeps a short list holding the <paramref name="capacity"/> earliest failures offered to it.
/// </summary>
private static void Offer(List<ExecutionRef> shortlist, ExecutionRef reference, int capacity)
{
if (shortlist.Count == capacity && ByExemplarOrder(reference, shortlist[^1]) >= 0)
return;

int at = shortlist.Count;
while (at > 0 && ByExemplarOrder(reference, shortlist[at - 1]) < 0)
at--;

shortlist.Insert(at, reference);

if (shortlist.Count > capacity)
shortlist.RemoveAt(shortlist.Count - 1);
}

/// <summary>
/// Orders failures the way exemplars are picked: newest run first, then earliest attempt.
/// </summary>
/// <remarks>
/// Total, so the same window always yields the same exemplars. Sessions are indexed newest
/// first, which is why ascending order here reads as most recent.
/// </remarks>
private static int ByExemplarOrder(ExecutionRef left, ExecutionRef right)
{
int bySession = left.SessionIndex.CompareTo(right.SessionIndex);
if (bySession != 0)
return bySession;

int byAttempt = (left.Execution.Retry?.AttemptNumber ?? 1)
.CompareTo(right.Execution.Retry?.AttemptNumber ?? 1);
if (byAttempt != 0)
return byAttempt;

int byTest = string.CompareOrdinal(
left.Execution.Identity.TestFingerprint, right.Execution.Identity.TestFingerprint);
if (byTest != 0)
return byTest;

// Ordinal over the hex form rather than Guid.CompareTo, which orders differently. Reached
// only by two attempts of one test in one run, so the formatting is off the hot path.
return string.CompareOrdinal(
left.Execution.ExecutionId.ToString("N", CultureInfo.InvariantCulture),
right.Execution.ExecutionId.ToString("N", CultureInfo.InvariantCulture));
}

private static FailureExemplar ToExemplar(AnalysisContext context, ExecutionRef reference)
Expand Down
Loading
Loading