pytest-flakefighters 0.2.3__tar.gz → 0.3.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 (69) hide show
  1. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/.github/workflows/ci-tests-drafts.yaml +1 -3
  2. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/.github/workflows/ci-tests.yaml +1 -3
  3. {pytest_flakefighters-0.2.3/src/pytest_flakefighters.egg-info → pytest_flakefighters-0.3.0}/PKG-INFO +1 -1
  4. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/source/_static/css/custom.css +4 -0
  5. pytest_flakefighters-0.3.0/docs/source/_static/images/html_summary.png +0 -0
  6. pytest_flakefighters-0.3.0/docs/source/ci_cd.rst +38 -0
  7. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/source/configuration.rst +10 -8
  8. pytest_flakefighters-0.3.0/docs/source/custom_flakefighters.rst +88 -0
  9. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/source/index.rst +7 -3
  10. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/source/installation.rst +12 -2
  11. pytest_flakefighters-0.3.0/docs/source/reports.rst +98 -0
  12. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/_version.py +3 -3
  13. pytest_flakefighters-0.3.0/src/pytest_flakefighters/config.py +90 -0
  14. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/database_management.py +68 -2
  15. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/flakefighters/deflaker.py +6 -7
  16. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/flakefighters/traceback_matching.py +8 -14
  17. pytest_flakefighters-0.3.0/src/pytest_flakefighters/main.py +147 -0
  18. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/plugin.py +191 -2
  19. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0/src/pytest_flakefighters.egg-info}/PKG-INFO +1 -1
  20. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters.egg-info/SOURCES.txt +5 -0
  21. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/flakefighters/test_deflaker.py +2 -4
  22. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/flakefighters/test_traceback_matching.py +2 -2
  23. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/test_database_management.py +77 -0
  24. pytest_flakefighters-0.3.0/tests/test_end_2_end.py +277 -0
  25. pytest_flakefighters-0.2.3/src/pytest_flakefighters/main.py +0 -167
  26. pytest_flakefighters-0.2.3/tests/test_end_2_end.py +0 -96
  27. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/.github/workflows/ci-mega-linter.yml +0 -0
  28. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/.github/workflows/publish-pypi.yaml +0 -0
  29. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/.gitignore +0 -0
  30. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/.mega-linter.yaml +0 -0
  31. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/.pre-commit-config.yaml +0 -0
  32. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/.readthedocs.yaml +0 -0
  33. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/LICENSE +0 -0
  34. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/README.md +0 -0
  35. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/codecov.yml +0 -0
  36. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/Makefile +0 -0
  37. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/make.bat +0 -0
  38. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/source/_static/images/favicon.png +0 -0
  39. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/source/_static/images/logo.png +0 -0
  40. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/source/acknowlegements.rst +0 -0
  41. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/source/conf.py +0 -0
  42. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/source/dev/actions_and_webhooks.rst +0 -0
  43. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/source/dev/documentation.rst +0 -0
  44. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/source/dev/version_release.rst +0 -0
  45. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/source/glossary.rst +0 -0
  46. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/docs/source/requirements.txt +0 -0
  47. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/pyproject.toml +0 -0
  48. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/setup.cfg +0 -0
  49. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/__init__.py +0 -0
  50. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/flakefighters/__init__.py +0 -0
  51. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/flakefighters/abstract_flakefighter.py +0 -0
  52. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/flakefighters/coverage_independence.py +0 -0
  53. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/function_coverage.py +0 -0
  54. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/rerun_strategies.py +0 -0
  55. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters.egg-info/dependency_links.txt +0 -0
  56. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters.egg-info/entry_points.txt +0 -0
  57. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters.egg-info/requires.txt +0 -0
  58. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters.egg-info/top_level.txt +0 -0
  59. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/__init__.py +0 -0
  60. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/conftest.py +0 -0
  61. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/flakefighters/test_coverage_independence.py +0 -0
  62. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/resources/deflaker_broken.py +0 -0
  63. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/resources/deflaker_example.py +0 -0
  64. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/resources/flaky_reruns.py +0 -0
  65. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/resources/pass_fail_flaky.py +0 -0
  66. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/resources/test.txt +0 -0
  67. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/resources/triangle.py +0 -0
  68. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/test_function_coverage.py +0 -0
  69. {pytest_flakefighters-0.2.3 → pytest_flakefighters-0.3.0}/tests/test_rerun_strategies.py +0 -0
