deflake-py 0.1.0__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.
Files changed (42) hide show
  1. deflake_py-0.1.0/.gitignore +25 -0
  2. deflake_py-0.1.0/CHANGELOG.md +16 -0
  3. deflake_py-0.1.0/LICENSE.txt +21 -0
  4. deflake_py-0.1.0/PKG-INFO +256 -0
  5. deflake_py-0.1.0/README.md +229 -0
  6. deflake_py-0.1.0/pyproject.toml +91 -0
  7. deflake_py-0.1.0/src/deflake/__init__.py +6 -0
  8. deflake_py-0.1.0/src/deflake/__main__.py +7 -0
  9. deflake_py-0.1.0/src/deflake/adapters/__init__.py +115 -0
  10. deflake_py-0.1.0/src/deflake/adapters/django.py +40 -0
  11. deflake_py-0.1.0/src/deflake/adapters/pytest.py +78 -0
  12. deflake_py-0.1.0/src/deflake/adapters/unittest.py +48 -0
  13. deflake_py-0.1.0/src/deflake/bisect.py +290 -0
  14. deflake_py-0.1.0/src/deflake/check.py +184 -0
  15. deflake_py-0.1.0/src/deflake/child.py +295 -0
  16. deflake_py-0.1.0/src/deflake/cli.py +227 -0
  17. deflake_py-0.1.0/src/deflake/django/__init__.py +19 -0
  18. deflake_py-0.1.0/src/deflake/django/databases.py +70 -0
  19. deflake_py-0.1.0/src/deflake/django/leaks.py +344 -0
  20. deflake_py-0.1.0/src/deflake/django/runner.py +212 -0
  21. deflake_py-0.1.0/src/deflake/events.py +123 -0
  22. deflake_py-0.1.0/src/deflake/execution.py +695 -0
  23. deflake_py-0.1.0/src/deflake/investigation.py +211 -0
  24. deflake_py-0.1.0/src/deflake/leaks/__init__.py +111 -0
  25. deflake_py-0.1.0/src/deflake/leaks/clock.py +31 -0
  26. deflake_py-0.1.0/src/deflake/leaks/env.py +40 -0
  27. deflake_py-0.1.0/src/deflake/leaks/logs.py +49 -0
  28. deflake_py-0.1.0/src/deflake/leaks/modules.py +582 -0
  29. deflake_py-0.1.0/src/deflake/leaks/patches.py +39 -0
  30. deflake_py-0.1.0/src/deflake/leaks/process.py +97 -0
  31. deflake_py-0.1.0/src/deflake/leaks/states.py +449 -0
  32. deflake_py-0.1.0/src/deflake/leaks/threads.py +17 -0
  33. deflake_py-0.1.0/src/deflake/leaks/warning_filters.py +24 -0
  34. deflake_py-0.1.0/src/deflake/models.py +507 -0
  35. deflake_py-0.1.0/src/deflake/pytest_plugin.py +526 -0
  36. deflake_py-0.1.0/src/deflake/repeat.py +200 -0
  37. deflake_py-0.1.0/src/deflake/reports/__init__.py +37 -0
  38. deflake_py-0.1.0/src/deflake/reports/json.py +86 -0
  39. deflake_py-0.1.0/src/deflake/reports/text.py +213 -0
  40. deflake_py-0.1.0/src/deflake/result_file.py +25 -0
  41. deflake_py-0.1.0/src/deflake/tail.py +76 -0
  42. deflake_py-0.1.0/src/deflake/unittest_runner.py +391 -0
