Skip to content

fix(cli): count a test's blocking rate in sessions, not retry attempts - #205

Merged
xping-admin merged 1 commit into
mainfrom
fix/181-blocking-rate-sessions
Sep 7, 2026
Merged

fix(cli): count a test's blocking rate in sessions, not retry attempts#205
xping-admin merged 1 commit into
mainfrom
fix/181-blocking-rate-sessions

Conversation

@xping-admin

Copy link
Copy Markdown
Collaborator

Closes #181.

The problem

TestIndex.BlockingRateOf divided blocking failures by failed executions. Both counts were per attempt, so a retry-masked session — one the test failed several times and then passed, leaving the session green — contributed several to the denominator and none to the numerator. A test that fails four attempts in one masked session and once in a red session scored 1/5 = 0.20 where the occasions say 1/2 = 0.50. The more a test retried, the less blocking it appeared. The term carries 0.20 of the impact weight and sits directly beneath RunFrequencyOf, which #176 moved to sessions for this exact reason, so the two disagreed about their unit inside one scorer. It was the last per-attempt counter left in the impact formula after #176 and #179.

Reviewing it turned up a second defect in the same expression, not named in the issue. The numerator asked _sessionsWithFinalFailures.Contains(...) — a session-wide set fed by SessionOutcomes.HasFinalFailure, true when any test ended the session red. A test whose every failure was masked by a retry therefore scored a blocking rate of 1.00 as long as some neighbour failed finally in the same session, which is the exact opposite of the "masked by a retry" separation the method's own remark claims to make.

What changed

Both counts are sessions now:

  • Denominator — the sessions the test failed in at all, counted by walking ExecutionsOf and advancing only when a failure comes from a session the last one did not. Executions arrive grouped by session, so this is the same adjacency check, and the same reasoning, as the sessionsRunIn increment in Build.
  • Numerator — the sessions the test itself ended red, read off its deciding attempt via RunsOf (added in fix(cli): count sessions, not executions, in every arm gate and evidence floor #179). Blocking means this test stopped the build, not that the build stopped.

A failed run implies a failed attempt in that session, so the numerator cannot outrun the denominator and the ratio needs no clamping. _sessionsWithFinalFailures and its only feeder, SessionOutcomes.HasFinalFailure, are gone; Tally keeps FinalOutcomes alive for SessionView. ARunAgreesWithSessionOutcomesWhenAttemptsArriveOutOfOrder now pins the same invariant against Tally.

Before/after over a real store

The issue asks for this on its own rather than riding along with an unrelated change, since impact scores and therefore finding order move.

The repo's ./.xping store (SampleApp.XUnit, 45 runs) is byte-identicaldiff <(jq -S '.findings' before.json) <(jq -S '.findings' after.json) is empty. That store records no retries at all (every execution is attemptNumber: 1), so nothing in it can distinguish the two counts. It confirms only that nothing else moved.

To exercise the fix end to end I built a second store from six runs of samples/SampleApp.MSTest, which carries FlakyTest_PassesOnRetry ([Retry(3)], fails attempt 1 and passes attempt 2 every time) alongside the always-failing and timing-out tests that end each session red:

rank before after
4 Flaky · FlakyTest_PassesOnRetry (high) Flaky · FlakyTest_RaceCondition (high)
5 RetryMasked · FlakyTest_PassesOnRetry (high) Flaky · FlakyTest_PassesOnRetry (high)
6 Flaky · FlakyTest_RaceCondition (high) RetryMasked · FlakyTest_PassesOnRetry (medium)

FlakyTest_PassesOnRetry never ends a session red — its failures are always masked — but the sessions are red because of other tests, so it scored a blocking rate of 1.00 and collected the full 0.20. It now scores 0.00, both of its findings drop 0.20 of impact, RetryMasked falls out of the high band, and FlakyTest_RaceCondition — which genuinely does end 2 of 6 sessions red, and whose blocking rate is 1.00 before and after — moves up past it. Ranks 1–3 are unchanged.

That store demonstrates the second defect. The retry-dilution direction, where a heavily retried test's impact goes up, has no fixture in samples/ (nothing there retries and then still fails), so it rests on the unit test below.

Acceptance

  • Both terms counted in sessions — BlockingRateCountsSessionsRatherThanAttempts reproduces the issue's own arithmetic: 0.20 before, 0.50 after.
  • A masked failure is not blocking, whoever else failed the session — AMaskedFailureIsNotBlockingEvenWhenAnotherTestFailedTheSession: 1.00 before, 0.00 after.
  • Both were confirmed to fail against the previous body, so neither passes vacuously. The remaining four (ATestThatEndsEverySessionRedBlocksEveryTimeItFails, ATimeoutCountsAsAFailureAndAsABlock, and the two zero cases) pass either way — they are guards, not pins.
  • Its own before/after over a store, above.

Verification

932 CLI tests pass; Xping.Sdk.sln builds clean with no warnings from the removed members.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TMQcZLBGFTd8wz3mcfn9vc

BlockingRateOf divided blocking failures by failed executions. Both
counts were per attempt, so a retry-masked session — one the test
failed several times and then passed — put several failures in the
denominator and none in the numerator. Four failed attempts in one
green session and one failure in a red one scored 0.20 where the
occasions say 0.50: the more a test retried, the less blocking it
looked, and it ranked below a test that fails once per build. The term
carries 0.20 of the impact weight and sits directly beneath
RunFrequencyOf, which #176 already moved to sessions, so the two
disagreed about their unit inside one scorer.

The numerator was also asking the wrong question. _sessionsWithFinalFailures
is session-wide — true when any test ended the session red — so a test
whose every failure was masked scored 1.00 as long as a neighbour failed
finally in the same session, the opposite of the separation the method's
own remark claims.

Both counts are now sessions: the sessions the test ended red, read off
its deciding attempt via RunsOf, over the sessions it failed in at all.
A failed run implies a failed attempt in that session, so the ratio
needs no clamping. _sessionsWithFinalFailures and its only feeder,
SessionOutcomes.HasFinalFailure, are removed.

Closes #181.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMQcZLBGFTd8wz3mcfn9vc
@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/Xping.Cli/Report/Indexes/SessionOutcomes.cs 100.00% <ø> (ø)
src/Xping.Cli/Report/Indexes/TestIndex.cs 95.32% <100.00%> (+0.88%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@xping-admin
xping-admin merged commit 82c5682 into main Sep 7, 2026
2 checks passed
@xping-admin
xping-admin deleted the fix/181-blocking-rate-sessions branch September 7, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(cli): BlockingRateOf is execution-denominated, so the more a test retries the less blocking it looks

1 participant