flakebox 0.4.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flakebox-0.4.4/LICENSE +21 -0
- flakebox-0.4.4/MANIFEST.in +1 -0
- flakebox-0.4.4/PKG-INFO +365 -0
- flakebox-0.4.4/README.md +348 -0
- flakebox-0.4.4/action.yml +180 -0
- flakebox-0.4.4/flakebox/__init__.py +0 -0
- flakebox-0.4.4/flakebox/__main__.py +4 -0
- flakebox-0.4.4/flakebox/cli.py +475 -0
- flakebox-0.4.4/flakebox/detect.py +175 -0
- flakebox-0.4.4/flakebox/junit.py +139 -0
- flakebox-0.4.4/flakebox/quarantine.py +194 -0
- flakebox-0.4.4/flakebox/store.py +363 -0
- flakebox-0.4.4/flakebox.egg-info/PKG-INFO +365 -0
- flakebox-0.4.4/flakebox.egg-info/SOURCES.txt +20 -0
- flakebox-0.4.4/flakebox.egg-info/dependency_links.txt +1 -0
- flakebox-0.4.4/flakebox.egg-info/entry_points.txt +2 -0
- flakebox-0.4.4/flakebox.egg-info/top_level.txt +1 -0
- flakebox-0.4.4/pyproject.toml +27 -0
- flakebox-0.4.4/setup.cfg +4 -0
- flakebox-0.4.4/tests/test_flakebox.py +4114 -0
- flakebox-0.4.4/tests/test_harden_streams.py +40 -0
- flakebox-0.4.4/tests/test_windows_pipe.py +93 -0
flakebox-0.4.4/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Calas Automations Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include action.yml
|
flakebox-0.4.4/PKG-INFO
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flakebox
|
|
3
|
+
Version: 0.4.4
|
|
4
|
+
Summary: Self-hosted flaky-test triage for small teams.
|
|
5
|
+
Author: Calas Automations Inc.
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/calasdan-sketch/flakebox
|
|
8
|
+
Project-URL: Issues, https://github.com/calasdan-sketch/flakebox/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Topic :: Software Development :: Testing
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# Flakebox (`flakebox`)
|
|
19
|
+
|
|
20
|
+
Self-hosted flaky-test triage for small teams. Flaky tests eat 10–25% of
|
|
21
|
+
CI compute and erode trust in every red build. Trunk and Datadog will sell
|
|
22
|
+
you the fix per-seat; flakebox is the free, self-hosted alternative:
|
|
23
|
+
ingest JUnit XML, detect flakiness statistically, quarantine the flakes,
|
|
24
|
+
and get told when they're stable enough to come back. SQLite, stdlib-only,
|
|
25
|
+
no SaaS.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
Zero dependencies — Python 3.10+ stdlib only.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install git+https://github.com/calasdan-sketch/flakebox
|
|
33
|
+
flakebox --version
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# in CI, after your test step (pytest --junitxml=reports/junit.xml):
|
|
40
|
+
flakebox record --junit reports/junit.xml --commit="$GITHUB_SHA"
|
|
41
|
+
|
|
42
|
+
# what is flaky?
|
|
43
|
+
flakebox report
|
|
44
|
+
flakebox report --format json
|
|
45
|
+
|
|
46
|
+
# quarantine the flaky set for pytest:
|
|
47
|
+
flakebox quarantine --format pytest -o .
|
|
48
|
+
python -m pytest -p flakebox_quarantine # flakes are skipped, build stays green
|
|
49
|
+
|
|
50
|
+
# ...or keep running the flakes without letting them fail the build, so
|
|
51
|
+
# they keep recording results and can earn their way back out:
|
|
52
|
+
flakebox quarantine --format pytest --mode xfail -o .
|
|
53
|
+
|
|
54
|
+
# the full quarantine set as shell-quoted pytest --deselect args (best
|
|
55
|
+
# effort, see below; eval parses the quoting) or as JSON for other runners
|
|
56
|
+
eval "pytest $(flakebox quarantine --format deselect)"
|
|
57
|
+
flakebox quarantine --format json
|
|
58
|
+
|
|
59
|
+
# when a quarantined test stabilizes, the report tells you:
|
|
60
|
+
flakebox unquarantine --test "pkg.Class::test_x"
|
|
61
|
+
flakebox history --test "pkg.Class::test_x" # per-test run history, same
|
|
62
|
+
# thresholds/status words as report
|
|
63
|
+
|
|
64
|
+
# carry the history around (CI artifact -> laptop, job -> aggregator):
|
|
65
|
+
flakebox export --out flakebox-export.json
|
|
66
|
+
flakebox import flakebox-export.json # merges; re-import adds nothing
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`--db PATH` picks the database and goes before **or** after the
|
|
70
|
+
subcommand: `flakebox --db ci.db report` and `flakebox report --db ci.db`
|
|
71
|
+
are the same command. Default `~/.flakebox/history.db`
|
|
72
|
+
(`%USERPROFILE%\.flakebox\history.db` on Windows). A `--db` that is not a
|
|
73
|
+
SQLite database is a clean `error: cannot use database ...`, exit 2.
|
|
74
|
+
So are the other arguments flakebox cannot use: each ends in an `error:`
|
|
75
|
+
line and exit 2, never a traceback. That covers an `-o` that is a file or
|
|
76
|
+
cannot be created, an integer option that reaches the database
|
|
77
|
+
(`--retire-after`, `history --limit`) outside the 64-bit range SQLite
|
|
78
|
+
stores (-9223372036854775808 to 9223372036854775807), and a `--commit`,
|
|
79
|
+
`--branch` or `--test` that is not valid UTF-8 text (a stray byte in the
|
|
80
|
+
command line).
|
|
81
|
+
|
|
82
|
+
`flakebox record` hashes the report: recording the same file for the
|
|
83
|
+
same commit twice says `already recorded as run N` and adds nothing
|
|
84
|
+
(`--force` records it anyway), so a retried CI step cannot double-count.
|
|
85
|
+
Nested `<testsuite>` elements (surefire aggregates) are read, `time="0,5"`
|
|
86
|
+
is 500 ms, and a `time` that is not a finite, non-negative number
|
|
87
|
+
(`fast`, `inf`, `nan`, `-5`, `1e300`) is recorded as 0 with a warning
|
|
88
|
+
instead of rejecting the report. A report that declares a `DOCTYPE` or an
|
|
89
|
+
`ENTITY` is refused (exit 2) as a possible XML bomb, before anything is
|
|
90
|
+
expanded; text that merely mentions `<!DOCTYPE html>`, such as captured
|
|
91
|
+
output in a CDATA section, is fine.
|
|
92
|
+
|
|
93
|
+
A report with no test cases at all (`<testsuite tests="0"/>`, or a file
|
|
94
|
+
written before the tests ran) is not recorded. It is nearly always a test
|
|
95
|
+
step that broke, and a run without tests makes every test look absent:
|
|
96
|
+
three of them in a row retire the whole suite, and `report` says "No flaky
|
|
97
|
+
tests detected". `record` prints a warning on stderr and exits 0, so a
|
|
98
|
+
build that really ran no tests (docs only) is not failed. `--allow-empty`
|
|
99
|
+
records the report anyway; it then counts like any other run. Empty runs
|
|
100
|
+
that an earlier flakebox recorded stay in the history (there is no
|
|
101
|
+
command to delete a run) and stop mattering once `--retire-after` (3) real
|
|
102
|
+
runs have followed, or at once with `--retire-after 0`.
|
|
103
|
+
|
|
104
|
+
`flakebox quarantine` is idempotent and safe to run on every build: it
|
|
105
|
+
adds the current flaky set (never re-stamping tests already in
|
|
106
|
+
quarantine) and always emits the **whole** quarantine set, not just the
|
|
107
|
+
newly added tests. The one-line summary (`quarantined 1 new test(s); 3
|
|
108
|
+
in quarantine.`) goes to stderr for `--format deselect`, so stdout is
|
|
109
|
+
only ever the `--deselect` arguments.
|
|
110
|
+
|
|
111
|
+
Run the plugin with `python -m pytest -p flakebox_quarantine`, not as the
|
|
112
|
+
bare `pytest` command: `-p` imports `flakebox_quarantine` at start-up, and
|
|
113
|
+
only `python -m pytest` puts the current directory (where `-o .` wrote the
|
|
114
|
+
plugin) on `sys.path`. The bare command fails with an `ImportError` (`No
|
|
115
|
+
module named 'flakebox_quarantine'`). If `-o` wrote the plugin to another
|
|
116
|
+
directory, put that directory on `PYTHONPATH` first
|
|
117
|
+
(`PYTHONPATH=ci python -m pytest -p flakebox_quarantine`; `set
|
|
118
|
+
PYTHONPATH=ci` in `cmd.exe`, `$env:PYTHONPATH = "ci"` in PowerShell); with
|
|
119
|
+
it set, the bare `pytest` command works too.
|
|
120
|
+
|
|
121
|
+
`--format deselect` is **best effort**: JUnit ids are not pytest node
|
|
122
|
+
ids, so flakebox maps the common pytest shapes
|
|
123
|
+
(`pkg.module.Class::test_x` → `pkg/module.py::Class::test_x`,
|
|
124
|
+
`pkg.module::test_x` → `pkg/module.py::test_x`) and pytest ignores a
|
|
125
|
+
`--deselect` that matches nothing. A lowercase class name, a
|
|
126
|
+
`python_files` pattern other than `*.py`, or a non-pytest producer breaks
|
|
127
|
+
the guess, and pytest's `--deselect` is a *prefix* match (`--deselect
|
|
128
|
+
t.py::test_x` also drops `test_xy` and `test_x[1]`); the generated plugin
|
|
129
|
+
(`--format pytest`) matches whole names and path components instead (see
|
|
130
|
+
below) and is the reliable path. The line quotes each argument for
|
|
131
|
+
the shell (POSIX quoting; `cmd.exe` quoting on Windows), so the shell
|
|
132
|
+
has to parse it once more: `eval "pytest $(flakebox quarantine --format
|
|
133
|
+
deselect)"` hands pytest every id as one argument, even
|
|
134
|
+
`test_param[a b]` or one containing `$(...)`. A bare `pytest
|
|
135
|
+
$(flakebox quarantine --format deselect)` does not: command
|
|
136
|
+
substitution splits the output at whitespace and never removes the
|
|
137
|
+
quotes in it, so an id with a space or a shell character arrives in
|
|
138
|
+
pieces with the quotes still attached (ids without either are fine).
|
|
139
|
+
PowerShell has no `$(...)` word splitting, so `Invoke-Expression "pytest
|
|
140
|
+
$(flakebox quarantine --format deselect)"` is the counterpart of `eval`,
|
|
141
|
+
but it is **untested**: the Windows line is quoted for `cmd.exe`, which
|
|
142
|
+
leaves `;`, `&`, `,`, `$`, backticks and parentheses bare, and PowerShell
|
|
143
|
+
may parse those itself (an id that contains one may be split, or even
|
|
144
|
+
run). If an id can contain one, use the plugin.
|
|
145
|
+
|
|
146
|
+
On Windows the `--format deselect` line is written with a plain LF ending,
|
|
147
|
+
not CR LF: text-mode output would end the last id with a carriage return,
|
|
148
|
+
which Git-Bash's `$(...)` keeps (the id then matches nothing). That is
|
|
149
|
+
simulated in the tests with a text stream that writes CR LF; it has not
|
|
150
|
+
been run on Windows.
|
|
151
|
+
|
|
152
|
+
The plugin matches a test to a quarantined id by name and by whole path
|
|
153
|
+
components, in either spelling (`tests/test_mod.py::TestA::test_x` and
|
|
154
|
+
`tests.test_mod.TestA::test_x` are the same test): the names, with any
|
|
155
|
+
`[parameters]`, must be equal, and so must the module path and class
|
|
156
|
+
names, except that one side may lack leading directories, because CI and
|
|
157
|
+
your machine can have different rootdirs. So `test_mod.py::test_x` is the
|
|
158
|
+
quarantined `tests.test_mod::test_x`, but `pkg_a/test_util.py::test_x` is
|
|
159
|
+
never `pkg_b/test_util.py::test_x`. When the *recorded* id is the shorter
|
|
160
|
+
one, it cannot say which directory it came from and every test that ends
|
|
161
|
+
the same way matches: record and test from the same rootdir. Parametrised
|
|
162
|
+
ids are matched whole: quarantining `test_version[1.5]` skips exactly that
|
|
163
|
+
case, never `test_unrelated[0.5]` or `test_version[2.0]`.
|
|
164
|
+
|
|
165
|
+
Example report:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
## Flaky (pass AND fail — quarantine candidates)
|
|
169
|
+
| test | flake rate | fails/passes | runs |
|
|
170
|
+
|---|---|---|---|
|
|
171
|
+
| `tests.test_a.TestA::test_flaky` | 50% | 3/3 | 6 |
|
|
172
|
+
|
|
173
|
+
## Consistently failing (broken, not flaky — fix, don't quarantine)
|
|
174
|
+
- `tests.test_b::test_broken` (6 failures, 0 passes)
|
|
175
|
+
|
|
176
|
+
## Stable — ready to un-quarantine
|
|
177
|
+
- `tests.test_c::test_old_flake` (12 consecutive passes)
|
|
178
|
+
|
|
179
|
+
## Retired (absent from the last 3 runs — deleted or renamed, not counted)
|
|
180
|
+
- `tests.test_d::test_removed` (was failing; last seen run 41, commit 9f3c2a1)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## How detection works
|
|
184
|
+
|
|
185
|
+
No ML, fully explainable. Only *executed* runs count — a skipped run is
|
|
186
|
+
not evidence either way, so it never pushes a test over `--min-runs` and
|
|
187
|
+
never breaks a pass streak. "Newest" always means the newest recorded
|
|
188
|
+
time of a run (its row id only breaks a tie), so merging an older export
|
|
189
|
+
into a DB never makes old runs look recent.
|
|
190
|
+
|
|
191
|
+
- **flaky**: at least one pass *and* at least one failure (`failed` or
|
|
192
|
+
`error`) across ≥5 executed runs (`--min-runs`), and not currently on
|
|
193
|
+
a pass streak of ≥10 (`--stable-runs`).
|
|
194
|
+
- **recovered**: would be flaky, but the newest ≥10 executed runs all
|
|
195
|
+
passed — it has recovered and is left alone, so quarantining it,
|
|
196
|
+
releasing it and re-quarantining it from the same stale failures can't
|
|
197
|
+
loop. `report` leaves it out of the flaky table and `flakebox history`
|
|
198
|
+
says `recovered (18 consecutive passes)`: one `classify()` decides the
|
|
199
|
+
word for every command, and `history` takes the same `--min-runs`,
|
|
200
|
+
`--stable-runs` and `--retire-after` flags as `report`.
|
|
201
|
+
- **failing** (the "Consistently failing" section): ≥5 executed runs and
|
|
202
|
+
never a pass. Broken, not flaky — different problem, don't quarantine
|
|
203
|
+
it. `flakebox history` labels it `failing` and shows no flake rate.
|
|
204
|
+
- **stable**: a quarantined test whose newest ≥10 executed runs all
|
|
205
|
+
passed — the report tells you to un-quarantine it.
|
|
206
|
+
- **retired**: a test with history that appears in none of the newest 3
|
|
207
|
+
recorded runs (`--retire-after`, `0` disables) — deleted or renamed.
|
|
208
|
+
It is listed on its own with where it was last seen and counted
|
|
209
|
+
nowhere else, so a test you removed does not stay "consistently
|
|
210
|
+
failing" for the rest of its 50-run window. One or two partial runs
|
|
211
|
+
(someone recorded a single file) do not retire anything. A retired
|
|
212
|
+
test still in quarantine is flagged with the `unquarantine` command
|
|
213
|
+
to run.
|
|
214
|
+
- Everything else (always passing, or too few executed runs) is not
|
|
215
|
+
reported.
|
|
216
|
+
|
|
217
|
+
The flake rate is failures ÷ executed runs; the `runs` column is
|
|
218
|
+
executed runs. History lives in SQLite (`--db` to override).
|
|
219
|
+
|
|
220
|
+
### Quarantine modes
|
|
221
|
+
|
|
222
|
+
The generated pytest plugin reads `flakebox_quarantine.json` (the sorted
|
|
223
|
+
ids, the mode and a count; no timestamp, so regenerating an unchanged set
|
|
224
|
+
rewrites identical files and `git` shows no diff):
|
|
225
|
+
|
|
226
|
+
- `--mode skip` (default): quarantined tests are skipped. They record no
|
|
227
|
+
result, so they cannot become *stable* on their own — run them
|
|
228
|
+
somewhere non-blocking (a separate job) and `flakebox record` that, or
|
|
229
|
+
use xfail mode.
|
|
230
|
+
- `--mode xfail`: quarantined tests still run as non-strict `xfail`,
|
|
231
|
+
so they can never fail the build, but their real pass/fail lands in
|
|
232
|
+
the JUnit report (`flakebox record` maps the plugin's xfail back to a
|
|
233
|
+
failure) and the report can tell you when they have stabilised.
|
|
234
|
+
|
|
235
|
+
### Export / import
|
|
236
|
+
|
|
237
|
+
`flakebox export --out FILE` writes the whole DB (runs with results, and
|
|
238
|
+
the quarantine set) as one JSON document. `flakebox import FILE...`
|
|
239
|
+
merges exports into a DB: a run already present (same commit and same
|
|
240
|
+
report hash) is skipped, so importing the same file twice, or into the
|
|
241
|
+
DB it came from, adds nothing; quarantine entries merge as a set. Runs
|
|
242
|
+
recorded without a report hash (flakebox < 0.3, or through the Python
|
|
243
|
+
API) carry a content **fingerprint** (`legacy:` + a digest of commit,
|
|
244
|
+
timestamp and results) instead, stamped when the DB is opened, so they
|
|
245
|
+
are recognised too. Imported runs keep their original timestamp, and
|
|
246
|
+
the newest-runs windows (the pass streak, `--retire-after`, `history`)
|
|
247
|
+
follow timestamps, not the order rows were added; a run exported
|
|
248
|
+
without a timestamp is imported as the oldest. A malformed export is
|
|
249
|
+
refused as a whole before anything is written: every field is checked
|
|
250
|
+
first (types, finite numbers that fit a 64-bit integer, text UTF-8 can
|
|
251
|
+
encode, `"flakebox_export": 1` exactly) and the error names the first bad
|
|
252
|
+
one (`error: cannot import ...: runs[0] must be an object`, exit 2).
|
|
253
|
+
A path that cannot be read (a directory, no permission) and JSON nested
|
|
254
|
+
too deeply to parse are the same one-line error and exit 2, and so is an
|
|
255
|
+
`export --out` that cannot be written.
|
|
256
|
+
This is how CI's history reaches a laptop, and how per-job DBs of a
|
|
257
|
+
matrix build become one (see the Action).
|
|
258
|
+
|
|
259
|
+
## GitHub Action
|
|
260
|
+
|
|
261
|
+
`action.yml` is a composite action: restores the DB from cache, records
|
|
262
|
+
the JUnit report, posts the flakebox report to the job summary, and —
|
|
263
|
+
optionally — quarantines the flaky set and exports the history as
|
|
264
|
+
artifact-ready files.
|
|
265
|
+
|
|
266
|
+
```yaml
|
|
267
|
+
- uses: calasdan-sketch/flakebox@main
|
|
268
|
+
with:
|
|
269
|
+
junit: reports/junit.xml
|
|
270
|
+
min-runs: "5" # -> flakebox report --min-runs 5
|
|
271
|
+
fail-on-flaky: "true"
|
|
272
|
+
quarantine-output: flakebox/quarantine.json
|
|
273
|
+
export-output: flakebox/history.json
|
|
274
|
+
- uses: actions/upload-artifact@v4
|
|
275
|
+
with:
|
|
276
|
+
name: flakebox
|
|
277
|
+
path: flakebox/
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`@main` is this repository's default branch. A `@v1` reference works only once a
|
|
281
|
+
release is tagged `v1`; until then use the branch, or pin a commit SHA.
|
|
282
|
+
|
|
283
|
+
| input | default | what it does |
|
|
284
|
+
|---------------------|------------|-----------------------------------------------|
|
|
285
|
+
| `junit` | (required) | path to the JUnit XML → `flakebox record --junit` (a report with no test cases is warned about, not recorded) |
|
|
286
|
+
| `db-key` | `flakebox` | cache key prefix for the history DB |
|
|
287
|
+
| `min-runs` | `5` | executed runs before "flaky" → `flakebox report --min-runs` |
|
|
288
|
+
| `fail-on-flaky` | `false` | fail the step when flaky tests are found that are not quarantined |
|
|
289
|
+
| `quarantine-output` | (off) | run `flakebox quarantine --format json` after the report and write the whole set here |
|
|
290
|
+
| `export-output` | (off) | `flakebox export --out` the history DB here |
|
|
291
|
+
|
|
292
|
+
**The acknowledgement loop.** The history DB lives in the CI cache, where
|
|
293
|
+
nobody can run `flakebox quarantine` by hand — so on its own,
|
|
294
|
+
`fail-on-flaky: true` fails *every* build from the first flake. With
|
|
295
|
+
`quarantine-output` set, the action quarantines the flaky set in the
|
|
296
|
+
history DB right after the report (before the fail step): a new flake
|
|
297
|
+
fails exactly one build, is acknowledged from the next run on, and the
|
|
298
|
+
JSON file names it. Then, on a laptop:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
gh run download --name flakebox # quarantine.json + history.json
|
|
302
|
+
flakebox import flakebox/history.json # CI's history, locally
|
|
303
|
+
flakebox quarantine --format pytest -o . # flakebox_quarantine.{py,json}
|
|
304
|
+
git add flakebox_quarantine.py flakebox_quarantine.json && git commit
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
and run the suite with `python -m pytest -p flakebox_quarantine` (skip
|
|
308
|
+
mode) or `--mode xfail` so the quarantined tests keep reporting and can
|
|
309
|
+
earn their way back out. When the report says a test is stable,
|
|
310
|
+
`flakebox unquarantine --test ...` locally, regenerate the plugin, commit.
|
|
311
|
+
|
|
312
|
+
Input names use dashes everywhere (`min-runs`, never `min_runs`); the test
|
|
313
|
+
suite reads `action.yml` and checks that every referenced input is
|
|
314
|
+
declared, every declared input is used, and every CLI flag it passes
|
|
315
|
+
exists. Matrix builds: see the cache-race note at the top of `action.yml`
|
|
316
|
+
(per-job DBs, then an aggregator job that `flakebox import`s every job's
|
|
317
|
+
export).
|
|
318
|
+
|
|
319
|
+
**No expression ever reaches a shell.** GitHub pastes the value of a
|
|
320
|
+
`${{ ... }}` expression into a `run:` script *before* bash parses it, so a
|
|
321
|
+
branch named `x";touch${IFS}pwned;echo"y` (a legal git ref) would run a
|
|
322
|
+
command in your job. The action passes every input and `github.*` value
|
|
323
|
+
through the step's `env:` and reads it as a quoted variable
|
|
324
|
+
(`"$FB_BRANCH"`), so the shell only ever sees a string. Each option takes
|
|
325
|
+
its value as one word, `--branch="$FB_BRANCH"`, so a value that starts
|
|
326
|
+
with `-` (a ref or a path can) is still that option's value and never the
|
|
327
|
+
start of another option. The test suite scans `action.yml` for `${{`
|
|
328
|
+
inside any `run:` and for an option and its value written as two words,
|
|
329
|
+
and runs each script under bash with hostile values and with values that
|
|
330
|
+
start with `-`. The two `actions/cache` steps are first-party
|
|
331
|
+
and use the `v4` tag, not a commit SHA; pin them in a fork if your policy
|
|
332
|
+
requires it.
|
|
333
|
+
|
|
334
|
+
## Limitations
|
|
335
|
+
|
|
336
|
+
- **Recency is wall-clock time.** "Newest" is when a run was recorded
|
|
337
|
+
(an imported run keeps the time it was exported with); runs recorded in
|
|
338
|
+
the same second tie and fall back to the order they were added, and
|
|
339
|
+
machines whose clocks disagree order their merged runs by those clocks.
|
|
340
|
+
- **`--format deselect` is a guess** at pytest node ids (see above); the
|
|
341
|
+
generated plugin is the reliable path, `eval` is needed for ids with a
|
|
342
|
+
space or a shell character, and the PowerShell form is untested.
|
|
343
|
+
- **Any DOCTYPE is refused**, harmless or not: JUnit reports never need
|
|
344
|
+
one, and refusing them all keeps the XML-bomb check simple.
|
|
345
|
+
- **The Action's cache steps use the `v4` tag**, not a commit SHA (above).
|
|
346
|
+
|
|
347
|
+
## Tests
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
python3 -m unittest discover -s tests # no network, stdlib only
|
|
351
|
+
python3 -m pytest -q # same suite
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
No PyYAML anywhere (the tests read `action.yml` with a tiny flat reader
|
|
355
|
+
and prove `import flakebox` works with `yaml` blocked). Tests point
|
|
356
|
+
`HOME`/`USERPROFILE`/`APPDATA` at a throwaway directory, so they never
|
|
357
|
+
touch your real `~/.flakebox`. A few tests drive real `pytest` as a
|
|
358
|
+
subprocess to prove the mapped `--deselect` ids and the generated plugin
|
|
359
|
+
work on parametrised ids (skipped when pytest is not installed). Runs on Windows 11: stdout/stderr are
|
|
360
|
+
reconfigured to replace unencodable characters (the report's emoji on a
|
|
361
|
+
cp1252 console), and every text file is opened as UTF-8.
|
|
362
|
+
|
|
363
|
+
## License
|
|
364
|
+
|
|
365
|
+
MIT — see `LICENSE`.
|