@@ -0,0 +1,25 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .pytest_cache/
11
+ .coverage
12
+ htmlcov/
13
+
14
+ # Scratch space
15
+ /tmp/
16
+
17
+ # macOS
18
+ .DS_Store
19
+ ._*
20
+
21
+ # Editors
22
+ .vscode/
23
+ .history/
24
+ *.code-workspace
25
+ .idea/
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0]
4
+
5
+ First public release.
6
+
7
+ - `deflake TEST_PATH[:LINE|::NAME]` runs a test file many times, each in a fresh process with a new seed, and reports how often every test failed, with errors grouped by message and the seeds that reproduce them. A folder checks every test file in it.
8
+ - Runs pytest, Django's `manage.py test` (on top of the project's own `TEST_RUNNER`) and unittest; the runner is picked from the project, or with `--runner`.
9
+ - Every failing test is run again on its own, which tells a test that fails on its own from one that fails only after other tests.
10
+ - Bisect finds the tests that break an order-dependent test, names every set of them that does it on its own, and points at a fixture or hook of a group when any of its tests would do. A test that fails at random gets no culprits.
11
+ - Leaks of global state: environment variables, the working directory, freezegun and time-machine, module globals, `mock.patch`, `sys`, logging, warning filters, threads, signal handlers, the socket timeout and the `decimal` context; with Django also settings, local memory caches, the active language, time zone and URLconf, signal receivers and database rows. Leaks of fixtures wider than a test and of `setUpClass` are reported apart from the tests.
12
+ - A leak next to a failure is proven its cause: deflake puts the state back after the culprit, sees the test pass, sets it again and sees the test fail. This works for environment variables, module values, `mock.patch`, freezegun, the working directory, `sys.path`, the recursion limit, the socket timeout, the decimal context, signal handlers, warning filters and logging, and under Django for the active language, time zone, URLconf, settings, local memory caches and signal receivers. Other leaks of a culprit are shown under the failing test as not checked as the cause.
13
+ - `random`, Faker, factory_boy and NumPy are seeded before every test.
14
+ - `--jobs` runs test processes at once, with test databases of their own under Django and pytest-django; `--timeout` stops a hanging one; `--format json` gives a report for tools.
15
+ - An interrupt lets the tests tear down what they set up, as test databases, before their processes are stopped; a second interrupt stops them at once. Processes the tests start, servers in a session of their own included, are stopped with them.
16
+ - Plugins that reorder or rerun tests (pytest-randomly, pytest-random-order, pytest-xdist, pytest-rerunfailures, flaky) are switched off in the test processes deflake starts, and tests marked `flaky` are pointed out.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Роман Беляев
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,256 @@
1
+ Metadata-Version: 2.5
2
+ Name: deflake-py
3
+ Version: 0.1.0
4
+ Summary: Find flaky tests in pytest, unittest and Django and explain why they fail.
5
+ Project-URL: Homepage, https://github.com/SlyDeath/python-deflake
6
+ Project-URL: Source, https://github.com/SlyDeath/python-deflake
7
+ Project-URL: Changelog, https://github.com/SlyDeath/python-deflake/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/SlyDeath/python-deflake/issues
9
+ Author-email: Roman Belyaev <slyrs@yandex.ru>
10
+ License-Expression: MIT
11
+ License-File: LICENSE.txt
12
+ Keywords: bisect,django,flaky,pytest,test pollution,tests,unittest
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Framework :: Django
16
+ Classifier: Framework :: Pytest
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Operating System :: MacOS
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Topic :: Software Development :: Testing
23
+ Requires-Python: >=3.11
24
+ Provides-Extra: django
25
+ Requires-Dist: django>=5.2; extra == 'django'
26
+ Description-Content-Type: text/markdown
27
+
28
+ <div align="center">
29
+
30
+ # deflake
31
+
32
+ **Find flaky tests in pytest, unittest and Django and explain why they fail.**
33
+
34
+ Repeat · Isolate · Bisect · Detect leaked state
35
+
36
+ [![PyPI](https://img.shields.io/pypi/v/deflake-py?logo=pypi&logoColor=white&color=3775a9)](https://pypi.org/project/deflake-py/)
37
+ [![CI](https://github.com/SlyDeath/python-deflake/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/SlyDeath/python-deflake/actions/workflows/ci.yml)
38
+ [![Python](https://img.shields.io/pypi/pyversions/deflake-py?logo=python&logoColor=white)](https://www.python.org)
39
+ [![License: MIT](https://img.shields.io/github/license/SlyDeath/python-deflake?color=blue)](LICENSE.txt)
40
+
41
+ </div>
42
+
43
+ A flaky test fails once in a while, someone adds a rerun, and the cause stays.
44
+ deflake runs a suspicious test many times and answers three questions:
45
+
46
+ 1. **How often does it fail**, and with which errors and seeds?
47
+ 2. **Does it fail on its own**, or only after some other test? If the latter, which one?
48
+ 3. **What global state does a test leave behind**: environment variables, the clock, module globals, active `mock.patch`es, Django settings, caches and database rows?
49
+ 4. **Is that leak what breaks the other test?** deflake puts it back and sets it again to prove it.
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ pip install deflake-py
55
+ ```
56
+
57
+ or add `deflake-py` to the development dependencies of your project. The package is `deflake-py`; the command and the module are `deflake`. It needs Python 3.11 or newer on Linux (with pidfd, kernel 5.3 or newer) or macOS, and has no dependencies of its own. It works with pytest 8 or newer, unittest and Django 5.2 or newer.
58
+
59
+ ## Usage
60
+
61
+ Run deflake from the root of your project, where you run your tests (next to `manage.py` in a Django project):
62
+
63
+ ```bash
64
+ deflake tests/test_orders.py --runs 50
65
+ deflake tests/test_orders.py::test_total --runs 50 --jobs 4
66
+ deflake tests/test_orders.py:42 --runs 50
67
+ deflake shop/tests.py::OrderTest --runs 20
68
+ deflake tests --runs 5
69
+ ```
70
+
71
+ ```
72
+ test_pollutes: failed 0 of 10 runs (0%)
73
+ leaks: os.environ['FEATURE_FLAG'] was set (10 of 10 runs)
74
+ leaks: test_order.STATE was changed (10 of 10 runs)
75
+ test_victim: failed 5 of 10 runs (50%)
76
+ verdict: fails only after other tests (alone: failed 0 of 10 runs (0%))
77
+ culprit: test_pollutes
78
+ reproduce: DEFLAKE_SEED=40331 python -m pytest -p deflake.pytest_plugin test_order.py
79
+ seeds 40331, 48172, 30602, 40073, 7643:
80
+ assert not True
81
+ at test_order.py:12
82
+ ```
83
+
84
+ What happens:
85
+
86
+ 1. **The whole file runs `--runs` times** (10 by default), every time in a fresh process with a new seed. The seed shuffles the classes and the tests inside them, but keeps every module and class together, so their fixtures, `setUpClass` and `setUpModule` still run once (pytest itself still regroups tests by the parameters of module and class fixtures, as it does without deflake). Before every test `random` is seeded with it, and so are Faker, factory_boy and NumPy when the tests use them. Running the whole file, not just one test, is what makes order-dependent failures show up. A test that takes its process down (`os._exit`, a crash, a signal) counts as failed in that run.
87
+ 2. **Every test that failed runs again on its own**, as many times. The verdict is *fails on its own* if it still fails, which points at time, randomness or a race inside the test, or *fails only after other tests* if it does not. When it cannot run alone at all, as with errors outside of tests, the verdict is *failed, could not run it alone*.
88
+ 3. **Order-dependent failures are bisected** with one of the failing seeds, down to a smallest set of tests that break the victim. deflake has the test runner run just those tests in the recorded order (pytest may regroup them by parametrized fixtures, as above) and watches only the victim, so other failing tests do not get in the way. A failure counts only when it is the failure seen in the runs of the file (the same error apart from numbers, or the same kind of error at the same line of the test file) and happens twice in a row, the set found must break the victim again at the end, and the victim must pass every time without each culprit, so a test that fails at random gets no culprits: the verdict is then *fails at random, not after particular tests*, with the command that runs that seed. When the failure came back with the same order but not with fewer tests, the report says so, with the command of that seed. When other tests break the victim just as well on their own, they are named too, after `or, just as well:`. Up to three failing seeds are tried. A culprit that two other tests of its class or module replace, because the state comes from a fixture they share, is named as such. A victim stopped by `--timeout` does not count as failing in a bisect, and a run that never got to the victim, because another test hung, is tried again.
89
+ 4. **Every test is checked for leaks**, tests that skip themselves included: a snapshot of global state before the test, its fixtures and `setUp` included, and after their teardown. For a Django test that is before its transaction, fixtures and settings are set up and after they are rolled back, and for an `IsolatedAsyncioTestCase` after its event loop is closed. What a fixture wider than a test (`scope="module"`, `"class"`, `"package"` or `"session"`) or a `setUpClass` or `setUpModule` sets up and never puts back shows up under *errors outside of tests*, as `fixture NAME (module scope), outside its tests: ...`, not as a leak of the test that happened to run first.
90
+
91
+ A process that fails while every test in it passed, a file that cannot be collected or imported, or a failing `setUpClass`, is reported as *errors outside of tests*, with the end of its output. A run that breaks before its first test does not stop the others; only when no run gets to a test does deflake give up with exit code 2 and show how the test process ended.
92
+
93
+ `TEST_PATH::NAME` runs the whole file but reports only that test: all its parameters for a parametrized test, every test of a class for a class name. A unittest or Django test is named `Class.test_name`, and `::test_name` or `::Class` works too. `TEST_PATH:LINE` does the same for the test whose body holds the line, every test of the innermost class around the line, or else the test declared closest above it. A selected test that never started in any run makes deflake say so and exit with 1; the JSON report lists it with the verdict `not_run`.
94
+
95
+ `TEST_PATH` may be a folder: deflake then finds every `test_*.py`, `*_test.py` and `tests.py` in it and in its subfolders, leaving out virtual environments, `site-packages` and `node_modules`, and checks them one after another, each as if it were given on its own. The report of each file starts with `== FILE (3 of 40)`; with `--format json` that line goes to stderr as `deflake: checking FILE (3 of 40)`. A file that cannot run is reported and the others are still checked. At the end deflake names the files with failing or leaking tests and those it could not check, also after Ctrl-C, for the files checked so far. When the reader of the report stops early, as `head` does, deflake stops too, with exit code 141 if files were left unchecked.
96
+
97
+ ### Options
98
+
99
+ | Option | Default | |
100
+ | --- | --- | --- |
101
+ | `-n`, `--runs N` | 10 | How many times to run each file |
102
+ | `-j`, `--jobs N` | 1 | How many test processes to run at once |
103
+ | `-t`, `--timeout SECONDS` | none | Stop a test process that runs longer; the test it was running counts as failed |
104
+ | `-f`, `--format FORMAT` | `text` | `text` or `json` |
105
+ | `-r`, `--runner RUNNER` | `auto` | `auto`, `pytest`, `django` or `unittest` |
106
+
107
+ Exit code: 0 when every test passed and nothing leaked (or every test skipped itself), 1 otherwise, 2 on usage errors, when the file has no tests or a filter of the project leaves them all out, or when the tests could not run (deflake then shows their output). For a folder it is the highest code of its files, and 2 when it has no test files. When interrupted with Ctrl-C, deflake interrupts its test processes, gives their tests up to 5 seconds to tear down what they set up, such as test databases, kills what is left and exits with 130 (a second Ctrl-C kills them at once), so a shell loop around it stops too. SIGTERM and SIGHUP, as from a CI timeout or a closed terminal, stop the test processes as well, with exit code 143 or 129.
108
+
109
+ ### Test runners
110
+
111
+ With `--runner auto` deflake picks pytest when the project configures it (`pytest.toml`, `pytest.ini`, `[tool.pytest]` or `[tool.pytest.ini_options]` in `pyproject.toml`, `[pytest]` in `tox.ini`, `[tool:pytest]` in `setup.cfg`, or a `conftest.py` from the file up to the current folder) or when the file imports pytest or has test functions of its own. Otherwise it is Django's runner when there is a `manage.py` in the current folder, and unittest when there is not. A project with pytest-django goes to pytest, and Django's leak detectors work there too.
112
+
113
+ | Runner | Command | Reproduce |
114
+ | --- | --- | --- |
115
+ | pytest | `python -m pytest -p deflake.pytest_plugin FILE` | `DEFLAKE_SEED=N python -m pytest -p deflake.pytest_plugin FILE` |
116
+ | Django | `python manage.py test --testrunner deflake.django.Runner FILE` | `DEFLAKE_SEED=N python manage.py test --testrunner deflake.django.Runner FILE` |
117
+ | unittest | `python -m deflake.unittest_runner FILE` | `DEFLAKE_SEED=N python -m deflake.unittest_runner FILE` |
118
+
119
+ Run the `reproduce:` command as it is: `DEFLAKE_SEED` alone does nothing in a plain test run.
120
+
121
+ #### pytest
122
+
123
+ deflake runs pytest with the project's own configuration, and takes the order, the selection and the results into its own hands:
124
+
125
+ - Plugins that reorder or rerun tests are switched off in these processes: pytest-randomly, pytest-random-order, pytest-xdist, pytest-rerunfailures and flaky. Their options still parse, so `addopts = -n auto --reruns 3` keeps working; a `flaky` marker is accepted under `--strict-markers`.
126
+ - So are options that select, reorder or stop the tests on their own: `--lf`, `--ff`, `--nf`, `--sw`, `-x`, `--maxfail`, `--pdb` and `--trace`.
127
+ - pytest's cache goes to a folder of deflake's own, so `--lf` in your next run still remembers your last run, not deflake's.
128
+ - A test marked `flaky` is reported as such: its failures are hidden by reruns in the project's own runs, and a marked test that never fails here may not need the marker any more.
129
+ - pytest-timeout stays on and its timer is not reported as a leak. Its `signal` method fails the test as usual. Its `thread` method ends the whole process, so deflake reports the test as interrupted, sees no leaks after it and never takes such a run as evidence of a cause. deflake's own `--timeout` limits the whole test process.
130
+
131
+ #### Django
132
+
133
+ `deflake.django.Runner` is the project's own `TEST_RUNNER` with deflake on top, so what a custom runner sets up still happens. The file is taken as the module it is on the project's `sys.path`, as `manage.py` sets it up, so apps kept in a folder such as `apps/` are found. Django runs `TestCase`s first, then other database tests, then the rest; the seed shuffles each kind and keeps that order. `--parallel`, `--shuffle`, `--reverse`, `--failfast`, `--pdb` and `--debug-sql` are switched off, as deflake runs tests in its own order, one process per run.
134
+
135
+ #### unittest
136
+
137
+ `deflake.unittest_runner` imports the file as `python -m unittest FILE` does, as a module named after its path from the current folder, and runs its tests with unittest's own runner, `load_tests` included.
138
+
139
+ Every test process runs in a session of its own, without a terminal, as in CI: its input is empty and its output is not a TTY.
140
+
141
+ Processes left in its group are killed when the run ends. On Linux and macOS, processes started from the test interpreter through Python's `subprocess.Popen`, `os.fork` and `os.posix_spawn` are also registered and stopped even if they create another session, together with the processes of the group such a process leads, as a server started with `start_new_session=True` and its workers. Cleanup sends signals through Linux pidfds or macOS audit tokens, so PID reuse cannot redirect a signal to a replacement process. Group members are captured before the test process is reaped and signalled individually through these identities. A system without the required kernel API is rejected before tests start; cleanup never falls back to numeric PID or group signals. Forked Python children inherit this tracking until they exec another program. Programs that daemonize further after exec, out of that group, must be stopped by the test's teardown.
142
+
143
+ ### Parallel runs and databases
144
+
145
+ With `--jobs` several processes run at once. Each gets `DEFLAKE_JOB` set to `1`, `2`, `3`... With Django's runner and with pytest-django every process gets test databases of its own: `test_myapp_1`, `test_myapp_2`... (an in-memory SQLite database is the process's own anyway). Tests that share another outside resource, such as a port, can do the same:
146
+
147
+ ```python
148
+ # conftest.py
149
+ import os
150
+
151
+ DATABASE = f"myapp_test{os.environ.get('DEFLAKE_JOB', '')}"
152
+ ```
153
+
154
+ The runs alone use `--jobs` and `DEFLAKE_JOB` as well. Bisect runs one process at a time, without `DEFLAKE_JOB`.
155
+
156
+ ### Leak detectors
157
+
158
+ | Detector | Reports |
159
+ | --- | --- |
160
+ | Environment | Variables in `os.environ` set, changed or removed |
161
+ | Working directory | `os.chdir` not undone |
162
+ | Clock | freezegun or time-machine left on (`start()` without `stop()`) |
163
+ | Module globals | Global variables of the project's own modules assigned or changed in place; modules of installed packages are left out. Data is compared by value: numbers, strings, dates, paths and the like, lists, tuples, dicts and sets of them, objects of the standard library such as a counter, and objects of the project's own classes by their attributes, dataclasses, namespaces and pydantic models included. A global of more than 50,000 parts is compared as `pickle` dumps it, by the same rules, but with the order of its keys and of the items of its sets. An object of an installed library, such as a client, a logger, a QuerySet, a Faker or a lazy proxy, counts by identity: replacing it is a leak, what it sets up inside on its own use is not, and it is never asked to compute itself. Random generators and decimal contexts count by identity too, as they change on their own use. Python's own files count as installed even when its environment is inside the project folder, as with pixi or conda. Cached properties and methods an object of the project puts on itself on first use are left out |
164
+ | Patches | `mock.patch(...).start()` without `stop()` |
165
+ | sys | `sys.path`, the standard streams and the recursion limit |
166
+ | Logging | The level, handlers, `propagate` or `disabled` of a logger, new loggers included, and `logging.disable()` |
167
+ | Warning filters | `warnings.simplefilter` or `filterwarnings` outside of `catch_warnings` |
168
+ | Threads | Threads a test started and left running; numbers in their names, as in `Thread-3` or `ThreadPoolExecutor-0_1`, are left out, as they depend on the tests before |
169
+ | Signals | Signal handlers replaced and not put back |
170
+ | Process settings | `socket.setdefaulttimeout` and the `decimal` context |
171
+
172
+ With Django configured, by its test runner or by pytest-django, these are watched too:
173
+
174
+ | Detector | Reports |
175
+ | --- | --- |
176
+ | Settings | A setting assigned and not put back, and `override_settings(...).enable()` without `disable()` |
177
+ | Caches | New keys in a local memory cache (`LocMemCache`); other backends live outside of the test process |
178
+ | Activated | `translation.activate`, `timezone.activate` and `set_urlconf` without a matching deactivate |
179
+ | Receivers | Signal receivers of Django's own signals connected or disconnected and not put back; a file storage connecting itself on first use is Django's own doing |
180
+ | Rows | Rows created or deleted outside of a rolled back transaction, in every database, as a plain `unittest.TestCase` or a `TransactionTestCase` that flushes the data of migrations does. A test that may not use the database, such as a `SimpleTestCase`, is not checked. Only test databases are counted, never the project's own, and a mirror (`TEST['MIRROR']`) is counted with the database it mirrors |
181
+
182
+ A detector is any object with two methods: `snapshot()` returns the current state, or `None` when it does not apply, and `diff(before, after)` returns one message per leak. Add your own in `conftest.py`; detectors run only in the test processes deflake starts, so your regular test runs are not affected:
183
+
184
+ ```python
185
+ from deflake import leaks
186
+
187
+
188
+ class FeatureFlagLeaks:
189
+ def snapshot(self):
190
+ return sorted(FeatureFlags.enabled())
191
+
192
+ def diff(self, before, after):
193
+ return [f"feature flag {flag} left on" for flag in sorted(set(after) - set(before))]
194
+
195
+
196
+ leaks.detectors.append(FeatureFlagLeaks())
197
+ ```
198
+
199
+ The Module globals detector compares every global of the project before and after each test, and the Rows detector counts the rows of every table; if one of them is too slow for your project, leave it out, in `conftest.py` or in the test settings of a Django project:
200
+
201
+ ```python
202
+ from deflake import leaks
203
+ from deflake.django.leaks import Rows
204
+ from deflake.leaks.modules import Modules
205
+
206
+ leaks.excluded += [Modules, Rows]
207
+ ```
208
+
209
+ ### Proving a leak is the cause
210
+
211
+ A leak next to a failure is a suspect, not a proof. For each culprit deflake takes every piece of state it leaked and runs an experiment in fresh processes with the same seed and order: two runs as they are, two that put the state back after the culprit, and two that put it back and set it again. A leak is confirmed as the cause only when the victim fails the same way in the runs as they are and when set again, and passes both times when the state is put back. Skips, timeouts, interrupted processes, errors outside of tests and other failures never confirm a cause.
212
+
213
+ ```
214
+ test_victim: failed 5 of 10 runs (50%)
215
+ verdict: fails only after other tests (alone: failed 0 of 10 runs (0%))
216
+ culprit: test_pollutes
217
+ environment cause: FEATURE_FLAG from test_order.py::test_pollutes: confirmed (seed 40331)
218
+ baseline: same_failure, same_failure; repair: passed, passed; reapply: same_failure, same_failure
219
+ ```
220
+
221
+ This works for:
222
+
223
+ - environment variables and plain values of the project's modules (`None`, bools, numbers, strings and bytes);
224
+ - `mock.patch` left started, set again with the culprit's own mock, and the keys a `mock.patch.dict` patched;
225
+ - time freezegun left frozen;
226
+ - the working directory, `sys.path`, the recursion limit, the socket timeout, the `decimal` context, signal handlers, warning filters and logging;
227
+ - under Django, the active language, time zone and URLconf, settings (an `override_settings` left enabled is tried setting by setting), local memory caches and signal receivers.
228
+
229
+ Other leaks of a culprit, such as a list changed in place, rows of a database, a thread or time-machine, are shown under the victim as `not checked as the cause`: they are next to the failure, but not proven to cause it. Only what the report shows as the culprit's own leaks is tried, up to twenty per culprit; a change of a wider fixture set up with it is not the culprit's. A confirmed cause holds for the order tested; putting a value back cannot undo copies of it already made into other objects or services.
230
+
231
+ The JSON report adds `causes` to each test, with `culprit`, `key`, `seed`, `baseline`, `repair`, `reapply`, `detector` (`environment`, `module`, `patch`, `clock`, `process` or `django`) and `confirmed`, and `culprit_leaks` with every leak of the culprits and whether it was `checked` and `confirmed`. Values of variables are never included.
232
+
233
+ ### JSON report
234
+
235
+ `--format json` prints one array for the whole run, with one object per test that ran or leaked (a test that skipped itself in every run and leaked nothing is left out, as in the text report) with `file`, `name`, `id` (the pytest node id), `verdict` (`stable`, `failed`, `fails_on_its_own`, `order_dependent`, `random`, `not_run` for a selected test that never started, or `not_checked` for a file of a folder that could not run, with its error under `errors`; `failed` means a test could not run alone, or that every run looking for its culprits broke before it), `runs` and `isolated_runs` counts (`isolated_runs` is `null` when the test never failed or could not run alone) (`total`, `failures`, `failure_rate` and `skipped`: `total` leaves out the runs where the test skipped itself), `errors` with their seeds, `leaks` (with `runs`, counting the runs where the test skipped itself too) and `culprits` (`ids`, `names`, `command`, `alone` when the test fails with no test before it, `group_wide`, `others` and `other_names` for further sets of tests that break it just as well on their own, `reproduced` when the failure came back with the same order but not with fewer tests, and `broken` when every run broke before the test) and `marked_flaky`. The format may change until 1.0.
236
+
237
+ ## Tested on real projects
238
+
239
+ [Recorded cases](https://github.com/SlyDeath/python-deflake/blob/main/docs/real_world_cases.json) from SecureNative Python, Runway model-sdk and humanize, at pinned revisions: deflake finds the known flaky test and its cause before the published fix, and the test passes after it.
240
+
241
+ ## Limitations
242
+
243
+ - deflake runs one file at a time, even when given a folder. A test broken by a test in another file shows up as failing only when the two files run together, which deflake does not do yet.
244
+ - A folder is searched by file name, not by your `python_files` setting.
245
+ - Leak detection sees what changed in the test process. It cannot see changes made by other processes, such as a browser or a server started by a test.
246
+
247
+ ## Development
248
+
249
+ ```bash
250
+ uv sync
251
+ uv run ruff check && uv run ruff format --check && uv run mypy && uv run pytest
252
+ ```
253
+
254
+ ## License
255
+
256
+ MIT, see [LICENSE.txt](https://github.com/SlyDeath/python-deflake/blob/main/LICENSE.txt).
@@ -0,0 +1,229 @@
1
+ <div align="center">
2
+
3
+ # deflake
4
+
5
+ **Find flaky tests in pytest, unittest and Django and explain why they fail.**
6
+
7
+ Repeat · Isolate · Bisect · Detect leaked state
8
+
9
+ [![PyPI](https://img.shields.io/pypi/v/deflake-py?logo=pypi&logoColor=white&color=3775a9)](https://pypi.org/project/deflake-py/)
10
+ [![CI](https://github.com/SlyDeath/python-deflake/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/SlyDeath/python-deflake/actions/workflows/ci.yml)
11
+ [![Python](https://img.shields.io/pypi/pyversions/deflake-py?logo=python&logoColor=white)](https://www.python.org)
12
+ [![License: MIT](https://img.shields.io/github/license/SlyDeath/python-deflake?color=blue)](LICENSE.txt)
13
+
14
+ </div>
15
+
16
+ A flaky test fails once in a while, someone adds a rerun, and the cause stays.
17
+ deflake runs a suspicious test many times and answers three questions:
18
+
19
+ 1. **How often does it fail**, and with which errors and seeds?
20
+ 2. **Does it fail on its own**, or only after some other test? If the latter, which one?
21
+ 3. **What global state does a test leave behind**: environment variables, the clock, module globals, active `mock.patch`es, Django settings, caches and database rows?
22
+ 4. **Is that leak what breaks the other test?** deflake puts it back and sets it again to prove it.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install deflake-py
28
+ ```
29
+
30
+ or add `deflake-py` to the development dependencies of your project. The package is `deflake-py`; the command and the module are `deflake`. It needs Python 3.11 or newer on Linux (with pidfd, kernel 5.3 or newer) or macOS, and has no dependencies of its own. It works with pytest 8 or newer, unittest and Django 5.2 or newer.
31
+
32
+ ## Usage
33
+
34
+ Run deflake from the root of your project, where you run your tests (next to `manage.py` in a Django project):
35
+
36
+ ```bash
37
+ deflake tests/test_orders.py --runs 50
38
+ deflake tests/test_orders.py::test_total --runs 50 --jobs 4
39
+ deflake tests/test_orders.py:42 --runs 50
40
+ deflake shop/tests.py::OrderTest --runs 20
41
+ deflake tests --runs 5
42
+ ```
43
+
44
+ ```
45
+ test_pollutes: failed 0 of 10 runs (0%)
46
+ leaks: os.environ['FEATURE_FLAG'] was set (10 of 10 runs)
47
+ leaks: test_order.STATE was changed (10 of 10 runs)
48
+ test_victim: failed 5 of 10 runs (50%)
49
+ verdict: fails only after other tests (alone: failed 0 of 10 runs (0%))
50
+ culprit: test_pollutes
51
+ reproduce: DEFLAKE_SEED=40331 python -m pytest -p deflake.pytest_plugin test_order.py
52
+ seeds 40331, 48172, 30602, 40073, 7643:
53
+ assert not True
54
+ at test_order.py:12
55
+ ```
56
+
57
+ What happens:
58
+
59
+ 1. **The whole file runs `--runs` times** (10 by default), every time in a fresh process with a new seed. The seed shuffles the classes and the tests inside them, but keeps every module and class together, so their fixtures, `setUpClass` and `setUpModule` still run once (pytest itself still regroups tests by the parameters of module and class fixtures, as it does without deflake). Before every test `random` is seeded with it, and so are Faker, factory_boy and NumPy when the tests use them. Running the whole file, not just one test, is what makes order-dependent failures show up. A test that takes its process down (`os._exit`, a crash, a signal) counts as failed in that run.
60
+ 2. **Every test that failed runs again on its own**, as many times. The verdict is *fails on its own* if it still fails, which points at time, randomness or a race inside the test, or *fails only after other tests* if it does not. When it cannot run alone at all, as with errors outside of tests, the verdict is *failed, could not run it alone*.
61
+ 3. **Order-dependent failures are bisected** with one of the failing seeds, down to a smallest set of tests that break the victim. deflake has the test runner run just those tests in the recorded order (pytest may regroup them by parametrized fixtures, as above) and watches only the victim, so other failing tests do not get in the way. A failure counts only when it is the failure seen in the runs of the file (the same error apart from numbers, or the same kind of error at the same line of the test file) and happens twice in a row, the set found must break the victim again at the end, and the victim must pass every time without each culprit, so a test that fails at random gets no culprits: the verdict is then *fails at random, not after particular tests*, with the command that runs that seed. When the failure came back with the same order but not with fewer tests, the report says so, with the command of that seed. When other tests break the victim just as well on their own, they are named too, after `or, just as well:`. Up to three failing seeds are tried. A culprit that two other tests of its class or module replace, because the state comes from a fixture they share, is named as such. A victim stopped by `--timeout` does not count as failing in a bisect, and a run that never got to the victim, because another test hung, is tried again.
62
+ 4. **Every test is checked for leaks**, tests that skip themselves included: a snapshot of global state before the test, its fixtures and `setUp` included, and after their teardown. For a Django test that is before its transaction, fixtures and settings are set up and after they are rolled back, and for an `IsolatedAsyncioTestCase` after its event loop is closed. What a fixture wider than a test (`scope="module"`, `"class"`, `"package"` or `"session"`) or a `setUpClass` or `setUpModule` sets up and never puts back shows up under *errors outside of tests*, as `fixture NAME (module scope), outside its tests: ...`, not as a leak of the test that happened to run first.
63
+
64
+ A process that fails while every test in it passed, a file that cannot be collected or imported, or a failing `setUpClass`, is reported as *errors outside of tests*, with the end of its output. A run that breaks before its first test does not stop the others; only when no run gets to a test does deflake give up with exit code 2 and show how the test process ended.
65
+
66
+ `TEST_PATH::NAME` runs the whole file but reports only that test: all its parameters for a parametrized test, every test of a class for a class name. A unittest or Django test is named `Class.test_name`, and `::test_name` or `::Class` works too. `TEST_PATH:LINE` does the same for the test whose body holds the line, every test of the innermost class around the line, or else the test declared closest above it. A selected test that never started in any run makes deflake say so and exit with 1; the JSON report lists it with the verdict `not_run`.
67
+
68
+ `TEST_PATH` may be a folder: deflake then finds every `test_*.py`, `*_test.py` and `tests.py` in it and in its subfolders, leaving out virtual environments, `site-packages` and `node_modules`, and checks them one after another, each as if it were given on its own. The report of each file starts with `== FILE (3 of 40)`; with `--format json` that line goes to stderr as `deflake: checking FILE (3 of 40)`. A file that cannot run is reported and the others are still checked. At the end deflake names the files with failing or leaking tests and those it could not check, also after Ctrl-C, for the files checked so far. When the reader of the report stops early, as `head` does, deflake stops too, with exit code 141 if files were left unchecked.
69
+
70
+ ### Options
71
+
72
+ | Option | Default | |
73
+ | --- | --- | --- |
74
+ | `-n`, `--runs N` | 10 | How many times to run each file |
75
+ | `-j`, `--jobs N` | 1 | How many test processes to run at once |
76
+ | `-t`, `--timeout SECONDS` | none | Stop a test process that runs longer; the test it was running counts as failed |
77
+ | `-f`, `--format FORMAT` | `text` | `text` or `json` |
78
+ | `-r`, `--runner RUNNER` | `auto` | `auto`, `pytest`, `django` or `unittest` |
79
+
80
+ Exit code: 0 when every test passed and nothing leaked (or every test skipped itself), 1 otherwise, 2 on usage errors, when the file has no tests or a filter of the project leaves them all out, or when the tests could not run (deflake then shows their output). For a folder it is the highest code of its files, and 2 when it has no test files. When interrupted with Ctrl-C, deflake interrupts its test processes, gives their tests up to 5 seconds to tear down what they set up, such as test databases, kills what is left and exits with 130 (a second Ctrl-C kills them at once), so a shell loop around it stops too. SIGTERM and SIGHUP, as from a CI timeout or a closed terminal, stop the test processes as well, with exit code 143 or 129.
81
+
82
+ ### Test runners
83
+
84
+ With `--runner auto` deflake picks pytest when the project configures it (`pytest.toml`, `pytest.ini`, `[tool.pytest]` or `[tool.pytest.ini_options]` in `pyproject.toml`, `[pytest]` in `tox.ini`, `[tool:pytest]` in `setup.cfg`, or a `conftest.py` from the file up to the current folder) or when the file imports pytest or has test functions of its own. Otherwise it is Django's runner when there is a `manage.py` in the current folder, and unittest when there is not. A project with pytest-django goes to pytest, and Django's leak detectors work there too.
85
+
86
+ | Runner | Command | Reproduce |
87
+ | --- | --- | --- |
88
+ | pytest | `python -m pytest -p deflake.pytest_plugin FILE` | `DEFLAKE_SEED=N python -m pytest -p deflake.pytest_plugin FILE` |
89
+ | Django | `python manage.py test --testrunner deflake.django.Runner FILE` | `DEFLAKE_SEED=N python manage.py test --testrunner deflake.django.Runner FILE` |
90
+ | unittest | `python -m deflake.unittest_runner FILE` | `DEFLAKE_SEED=N python -m deflake.unittest_runner FILE` |
91
+
92
+ Run the `reproduce:` command as it is: `DEFLAKE_SEED` alone does nothing in a plain test run.
93
+
94
+ #### pytest
95
+
96
+ deflake runs pytest with the project's own configuration, and takes the order, the selection and the results into its own hands:
97
+
98
+ - Plugins that reorder or rerun tests are switched off in these processes: pytest-randomly, pytest-random-order, pytest-xdist, pytest-rerunfailures and flaky. Their options still parse, so `addopts = -n auto --reruns 3` keeps working; a `flaky` marker is accepted under `--strict-markers`.
99
+ - So are options that select, reorder or stop the tests on their own: `--lf`, `--ff`, `--nf`, `--sw`, `-x`, `--maxfail`, `--pdb` and `--trace`.
100
+ - pytest's cache goes to a folder of deflake's own, so `--lf` in your next run still remembers your last run, not deflake's.
101
+ - A test marked `flaky` is reported as such: its failures are hidden by reruns in the project's own runs, and a marked test that never fails here may not need the marker any more.
102
+ - pytest-timeout stays on and its timer is not reported as a leak. Its `signal` method fails the test as usual. Its `thread` method ends the whole process, so deflake reports the test as interrupted, sees no leaks after it and never takes such a run as evidence of a cause. deflake's own `--timeout` limits the whole test process.
103
+
104
+ #### Django
105
+
106
+ `deflake.django.Runner` is the project's own `TEST_RUNNER` with deflake on top, so what a custom runner sets up still happens. The file is taken as the module it is on the project's `sys.path`, as `manage.py` sets it up, so apps kept in a folder such as `apps/` are found. Django runs `TestCase`s first, then other database tests, then the rest; the seed shuffles each kind and keeps that order. `--parallel`, `--shuffle`, `--reverse`, `--failfast`, `--pdb` and `--debug-sql` are switched off, as deflake runs tests in its own order, one process per run.
107
+
108
+ #### unittest
109
+
110
+ `deflake.unittest_runner` imports the file as `python -m unittest FILE` does, as a module named after its path from the current folder, and runs its tests with unittest's own runner, `load_tests` included.
111
+
112
+ Every test process runs in a session of its own, without a terminal, as in CI: its input is empty and its output is not a TTY.
113
+
114
+ Processes left in its group are killed when the run ends. On Linux and macOS, processes started from the test interpreter through Python's `subprocess.Popen`, `os.fork` and `os.posix_spawn` are also registered and stopped even if they create another session, together with the processes of the group such a process leads, as a server started with `start_new_session=True` and its workers. Cleanup sends signals through Linux pidfds or macOS audit tokens, so PID reuse cannot redirect a signal to a replacement process. Group members are captured before the test process is reaped and signalled individually through these identities. A system without the required kernel API is rejected before tests start; cleanup never falls back to numeric PID or group signals. Forked Python children inherit this tracking until they exec another program. Programs that daemonize further after exec, out of that group, must be stopped by the test's teardown.
115
+
116
+ ### Parallel runs and databases
117
+
118
+ With `--jobs` several processes run at once. Each gets `DEFLAKE_JOB` set to `1`, `2`, `3`... With Django's runner and with pytest-django every process gets test databases of its own: `test_myapp_1`, `test_myapp_2`... (an in-memory SQLite database is the process's own anyway). Tests that share another outside resource, such as a port, can do the same:
119
+
120
+ ```python
121
+ # conftest.py
122
+ import os
123
+
124
+ DATABASE = f"myapp_test{os.environ.get('DEFLAKE_JOB', '')}"
125
+ ```
126
+
127
+ The runs alone use `--jobs` and `DEFLAKE_JOB` as well. Bisect runs one process at a time, without `DEFLAKE_JOB`.
128
+
129
+ ### Leak detectors
130
+
131
+ | Detector | Reports |
132
+ | --- | --- |
133
+ | Environment | Variables in `os.environ` set, changed or removed |
134
+ | Working directory | `os.chdir` not undone |
135
+ | Clock | freezegun or time-machine left on (`start()` without `stop()`) |
136
+ | Module globals | Global variables of the project's own modules assigned or changed in place; modules of installed packages are left out. Data is compared by value: numbers, strings, dates, paths and the like, lists, tuples, dicts and sets of them, objects of the standard library such as a counter, and objects of the project's own classes by their attributes, dataclasses, namespaces and pydantic models included. A global of more than 50,000 parts is compared as `pickle` dumps it, by the same rules, but with the order of its keys and of the items of its sets. An object of an installed library, such as a client, a logger, a QuerySet, a Faker or a lazy proxy, counts by identity: replacing it is a leak, what it sets up inside on its own use is not, and it is never asked to compute itself. Random generators and decimal contexts count by identity too, as they change on their own use. Python's own files count as installed even when its environment is inside the project folder, as with pixi or conda. Cached properties and methods an object of the project puts on itself on first use are left out |
137
+ | Patches | `mock.patch(...).start()` without `stop()` |
138
+ | sys | `sys.path`, the standard streams and the recursion limit |
139
+ | Logging | The level, handlers, `propagate` or `disabled` of a logger, new loggers included, and `logging.disable()` |
140
+ | Warning filters | `warnings.simplefilter` or `filterwarnings` outside of `catch_warnings` |
141
+ | Threads | Threads a test started and left running; numbers in their names, as in `Thread-3` or `ThreadPoolExecutor-0_1`, are left out, as they depend on the tests before |
142
+ | Signals | Signal handlers replaced and not put back |
143
+ | Process settings | `socket.setdefaulttimeout` and the `decimal` context |
144
+
145
+ With Django configured, by its test runner or by pytest-django, these are watched too:
146
+
147
+ | Detector | Reports |
148
+ | --- | --- |
149
+ | Settings | A setting assigned and not put back, and `override_settings(...).enable()` without `disable()` |
150
+ | Caches | New keys in a local memory cache (`LocMemCache`); other backends live outside of the test process |
151
+ | Activated | `translation.activate`, `timezone.activate` and `set_urlconf` without a matching deactivate |
152
+ | Receivers | Signal receivers of Django's own signals connected or disconnected and not put back; a file storage connecting itself on first use is Django's own doing |
153
+ | Rows | Rows created or deleted outside of a rolled back transaction, in every database, as a plain `unittest.TestCase` or a `TransactionTestCase` that flushes the data of migrations does. A test that may not use the database, such as a `SimpleTestCase`, is not checked. Only test databases are counted, never the project's own, and a mirror (`TEST['MIRROR']`) is counted with the database it mirrors |
154
+
155
+ A detector is any object with two methods: `snapshot()` returns the current state, or `None` when it does not apply, and `diff(before, after)` returns one message per leak. Add your own in `conftest.py`; detectors run only in the test processes deflake starts, so your regular test runs are not affected:
156
+
157
+ ```python
158
+ from deflake import leaks
159
+
160
+
161
+ class FeatureFlagLeaks:
162
+ def snapshot(self):
163
+ return sorted(FeatureFlags.enabled())
164
+
165
+ def diff(self, before, after):
166
+ return [f"feature flag {flag} left on" for flag in sorted(set(after) - set(before))]
167
+
168
+
169
+ leaks.detectors.append(FeatureFlagLeaks())
170
+ ```
171
+
172
+ The Module globals detector compares every global of the project before and after each test, and the Rows detector counts the rows of every table; if one of them is too slow for your project, leave it out, in `conftest.py` or in the test settings of a Django project:
173
+
174
+ ```python
175
+ from deflake import leaks
176
+ from deflake.django.leaks import Rows
177
+ from deflake.leaks.modules import Modules
178
+
179
+ leaks.excluded += [Modules, Rows]
180
+ ```
181
+
182
+ ### Proving a leak is the cause
183
+
184
+ A leak next to a failure is a suspect, not a proof. For each culprit deflake takes every piece of state it leaked and runs an experiment in fresh processes with the same seed and order: two runs as they are, two that put the state back after the culprit, and two that put it back and set it again. A leak is confirmed as the cause only when the victim fails the same way in the runs as they are and when set again, and passes both times when the state is put back. Skips, timeouts, interrupted processes, errors outside of tests and other failures never confirm a cause.
185
+
186
+ ```
187
+ test_victim: failed 5 of 10 runs (50%)
188
+ verdict: fails only after other tests (alone: failed 0 of 10 runs (0%))
189
+ culprit: test_pollutes
190
+ environment cause: FEATURE_FLAG from test_order.py::test_pollutes: confirmed (seed 40331)
191
+ baseline: same_failure, same_failure; repair: passed, passed; reapply: same_failure, same_failure
192
+ ```
193
+
194
+ This works for:
195
+
196
+ - environment variables and plain values of the project's modules (`None`, bools, numbers, strings and bytes);
197
+ - `mock.patch` left started, set again with the culprit's own mock, and the keys a `mock.patch.dict` patched;
198
+ - time freezegun left frozen;
199
+ - the working directory, `sys.path`, the recursion limit, the socket timeout, the `decimal` context, signal handlers, warning filters and logging;
200
+ - under Django, the active language, time zone and URLconf, settings (an `override_settings` left enabled is tried setting by setting), local memory caches and signal receivers.
201
+
202
+ Other leaks of a culprit, such as a list changed in place, rows of a database, a thread or time-machine, are shown under the victim as `not checked as the cause`: they are next to the failure, but not proven to cause it. Only what the report shows as the culprit's own leaks is tried, up to twenty per culprit; a change of a wider fixture set up with it is not the culprit's. A confirmed cause holds for the order tested; putting a value back cannot undo copies of it already made into other objects or services.
203
+
204
+ The JSON report adds `causes` to each test, with `culprit`, `key`, `seed`, `baseline`, `repair`, `reapply`, `detector` (`environment`, `module`, `patch`, `clock`, `process` or `django`) and `confirmed`, and `culprit_leaks` with every leak of the culprits and whether it was `checked` and `confirmed`. Values of variables are never included.
205
+
206
+ ### JSON report
207
+
208
+ `--format json` prints one array for the whole run, with one object per test that ran or leaked (a test that skipped itself in every run and leaked nothing is left out, as in the text report) with `file`, `name`, `id` (the pytest node id), `verdict` (`stable`, `failed`, `fails_on_its_own`, `order_dependent`, `random`, `not_run` for a selected test that never started, or `not_checked` for a file of a folder that could not run, with its error under `errors`; `failed` means a test could not run alone, or that every run looking for its culprits broke before it), `runs` and `isolated_runs` counts (`isolated_runs` is `null` when the test never failed or could not run alone) (`total`, `failures`, `failure_rate` and `skipped`: `total` leaves out the runs where the test skipped itself), `errors` with their seeds, `leaks` (with `runs`, counting the runs where the test skipped itself too) and `culprits` (`ids`, `names`, `command`, `alone` when the test fails with no test before it, `group_wide`, `others` and `other_names` for further sets of tests that break it just as well on their own, `reproduced` when the failure came back with the same order but not with fewer tests, and `broken` when every run broke before the test) and `marked_flaky`. The format may change until 1.0.
209
+
210
+ ## Tested on real projects
211
+
212
+ [Recorded cases](https://github.com/SlyDeath/python-deflake/blob/main/docs/real_world_cases.json) from SecureNative Python, Runway model-sdk and humanize, at pinned revisions: deflake finds the known flaky test and its cause before the published fix, and the test passes after it.
213
+
214
+ ## Limitations
215
+
216
+ - deflake runs one file at a time, even when given a folder. A test broken by a test in another file shows up as failing only when the two files run together, which deflake does not do yet.
217
+ - A folder is searched by file name, not by your `python_files` setting.
218
+ - Leak detection sees what changed in the test process. It cannot see changes made by other processes, such as a browser or a server started by a test.
219
+
220
+ ## Development
221
+
222
+ ```bash
223
+ uv sync
224
+ uv run ruff check && uv run ruff format --check && uv run mypy && uv run pytest
225
+ ```
226
+
227
+ ## License
228
+
229
+ MIT, see [LICENSE.txt](https://github.com/SlyDeath/python-deflake/blob/main/LICENSE.txt).