@@ -24,9 +24,7 @@ jobs:
24
24
  run: |
25
25
  python --version
26
26
  python -m pip install --upgrade pip
27
- pip install -e .
28
- pip install -e .[test]
29
- pip install pytest pytest-cov
27
+ pip install -e .[dev]
30
28
  - name: Test with pytest
31
29
  run: |
32
30
  pytest -p no:flakefighters --cov=src --cov-report=xml
@@ -29,9 +29,7 @@ jobs:
29
29
  run: |
30
30
  python --version
31
31
  python -m pip install --upgrade pip
32
- pip install -e .
33
- pip install -e .[test]
34
- pip install pytest pytest-cov
32
+ pip install -e .[dev]
35
33
  - name: Test with pytest
36
34
  run: |
37
35
  pytest -p no:flakefighters --cov=src --cov-report=xml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-flakefighters
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Pytest plugin implementing flaky test failure detection and classification.
5
5
  Author: TestFLARE Team
6
6
  Project-URL: Documentation, https://pytest-flakefighters.readthedocs.io
@@ -11,6 +11,10 @@ The CSS below will only work if 'logo_only':True in conf.py, but will require ad
11
11
  width:auto;
12
12
  }
13
13
 
14
+ .wy-side-nav-search > a:hover {
15
+ background: transparent !important;
16
+ }
17
+
14
18
  .wy-nav-content {
15
19
  background-color: white;
16
20
  }
@@ -0,0 +1,38 @@
1
+ CI/CD Integration
2
+ =================
3
+
4
+ The `TestFLARE <https://test-flare.github.io/>`__ project is motivated by the fact that flaky tests can be particularly problematic for CI/CD pipelines.
5
+ The :code:`flakefighers` extension is therefore developed to integrate seamlessly with your existing pipelines and testing workflows.
6
+ If you already have a workflow that uses :code:`pytest`, all you need to do is include :code:`pytest-flakefighers` as a dependency.
7
+ The extension will then automatically be run as part of :code:`pytest`.
8
+
9
+ .. _remote-databases:
10
+
11
+ Remote Databases
12
+ ----------------
13
+
14
+ When you run :code:`pytest`, the :code:`flakefighers` extension automatically saves the details of each run, including the classification result from each active flakefighter for each test.
15
+ This historical data can then be used on subsequent runs to inform flaky classification.
16
+ By default, this data is stored locally in an SQlite database called :code:`flakefighters.db`.
17
+ However, this is configurable using the :code:`--database-url` parameter, so you are free to save your data wherever you like.
18
+
19
+ If you are using the extension as part of your CI/CD pipeline and want to preserve the data between runs, you will need to use external service such as `Supabase <https://supabase.com/>`__ to host your database.
20
+ Fortunately, this is very straightforward as these services make make setup and connection very easy.
21
+ All you need to do is set up the database using your service of choice and pass the database URL into the :code:`--database-url` parameter.
22
+ You don't even need to create the tables yourself, as this will be done by the extension automatically on the first run!
23
+ Of course, for security reasons, we suggest making your URL a `secret <https://docs.github.com/en/actions/reference/security/secrets>`__ rather than including the raw string in your github workflow.
24
+
25
+ .. tip::
26
+ If you don't need to preserve the data between CI/CD runs, but do want to inspect it, you can include it as an `artifact <https://docs.github.com/en/actions/concepts/workflows-and-actions/workflow-artifacts>`__.
27
+
28
+ Hybrid Databases
29
+ ----------------
30
+
31
+ There may be situations where you sometimes want to store data locally and sometimes want to store it remotely.
32
+ For example, you may want your CI/CD runs to go into the remote database hosted externally, but your local runs to use a local database so that you don't clog up the remote database during debugging.
33
+ Depending on your workflow, you may even want to keep a local clone of your database and occasionally synchronise this with the remote database.
34
+ Fortunately, database providers such as Supabase make this a `relatively straightforward process <https://supabase.com/docs/guides/local-development/overview>`__.
35
+
36
+ .. warning::
37
+ As with any such setting, synchronisation may result in conflicts or inconsistent behaviour if entries have been made into the remote database after you have cloned your local copy and before you have resynchronised.
38
+ In particular, the results of flakefighters that examine historical runs can be confusing if historical runs that were performed before a particular :code:`pytest` run were not added until afterwards.
@@ -4,15 +4,17 @@ Configuration
4
4
  The flakefighters plugin implements several cutting edge flaky test detection tools from the research community.
