Skip to main content

Flaky Test Diagnosis

Finds flaky tests, works out why they're flaky, and proposes fixes it has verified.

The problem

A flaky test passes on some runs and fails on others with the same code. The failure comes from hidden nondeterminism in the environment, so the code under test is usually fine.

CI retry plugins and pytest-rerunfailures tell you which test is flaky. This tool tells you why. Flakiness has a small number of identifiable causes, and each one can be isolated by changing exactly one environmental variable and watching whether the failure rate moves.

Quickstart

pip install -e .
whyflaky scan path/to/repo --verify

Sample diagnosis, from examples/flaky_suite:

● test_invoice.py::test_default_currency_is_usd
  kind: order-dependent (victim)
  in suite: failed 5/6 rounds, Wilson CI [44%, 97%]
  alone:    failed 0/24 (0%), CI [0%, 14%] -> stable_pass
  cause: test-order dependence (state pollution)
  polluter(s): test_billing.py::test_eur_invoice_formatting (confidence: high, 2 oracle queries, 3 trials)
    polluted state: os.environ['APP_CURRENCY']
    polluted state: app.state.CURRENCY
    test_billing.py::test_eur_invoice_formatting: os.environ['APP_CURRENCY']: None -> 'EUR'
    test_billing.py::test_eur_invoice_formatting: app.state.CURRENCY: 'USD' -> 'EUR'
  proposed fix (balanced tier): Autouse fixture in conftest.py saves/restores ...
  verification: VERIFIED (replay 0/9 failures, regression ok, semantic ok)

The report names the polluted state, so the diagnosis reads as "test_billing leaves app.state.CURRENCY set to 'EUR'". That is actionable in thirty seconds.

How it works

Debugging is run as a controlled experiment.

  1. Detection rounds. The suite runs several times in shuffled orders (iDFlakies-style) so order-dependent failures actually surface.
  2. Isolation baseline. Every suspect runs alone in a fresh process. Consistent passes mean a victim of test-order pollution, mixed results mean non-order-dependent flakiness (seed, time, thread), and consistent failures mean a brittle or broken test. Without this baseline, order dependence and ambient nondeterminism are confounded and every later conclusion is wrong.
  3. Bisection, on the order-dependent path. Probabilistic delta debugging finds the minimal polluting prefix. State-diff instrumentation then names what was polluted: module globals, os.environ, cwd, RNG state, and so on.
  4. Screening, on the non-order-dependent path. One dimension is perturbed at a time (PYTHONHASHSEED, RNG seed) while everything else is pinned. A two-proportion test decides whether the failure rate moved, with Benjamini-Hochberg FDR control across all screened hypotheses.
  5. Fix synthesis and verification. Patches are risk-tiered and emitted as .diff files for human review. Each one is verified three ways: statistical replay of the exact original failing condition, a full-suite regression check, and a semantic guard proving the patch didn't weaken any test (no assertions deleted, no skip or xfail added).

The algorithm: ddmin under a noisy oracle

Standard delta debugging assumes a deterministic oracle. A flaky-test oracle is probabilistic, so a prefix that genuinely triggers the bug can still pass on any given trial. The bisector handles this by making every oracle query a sequential probability ratio test with asymmetric error thresholds:

  • If a query wrongly reports that a subset does not trigger the failure, the real polluter gets pruned away and the rest of the bisection is wrong. Discarding a subset therefore requires strong evidence (β = 0.02, about six consecutive clean passes).
  • If a query wrongly reports that a subset does trigger it, the only cost is extra trials. Accepting is therefore cheap (α = 0.10, one observed failure is usually enough).

SPRT is used everywhere a fixed-N design would be wasteful or misleading: detection, bisection oracle queries, and fix replay. Failure rates are reported as Wilson score intervals, because 1/5 and 20/100 are both "20%" with very different confidence.

Cost control

  • Oracle queries run [subset..., victim], never the whole suite.
  • Oracle results are cached per ordered subset.
  • Fresh process per trial is the default. Reusing a process introduces the state leakage that is being measured.
  • --budget N caps total trials. On exhaustion it still reports the smallest confirmed polluting prefix.
  • Cost is reported honestly: trials run and wall-clock spent.

Benchmarks

Measured on 75 generated pytest suites (15 scenario types, 5 generation seeds) with known injected flakes: order-dependent polluters of module globals, environment variables, and cwd; hash-seed and unseeded-RNG flakes at designed failure rates of 20 to 60 percent; an always-failing control and a fully stable control. Ground truth is recorded at generation time, so every diagnosis is scored against what was actually injected.

metric result
Detection precision 100% (55 flakes reported, 0 false positives)
Detection recall 92% (55/60; all 5 misses are 20 to 40 percent flakes that never failed in 6 detection rounds)
Kind classification (OD / NOD / broken) 98% (59/60)
Cause classification 97% (58/60)
Polluter localization, rank-1 100% (35/35 order-dependent cases)
Proposed fixes passing 3-stage verification 86% (37/43)
Total cost 2825 trials, 386 s wall

Trial cost against a fixed-repetition baseline (50 reruns per query, pytest-flakefinder's default), same scenarios, same conclusions required:

suite SPRT trials fixed-50 trials saving
10-test OD scenario 36 282 7.8x
30-test OD scenario 37 to 42 332 8.0 to 9.0x
80-test OD scenario 39 432 11.1x
all 15 scenarios 527 2591 4.9x

The two arms reached identical conclusions on 14 of 15 scenarios. The exception is a 20-percent-rate flake that surfaced in one arm's detection rounds and not the other's; at that rate a 6-round scan has roughly a 74% chance of surfacing the flake at all, which is a detection-round limit, and --rounds raises it.

Hardware for wall-clock numbers: Apple M3 Pro, Python 3.13. Trial counts are hardware-independent. Reproduce with:

python -m benchmark.run --seed 1
python -m benchmark.run --baseline --fixed-n 50

CLI

whyflaky scan [path]
    --rounds N          detection rounds (default 6; round 0 = collection order)
    --budget N          max total trials, partial results on exhaustion (default 400)
    --screen-trials N   trials per perturbation condition (default 12)
    --fix               synthesize candidate patches (.diff files)
    --verify            verify patches (replay + regression + semantic), implies --fix
    --json PATH         machine-readable report (default <path>/.whyflaky/report.json)
    --fail-on-flake     exit 1 if flakes found (CI mode)

Run history is stored in SQLite, at .whyflaky/whyflaky.db.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

whyflaky-0.1.0.tar.gz (37.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

whyflaky-0.1.0-py3-none-any.whl (32.3 kB view details)

Uploaded Python 3

File details

Details for the file whyflaky-0.1.0.tar.gz.

File metadata

  • Download URL: whyflaky-0.1.0.tar.gz
  • Upload date:
  • Size: 37.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.5

File hashes

Hashes for whyflaky-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3d7e7e6e779e72a368c8caac57048ad57cba0f7e3469238ea848a655d643327e
MD5 9f417f41ee9362f25c4e47d3caf3e9c8
BLAKE2b-256 a2a630ddea91ba0036283dd6fbd1be6f32cf7aa824ae8edc39c317bfea8ab726

See more details on using hashes here.

File details

Details for the file whyflaky-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: whyflaky-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 32.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.5

File hashes

Hashes for whyflaky-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7bbf85e0e3e85f6d4e8419e7ef10273fe68afb7c3a06fa948c270cf6e38e79c0
MD5 511b1e86cfd5b6d3d4a21dc171357f3b
BLAKE2b-256 83040af66ef3f43c55932b61d413ef9f30655c671afb51e494154ec3f687ae68

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page