|
| 1 | +# flake-test-fail-autorevert |
| 2 | + |
| 3 | +Standalone CLI that reads pytorch-auto-revert's **already-published** decisions |
| 4 | +from ClickHouse and writes a CSV. It does not re-run any analysis: it only reads |
| 5 | +stored decisions. |
| 6 | + |
| 7 | +The CSV is **long**: one row per signal. A commit (landing on `main` within the |
| 8 | +requested range) contributes one row for each test signal that either: |
| 9 | + |
| 10 | +- triggered an autorevert on a **test** regression (`category = regression`), or |
| 11 | +- was flagged **flaky** by autorevert (`category = flaky`). |
| 12 | + |
| 13 | +Only test-level signals are reported (signal keys containing `::`); job-level |
| 14 | +signals are excluded. |
| 15 | + |
| 16 | +## Output columns |
| 17 | + |
| 18 | +- `commit_sha` — full 40-char sha |
| 19 | +- `commit_url` — `https://github.com/{repo}/commit/{commit_sha}` |
| 20 | +- `commit_time` — landing time on `main` (`YYYY-MM-DD HH:MM:SS`, UTC) |
| 21 | +- `category` — `regression` (this signal triggered an autorevert) or `flaky` |
| 22 | + (autorevert observed both a pass and a fail on this commit for this signal) |
| 23 | +- `workflow` — the CI workflow the signal belongs to (e.g. `trunk`, `pull`, |
| 24 | + `inductor`, `periodic`, `slow`), or `unknown`. See attribution below. |
| 25 | +- `signal_key` — the test signal key, `file.py::test_name` |
| 26 | +- `advisor_verdict` — for `regression` rows, the auto-revert advisor's verdict |
| 27 | + (`related`, `not_related`, `infra_issue`, `garbage`, `revert`, `unsure`) when |
| 28 | + one exists, else empty. Always empty for `flaky` rows. |
| 29 | +- `advisor_confidence` — for `regression` rows with a verdict, the advisor's |
| 30 | + confidence formatted to two decimals (e.g. `0.99`), else empty. Always empty |
| 31 | + for `flaky` rows. |
| 32 | +- `premerge_status` — for `regression` rows on the `trunk` or `pull` workflow, the |
| 33 | + pre-merge trunk-gate status of the test on the merged commit's validated head; |
| 34 | + empty for all other rows. One of: |
| 35 | + - `RUN_SUCCEEDED` — the test ran on the pre-merge head and passed. Only ever |
| 36 | + emitted from a POSITIVE success-row observation, never from an empty read. |
| 37 | + - `RUN_FAILED` — the test ran and at least one shard failed ("merged despite |
| 38 | + red"). Checked before success, so a mixed pass/fail set reports as failed. |
| 39 | + - `NOT_RUN:force_merge` — a REAL force merge (`skip_mandatory_checks` set on the |
| 40 | + merge, i.e. `-f`) that bypassed the gate AND the test did not run at all. A |
| 41 | + force merge that still ran the test reports the test's real verdict instead — |
| 42 | + force_merge never masks a real outcome. |
| 43 | + - `NOT_RUN:skipped` — the test ran but every run was skipped. |
| 44 | + - `NOT_RUN:td_deselected` — the test's file ran but the test was deselected |
| 45 | + (test dependency / target determination). |
| 46 | + - `NOT_RUN:not_in_matrix` — the test's file never ran on the head (job not in |
| 47 | + the matrix, or no gate jobs at all on a non-force merge). |
| 48 | + - `NOT_RUN:no_merge_record` — no `default.merges` row resolved a pre-merge head |
| 49 | + for this commit, so we cannot classify it. This is the honest label for a |
| 50 | + ghstack **non-tip** commit (only the stack's tip PR gets a merges row keyed by |
| 51 | + its squashed commit), a revert, a direct push, or data predating the merges |
| 52 | + table. It is NOT an inference of force merge. See the coverage note below. |
| 53 | + - `ERROR` — a query failed after retries, or the merge timestamp was missing. |
| 54 | + |
| 55 | +Rows are sorted by `(commit_time, category, workflow, signal_key)` ascending. |
| 56 | + |
| 57 | +The same `(commit, signal_key)` can appear as both a `regression` row and a |
| 58 | +`flaky` row: the two categories answer independent questions (did it trigger an |
| 59 | +autorevert vs. did autorevert observe both a pass and a fail on that commit). |
| 60 | + |
| 61 | +### How `workflow` is attributed |
| 62 | + |
| 63 | +- **Flaky rows**: the workflow is exact, taken directly from the autorevert |
| 64 | + state snapshot the flaky signal was read from. A single `(commit, signal_key)` |
| 65 | + legitimately observed flaky under two workflows produces two rows. |
| 66 | +- **Regression rows**: the reverted event stores the triggering workflows and |
| 67 | + the source signals as two independent arrays (a deduped set and an ordered |
| 68 | + list) that cannot be positionally zipped, so the workflow is resolved per |
| 69 | + signal with a fallback: the auto-revert advisor's workflow for that |
| 70 | + `(commit, signal_key)` if present, otherwise the revert event's sole workflow |
| 71 | + when it triggered on exactly one workflow, otherwise `unknown`. |
| 72 | + |
| 73 | +## Environment |
| 74 | + |
| 75 | +Reads the same connection variables as the `pytorch-auto-revert` lambda: |
| 76 | + |
| 77 | +- `CLICKHOUSE_HOST` — host or URL (`https://` prefix and `:8443` suffix are |
| 78 | + stripped automatically) |
| 79 | +- `CLICKHOUSE_PORT` — optional, defaults to `8443` |
| 80 | +- `CLICKHOUSE_USERNAME` |
| 81 | +- `CLICKHOUSE_PASSWORD` |
| 82 | + |
| 83 | +Set these in the environment or in a `.env` file in the tool dir (see |
| 84 | +`.env.example`), which is loaded on startup. |
| 85 | + |
| 86 | +## Run |
| 87 | + |
| 88 | +``` |
| 89 | +cd tools/flake-test-fail-autorevert |
| 90 | +uv run flake-test-fail-autorevert --start 2026-07-01 --end 2026-07-14 \ |
| 91 | + [--repo pytorch/pytorch] [--output out.csv] |
| 92 | +``` |
| 93 | + |
| 94 | +`uv` auto-creates and manages a local `.venv` and installs the dependencies |
| 95 | +declared in `pyproject.toml`, so there is no manual venv or `pip install` step. |
| 96 | + |
| 97 | +Run the tests with: |
| 98 | + |
| 99 | +``` |
| 100 | +uv run pytest |
| 101 | +``` |
| 102 | + |
| 103 | +### Alternative (pip) |
| 104 | + |
| 105 | +``` |
| 106 | +pip install -e . |
| 107 | +flake-test-fail-autorevert --start 2026-07-01 --end 2026-07-14 |
| 108 | +# or equivalently: |
| 109 | +python -m flake_test_fail_autorevert --start 2026-07-01 --end 2026-07-14 |
| 110 | +``` |
| 111 | + |
| 112 | +- `--start` / `--end` are dates (`YYYY-MM-DD`). The range is by commit landing |
| 113 | + time on `main`, and `--end` is **inclusive** (the whole end day is included): |
| 114 | + the effective window is `[start 00:00:00, (end + 1 day) 00:00:00)`. |
| 115 | +- `--repo` defaults to `pytorch/pytorch`. |
| 116 | +- `--output` defaults to |
| 117 | + `flake_test_fail_autorevert_<start>_<end>.csv`. The path and a one-line summary |
| 118 | + (`N rows across M commits: R regression, F flaky`) are printed on completion. |
| 119 | + |
| 120 | +## Notes on `premerge_status` coverage |
| 121 | + |
| 122 | +The pre-merge head is resolved from `default.merges`, which is keyed by the |
| 123 | +**merge command's** commit — for a ghstack stack that is only the **tip** PR's |
| 124 | +squashed commit. A ghstack **non-tip** commit lands its own squashed commit on |
| 125 | +`main` but has no `default.merges` row keyed by that commit, so its pre-merge head |
| 126 | +cannot be resolved and it is reported as `NOT_RUN:no_merge_record`. Autorevert |
| 127 | +frequently bisects a regression to a non-tip culprit, so this is a real coverage |
| 128 | +gap, not an edge case. There is currently no clean, reliable way to recover the |
| 129 | +non-tip pre-merge head from ClickHouse, so `no_merge_record` is the honest label |
| 130 | +rather than guessing. Reverts and direct pushes (no merges row) also land here. |
| 131 | + |
| 132 | +A real `-f` force merge, by contrast, DOES write a `default.merges` row (with |
| 133 | +`skip_mandatory_checks` set), so it resolves a head and its test status is queried |
| 134 | +normally; `NOT_RUN:force_merge` is reported only when the gate was bypassed AND the |
| 135 | +test genuinely did not run. |
| 136 | + |
| 137 | +## Notes on the flaky scan |
| 138 | + |
| 139 | +Flaky signals are read from the `misc.autorevert_state` JSON snapshots. Several |
| 140 | +independent autorevert configurations run concurrently, each publishing its own |
| 141 | +snapshot stream distinguished by its `workflows` set. The flaky query scans **all** |
| 142 | +autorevert state snapshots in the range (exhaustive — every snapshot, deduped in |
| 143 | +Python), so the `flaky` rows reflect every (workflow, commit, test) triple autorevert |
| 144 | +flagged flaky (both a passing and a failing run on that commit) while it was in the |
| 145 | +state window. Scanning every snapshot rather than only the day's latest is required |
| 146 | +because commits age out of autorevert's sliding state window mid-day, so a |
| 147 | +latest-only sample would miss flaky states that appeared earlier in the day. |
| 148 | + |
| 149 | +The query is run once per 6-hour chunk of the padded window (~4 chunks/day) and |
| 150 | +results are accumulated and deduped in Python. Cost note: each chunked query runs |
| 151 | +with capped parallelism and peaks at roughly 4 GiB server-side on the busiest |
| 152 | +observed days, taking a few seconds per chunk. Results are exhaustive (every |
| 153 | +snapshot in range, deduped), and very large ranges scale linearly in the number of |
| 154 | +chunks. |
| 155 | + |
| 156 | +DNS to the ClickHouse cloud host flaps intermittently and the shared cluster can |
| 157 | +return transient server errors (e.g. `MEMORY_LIMIT_EXCEEDED`), so each query is |
| 158 | +retried with exponential backoff on connection, name-resolution, and transient |
| 159 | +database errors. Genuine query bugs fail fast on the first attempt: the driver |
| 160 | +raises a bare `DatabaseError` for server errors with the ClickHouse numeric code in |
| 161 | +the message, so a deterministic code (syntax error, unknown table/column/function, |
| 162 | +type mismatch, bad arguments, access denied, etc.) is detected and not retried; |
| 163 | +unknown or transient codes default to being retried. |
0 commit comments