5
5
  Each one is individually configurable and can be run individually or with other flakefighters.
6
6
  You can control which flakefighters to run and provide additional configuration options from your :code:`pyproject.toml` file by including sections of the following form for each flakefighter you want to run.
7
- Here, :code:`<FlakeFighterName>` is the class name of the flakefighter you wish to configure, assuming the class is in the :code:`pytest-flakefighters/flakefighters` directory.
8
- ::
9
- [tool.pytest.ini_options.pytest_flakefighters.<FlakeFighterName>]
10
- run_live=[true/false]
11
- option1=value1
12
- option2=value2
13
- ...
7
+ Here, :code:`<FlakeFighterClass>` is the class of the flakefighter you wish to configure as if you were going to import it into a source code file.
14
8
 
15
- Every flakefighter will have a :code:`run_live` option, which can be set to :code:`true` to classify each test execution as flaky immediately after it is run, or :code:`false` to clasify all tests at once at the end, although individual flakefighters may only support one particular option.
9
+ .. code-block:: ini
10
+
11
+ [tool.pytest.ini_options.pytest_flakefighters.<FlakeFighterClass>]
12
+ run_live=[true/false]
13
+ option1=value1
14
+ option2=value2
15
+ ...
16
+
17
+ Every flakefighter has a :code:`run_live` option, which can be set to :code:`true` to classify each test execution as flaky immediately after it is run, or :code:`false` to clasify all tests at once at the end, although individual flakefighters may only support one of these.
16
18
  Individual flakefighters have their own configurable options.
17
19
  These are detailed below.
18
20
 
@@ -0,0 +1,88 @@
1
+ Custom Flakefighters
2
+ ====================
3
+
4
+ In addition to the flakefighters that are distributed with the extension, you can also define your own custom flakefighter classes.
5
+ To do this, you simply need to extensd the :code:`Flakefighter` class and implement the abstract methods.
6
+ For example, see below
7
+
8
+ .. code-block:: python
9
+
10
+ from pytest_flakefighters.flakefighters.abstract_flakefighter import(
11
+ FlakeFighter,
12
+ FlakefighterResult
13
+ )
14
+ from pytest_flakefighters.database_management import Run, TestExecution
15
+
16
+
17
+ class CustomFlakefighter(FlakeFighter):
18
+
19
+ def __init__(self, run_live: bool, custom_arg: str):
20
+ super().__init__(run_live)
21
+ self.custom_arg = custom_arg
22
+
23
+ @classmethod
24
+ def from_config(cls, config: dict):
25
+ """
26
+ Factory method to create a new instance from a pytest configuration.
27
+ """
28
+ return CustomFlakefighter(
29
+ run_live=config.get("run_live", True),
30
+ custom_arg=config.get("custom_arg", ""),
31
+ )
32
+
33
+ def params(self):
34
+ """
35
+ Convert the key parameters into a dictionary so that the object can be replicated.
36
+ :return A dictionary of the parameters used to create the object.
37
+ """
38
+ return {"custom_arg": self.custom_arg}
39
+
40
+ def flaky_test_live(self, execution: TestExecution):
41
+ """
42
+ Detect whether a given test execution is flaky and append the result to its
43
+ `flakefighter_results` attribute.
44
+ :param execution: The test execution to classify.
45
+ """
46
+ execution.flakefighter_results.append(
47
+ FlakefighterResult(
48
+ name=self.__class__.__name__,
49
+ # Implement logic to determine this result at *execution level*
50
+ flaky=True,
51
+ )
52
+ )
53
+
54
+ def flaky_tests_post(self, run: Run):
55
+ """
56
+ Go through each test in the test suite and append the result to its
57
+ `flakefighter_results` attribute.
58
+ :param run: Run object representing the pytest run, with tests
59
+ accessible through run.tests.
60
+ """
61
+ for test in run.tests:
62
+ test.flakefighter_results.append(
63
+ FlakefighterResult(
64
+ name=self.__class__.__name__,
65
+ # Implement logic to determine this result at *test level*
66
+ # In many cases, it will be sufficient to test whether
67
+ # any executions are flaky
68
+ flaky=True
69
+ )
70
+ )
71
+
72
+ Once you have implemented your flakefighter class, you will need to register it as an extra entry point in your :code:`pyproject.toml` file so that the plugin can find it.
73
+ For example, if you had defined your :code:`CustomFlakefighter` class in a module called :code:`custom_flakefighter`, you would register it as follows.
74
+
75
+ .. code-block:: ini
76
+
77
+ [project.entry-points."pytest_flakefighters"]
78
+ CustomFlakefighter = "custom_flakefighter:CustomFlakefighter"
79
+
80
+ Of course, for this to work, your module needs to be discoverable on your python path.
81
+ That is, you should be able to execute :code:`from custom_flakefighter import CustomFlakefighter` successfully from within the same directory as where you are running :code:`pytest`.
82
+ You can then configure it just like any other flakefighter.
83
+
84
+ .. code-block:: ini
85
+
86
+ [tool.pytest.ini_options.pytest_flakefighters.flakefighters.custom_flakefighter.CustomFlakefighter]
87
+ run_live=true
88
+ custom_arg=0.1
@@ -21,10 +21,11 @@ Motivation
21
21
  :term:`Flaky tests` intermittently pass and fail without changes to test or project source code, often without an obvious cause.
22
22
  When flaky tests proliferate, developers may loose faith in their test suites, potentially exposing end-users to the consequences of software failures.
23
23
 
24
- The Extension
25
- -------------
24
+ Pytest Extension
25
+ ----------------
26
26
 
27
- Flakefighters is a pytest extension that provides a "Swiss army knife" of techniques to detect flaky tests.
27
+ Flakefighters is a pytest extension developed as part of the `TestFLARE <https://test-flare.github.io/>`__ project.
28
+ The extension provides a "Swiss army knife" of techniques (called flakefighters) to detect flaky tests.
28
29
  The extension incorporates several cutting edge flaky test detection techniques from research to automatically classify test failures as either genuine: indicating either a fault in the code or a mis-specified test case, or flaky: indicating a test with a nondeterministic outcome.
29
30
  Flaky tests are then reported separately in the test report, and can be optionally suppressed so they don't block CI/CD pipelines.
30
31
 
@@ -40,6 +41,9 @@ Flaky tests are then reported separately in the test report, and can be optional
40
41
 
41
42
  installation
42
43
  configuration
44
+ custom_flakefighters
45
+ reports
46
+ ci_cd
43
47
 
44
48
 
45
49
  .. toctree::
@@ -5,14 +5,14 @@ Installation
5
5
  -----------------
6
6
  * We currently support Python versions 3.10, 3.11, 3.12, and 3.13.
7
7
 
8
- * The Flakefighters plugin can be installed through the `Python Package Index (PyPI)`_ (recommended), or directly from source (recommended for contributors).
8
+ * The Flakefighters plugin can be installed through the `Python Package Index (PyPI)`_ (recommended for normal use), or directly from source (recommended for contributors).
9
9
 
10
10
  .. _Python Package Index (PyPI): https://pypi.org/project/pytest-flakefighters
11
11
 
12
12
  Method 1: Installing via pip
13
13
  ..............................
14
14
 
15
- To install the Causal Testing Framework using :code:`pip` for the latest stable version::
15
+ To install the extension using :code:`pip` for the latest stable version::
16
16
 
17
17
  pip install pytest-flakefighters
18
18
 
@@ -23,6 +23,16 @@ If you also want to install the framework with (optional) development packages/t
23
23
  pip install pytest-flakefighters[dev]
24
24
 
25
25
 
26
+ .. note::
27
+ If you plan to use the extension using a PostgreSQL database (see :ref:`remote-databases`), then you will need to install PostgreSQL on your system, have :code:`pg_config` on your path, and then install using the :code:`pg` option.
28
+
29
+ .. code-block:: python
30
+
31
+ pip install pytest-flakefighters[pg]
32
+
33
+ If you wish to use `other dialects <https://docs.sqlalchemy.org/en/20/dialects/>`_, you may need to install additional packages to support this.
34
+
35
+
26
36
  Method 2: Installing via Source (For Developers/Contributors)
27
37
  ...............................................................
28
38
 
@@ -0,0 +1,98 @@
1
+ Reporting and Logging
2
+ =====================
3
+
4
+ The extension supports a range of reporting formats.
5
+ By default, flaky tests will be flagged in the short test summary info in the console output as shown below.
6
+ Genuine failures will show as :code:`FAILED` as normal.
7
+ Failures which have been classified as flaky by at least one active flakefighter will show as :code:`FLAKY`.
8
+
9
+ .. code-block:: ini
10
+
11
+ ================================== short test summary info ==================================
12
+ FLAKY test_flaky_reruns.py::TestFlakyRuns::test_create_or_delete - assert not True
13
+ FAILED test_flaky_reruns.py::TestFlakyRuns::test_fail - assert False
14
+ ================================ 2 failed, 1 passed in 1.13s ================================
15
+
16
+ Writing to JSON
17
+ ---------------
18
+
19
+ The extension is designed to work with `pytest-json-report <https://pypi.org/project/pytest-json-report>`_ to create test reports as JSON.
20
+ To do this, you will need to :code:`pip install pytest-json-report`, after which you can run :code:`pytest` with the :code:`--json-report` option to save the test report to :code:`.report.json` by default.
21
+ The target path to save JSON report can be changed using the :code:`--json-report-file=PATH` option.
22
+ Each test :code:`call` will be assigned a :code:`metadata` field that records the execution-level flakefighter results for each (repeated) execution.
23
+ Each test will be assigned a :code:`metadata` field to record the test-level results.
24
+
25
+ In the example below, :code:`pytest` was called with the :code:`DeFlaker`, :code:`TracebackMatching` (at execution level), and :code:`CoverageIndependence` (at test level) flakefighters.
26
+ On the first execution of :code:`TestFlaky::test_flaky_example`, :code:`DeFlaker` classified the test failure as flaky, but :code:`TracebackMatching` classified it as genuine.
27
+ On the rerun, the outcome of :code:`DeFlaker` did not change, but :code:`TracebackMatching` classified it as flaky.
28
+ Finally, :code:`CoverageIndependence` classified the overall test as flaky.
29
+
30
+ .. code-block:: ini
31
+
32
+ {
33
+ "nodeid": "test_flaky.py::TestFlaky::test_flaky_example",
34
+ "lineno": 5,
35
+ "outcome": "failed",
36
+ "setup": {"duration": 0.00024656900131958537, "outcome": "passed"},
37
+ "call": {
38
+ "duration": 0.000256097000601585,
39
+ "outcome": "failed",
40
+ "metadata": {
41
+ "executions": [{
42
+ "start_time": "2026-01-19 11:19:06.214221",
43
+ "end_time": "2026-01-19 11:19:06.214703",
44
+ "outcome": failed,
45
+ "flakefighter_results": {"DeFlaker": "flaky", "TracebackMatching": "genuine"}
46
+ }, {
47
+ "start_time": "2026-01-19 11:19:06.264956",
48
+ "end_time": "2026-01-19 11:19:06.265155",
49
+ "outcome": failed,
50
+ "flakefighter_results": {"DeFlaker": "flaky", "TracebackMatching": "flaky"}
51
+ }
52
+ ]
53
+ }
54
+ },
55
+ "teardown": {"duration": 6.307100011326838e-05, "outcome": "passed"},
56
+ "metadata": {
57
+ "flakefighter_results": {"CoverageIndependence": "flaky"}
58
+ }
59
+ }
60
+
61
+ .. note::
62
+ The :code:`pytest-json` extension is not officially supported, but the execution-level flakefighter results will be printed in a similar manner if you use this extension instead of the newer :code:`pytest-json-report`.
63
+ Test-level flakefighter results will not be saved.
64
+
65
+ Writing to XML
66
+ --------------
67
+
68
+ The extension is designed to export results to JUnitXML when you run :code:`pytest` with the :code:`--junitxml` option.
69
+ The results will be saved in a :code:`<flakefighterresults>` element as a child of each :code:`<testcase>`.
70
+ Each execution-level result will be saved in an :code:`<execution>` element.
71
+ The test-level results will be saved in a :code:`<test>` element.
72
+
73
+ .. code-block:: ini
74
+
75
+ <testcase classname="test_flaky_reruns.TestFlakyRuns" name="test_pass" time="0.001">
76
+ <flakefighterresults>
77
+ <execution outcome="failed" starttime="2026-01-19T11:44:58.123723" endtime="2026-01-19T11:44:58.124223">
78
+ <DeFlaker>flaky</DeFlaker>
79
+ <TracebackMatching>genuine</TracebackMatching>
80
+ </execution>
81
+ <execution outcome="failed" starttime="2026-01-19T11:44:58.173746" endtime="2026-01-19T11:44:58.173929">
82
+ <DeFlaker>flaky</DeFlaker>
83
+ <TracebackMatching>flaky</TracebackMatching>
84
+ </execution>
85
+ <test>
86
+ <CoverageIndependence>flaky</CoverageIndependence>
87
+ </test>
88
+ </flakefighterresults>
89
+ </testcase>
90
+
91
+ Writing to HTML
92
+ ---------------
93
+
94
+ The extension is designed to work with :code:`pytest-html` to export results to HTML when you run :code:`pytest` with the :code:`--html` option.
95
+ Test-level flakefighter results are shown just underneath the summary.
96
+ Execution-level flakefighter results are shown with the details of each individual test.
97
+
98
+ .. image:: _static/images/html_summary.png
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.2.3'
32
- __version_tuple__ = version_tuple = (0, 2, 3)
31
+ __version__ = version = '0.3.0'
32
+ __version_tuple__ = version_tuple = (0, 3, 0)
33
33
 
34
- __commit_id__ = commit_id = 'g00e29e6af'
34
+ __commit_id__ = commit_id = 'gfae51daf6'
@@ -0,0 +1,90 @@
1
+ """
2
+ This module defines all the configuration options in a dictionary.
3
+ Keys should be of the form `(--long-name, -L)` or just `(--long-name,)`.
4
+ Options can then be specified on the commandline as `--long-name` or in a configuration file as `long_name`.
5
+ Options specified on the commandline will override those specified in configuration files.
6
+ """
7
+
8
+ import os
9
+
10
+ from pytest_flakefighters.rerun_strategies import All, FlakyFailure, PreviouslyFlaky
11
+
12
+ rerun_strategies = {"ALL": All, "FLAKY_FAILURE": FlakyFailure, "PREVIOUSLY_FLAKY": PreviouslyFlaky}
13
+
14
+
15
+ options = {
16
+ ("--root",): {
17
+ "dest": "root",
18
+ "action": "store",
19
+ "default": os.getcwd(),
20
+ "help": "The root directory of the project. Defaults to the current working directory.",
21
+ },
22
+ ("--suppress-flaky-failures-exit-code",): {
23
+ "dest": "suppress_flaky",
24
+ "action": "store_true",
25
+ "default": False,
26
+ "help": "Return OK exit code if the only failures are flaky failures.",
27
+ },
28
+ ("--no-save",): {
29
+ "action": "store_true",
30
+ "default": False,
31
+ "help": "Do not save this run to the database of previous flakefighters runs.",
32
+ },
33
+ ("--function-coverage",): {
34
+ "action": "store_true",
35
+ "default": False,
36
+ "help": "Use function-level coverage instead of line coverage.",
37
+ },
38
+ ("--load-max-runs", "-M"): {
39
+ "action": "store",
40
+ "default": None,
41
+ "help": "The maximum number of previous runs to consider.",
42
+ },
43
+ ("--database-url", "-D"): {
44
+ "action": "store",
45
+ "default": "sqlite:///flakefighters.db",
46
+ "help": "The database URL. Defaults to 'flakefighters.db' in current working directory.",
47
+ },
48
+ ("--store-max-runs",): {
49
+ "action": "store",
50
+ "default": None,
51
+ "type": int,
52
+ "help": "The maximum number of previous flakefighters runs to store. Default is to store all.",
53
+ },
54
+ ("--max-reruns",): {
55
+ "action": "store",
56
+ "default": 0,
57
+ "type": int,
58
+ "help": "The maximum number of times to rerun tests. "
59
+ "By default, only failing tests marked as flaky will be rerun. "
60
+ "This can be changed with the --rerun-strategy parameter.",
61
+ },
62
+ ("--rerun-strategy",): {
63
+ "action": "store",
64
+ "type": str,
65
+ "choices": list(rerun_strategies),
66
+ "default": "FLAKY_FAILURE",
67
+ "help": "The strategy used to determine which tests to rerun. Supported options are:\n "
68
+ + "\n ".join(f"{name} - {strat.help()}" for name, strat in rerun_strategies.items()),
69
+ },
70
+ ("--time-immemorial",): {
71
+ "action": "store",
72
+ "default": None,
73
+ "help": "How long to store flakefighters runs for, specified as `days:hours:minutes`. "
74
+ "E.g. to store tests for one week, use 7:0:0.",
75
+ },
76
+ ("--display-outcomes", "-O"): {
77
+ "action": "store",
78
+ "type": int,
79
+ "nargs": "?", # Allows 0 or 1 arguments
80
+ "const": 0, # Value used if -O is present but no value is provided
81
+ "default": 0, # Value used if -O is not present at all
82
+ "help": "Display historical test outcomes of the specified number of previous runs."
83
+ "If no value is specified, then display only the current verdict.",
84
+ },
85
+ ("--display-verdicts",): {
86
+ "action": "store_true",
87
+ "default": False,
88
+ "help": "Display the flaky classification verdicts alongside test outcomes.",
89
+ },
90
+ }
@@ -34,10 +34,12 @@ from sqlalchemy.orm import (
34
34
  class Base(DeclarativeBase):
35
35
  """
36
36
  Declarative base class for data objects.
37
+
38
+ :ivar id: Unique autoincrementing ID for the object.
37
39
  """
38
40
 
39
41
  id: Mapped[int] = Column(Integer, primary_key=True) # pylint: disable=C0103
40
- # @pytest, these are not the tests you're looking for...
42
+ # Explicitly flag that we don't want pytest to collect our Test, TestExecution, etc. classes.
41
43
  __test__ = False # pylint: disable=C0103
42
44
 
43
45
  @declared_attr
@@ -49,8 +51,16 @@ class Base(DeclarativeBase):
49
51
  class Run(Base):
50
52
  """
51
53
  Class to store attributes of a flakefighters run.
54
+ :ivar start_time: The time the test run was begun.
55
+ :ivar created_at: The time the entry was added to the database.
56
+ This is not necessarily equivalent to start_time if the test suite took a long time to run or
57
+ if the entry was migrated from a separate database.
58
+ :ivar root: The root directory of the project.
59
+ :ivar tests: The test suite.
60
+ :ivar active_flakefighters: The flakefighters that are active on the run.
52
61
  """
53
62
 
63
+ start_time = Column(DateTime)
54
64
  created_at = Column(DateTime, default=func.now())
55
65
  root: Mapped[str] = Column(String)
56
66
  tests = relationship("Test", backref="run", lazy="subquery", cascade="all, delete", passive_deletes=True)
@@ -63,6 +73,10 @@ class Run(Base):
63
73
  class ActiveFlakeFighter(Base):
64
74
  """
65
75
  Store relevant information about the active flakefighters.
76
+
77
+ :ivar run_id: Foreign key of the related run.
78
+ :ivar name: Class name of the flakefighter.
79
+ :ivar params: The parameterss of the flakefighter.
66
80
  """
67
81
 
68
82
  run_id: Mapped[int] = Column(Integer, ForeignKey("run.id"), nullable=False)
@@ -74,6 +88,17 @@ class ActiveFlakeFighter(Base):
74
88
  class Test(Base):
75
89
  """
76
90
  Class to store attributes of a test case.
91
+
92
+ :ivar run_id: Foreign key of the related run.
93
+ :ivar fspath: File system path of the file containing the test definition.
94
+ :ivar line_no: Line number of the test definition.
95
+ :ivar name: Name of the test case.
96
+ :ivar skipped: Boolean true if the test was skipped, else false.
97
+ :ivar executions: List of execution attempts.
98
+ :ivar flakefighter_results: List of test-level flakefighter results.
99
+
100
+ .. note::
101
+ Execution-level flakefighter results will be stored inside the individual TestExecution objects
77
102
  """
78
103
 
79
104
  run_id: Mapped[int] = Column(Integer, ForeignKey("run.id"), nullable=False)
@@ -104,6 +129,16 @@ class Test(Base):
104
129
  class TestExecution(Base): # pylint: disable=R0902
105
130
  """
106
131
  Class to store attributes of a test outcome.
132
+
133
+ :ivar test_id: Foreign key of the related test.
134
+ :ivar outcome: Outcome of the test. One of "passed", "failed", or "skipped".
135
+ :ivar stdout: The captured stdout string.
136
+ :ivar stedrr: The captured stderr string.
137
+ :ivar start_time: The start time of the test.
138
+ :ivar end_time: The end time of the test.
139
+ :ivar coverage: The line coverage of the test.
140
+ :ivar flakefighter_results: The execution-level flakefighter results.
141
+ :ivar exception: The exception associated with the test if one was thrown.
107
142
  """
108
143
 
109
144
  __tablename__ = "test_execution"
@@ -140,6 +175,10 @@ class TestExecution(Base): # pylint: disable=R0902
140
175
  class TestException(Base): # pylint: disable=R0902
141
176
  """
142
177
  Class to store information about the exceptions that cause tests to fail.
178
+
179
+ :ivar execution_id: Foreign key of the related execution.
180
+ :ivar name: Name of the exception.
181
+ :traceback: The full stack of traceback entries.
143
182
  """
144
183
 
145
184
  __tablename__ = "test_exception"
@@ -155,6 +194,13 @@ class TestException(Base): # pylint: disable=R0902
155
194
  class TracebackEntry(Base): # pylint: disable=R0902
156
195
  """
157
196
  Class to store attributes of entries in the stack trace.
197
+
198
+ :ivar exception_id: Foreign key of the related exception.
199
+ :ivar path: Filepath of the source file.
200
+ :ivar lineno: Line number of the executed statement.
201
+ :ivar colno: Column number of the executed statement.
202
+ :ivar statement: The executed statement.
203
+ :ivar source: The surrounding source code.
158
204
  """
159
205
 
160
206
  exception_id: Mapped[int] = Column(Integer, ForeignKey("test_exception.id"), nullable=False)
@@ -169,6 +215,11 @@ class TracebackEntry(Base): # pylint: disable=R0902
169
215
  class FlakefighterResult(Base): # pylint: disable=R0902
170
216
  """
171
217
  Class to store flakefighter results.
218
+
219
+ :ivar test_execution_id: Foreign key of the related test execution. Should not be set if test_id is present.
220
+ :ivar test_id: Foreign key of the related test. Should not be set if test_execution_id is present.
221
+ :ivar name: Name of the flakefighter.
222
+ :ivar flaky: Boolean true if the test (execution) was classified as flaky.
172
223
  """
173
224
 
174
225
  __tablename__ = "flakefighter_result"
@@ -182,10 +233,25 @@ class FlakefighterResult(Base): # pylint: disable=R0902
182
233
  CheckConstraint("NOT (test_execution_id IS NULL AND test_id IS NULL)", name="check_test_id_not_null"),
183
234
  )
184
235
 
236
+ @property
237
+ def classification(self):
238
+ """
239
+ Return the classification as a string.
240
+ "flaky" if the test was classified as flaky, else "genuine".
241
+ """
242
+ return "flaky" if self.flaky else "genuine"
243
+
185
244
 
186
245
  class Database:
187
246
  """
188
247
  Class to handle database setup and interaction.
248
+
249
+ :ivar engine: The database engine.
250
+ :ivar store_max_runs: The maximum number of previous runs that should be stored. If the database exceeds this size,
251
+ older runs will be pruned to make space for newer ones.
252
+ :ivar time_immemorial: Time before which runs should not be considered. Runs before this date will be pruned when
253
+ saving new runs.
254
+ :ivar previous_runs: List of previous flakefighter runs with most recent first.
189
255
  """
190
256
 
191
257
  def __init__(
@@ -230,4 +296,4 @@ class Database:
230
296
  :param limit: The maximum number of runs to return (these will be most recent runs).
231
297
  """
232
298
  with Session(self.engine) as session:
233
- return session.scalars(select(Run).order_by(desc(Run.id)).limit(limit)).all()
299
+ return session.scalars(select(Run).order_by(desc(Run.start_time)).limit(limit)).all()
@@ -68,7 +68,10 @@ class DeFlaker(FlakeFighter):
68
68
  self.method_declarations = {}
69
69
  for file, lines in self.lines_changed.items():
70
70
  with open(file) as f:
71
- tree = ast.parse(f.read())
71
+ try:
72
+ tree = ast.parse(f.read())
73
+ except SyntaxError:
74
+ continue
72
75
 
73
76
  self.method_declarations[file] = [
74
77
  node.lineno
@@ -133,9 +136,5 @@ class DeFlaker(FlakeFighter):
133
136
  :param run: Run object representing the pytest run, with tests accessible through run.tests.
134
137
  """
135
138
  for test in run.tests:
136
- test.flakefighter_results.append(
137
- FlakefighterResult(
138
- name=self.__class__.__name__,
139
- flaky=any(self._flaky_execution(execution) for execution in test.executions),
140
- )
141
- )
139
+ for execution in test.executions:
140
+ self.flaky_test_live(execution)