pytest-flakefighters 0.2.2__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.
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/.github/workflows/ci-tests-drafts.yaml +1 -3
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/.github/workflows/ci-tests.yaml +1 -3
- {pytest_flakefighters-0.2.2/src/pytest_flakefighters.egg-info → pytest_flakefighters-0.3.0}/PKG-INFO +2 -2
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/README.md +1 -1
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/source/_static/css/custom.css +4 -0
- pytest_flakefighters-0.3.0/docs/source/_static/images/html_summary.png +0 -0
- pytest_flakefighters-0.3.0/docs/source/ci_cd.rst +38 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/source/configuration.rst +10 -8
- pytest_flakefighters-0.3.0/docs/source/custom_flakefighters.rst +88 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/source/index.rst +7 -3
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/source/installation.rst +12 -2
- pytest_flakefighters-0.3.0/docs/source/reports.rst +98 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/_version.py +3 -3
- pytest_flakefighters-0.3.0/src/pytest_flakefighters/config.py +90 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/database_management.py +68 -2
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/flakefighters/deflaker.py +6 -7
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/flakefighters/traceback_matching.py +8 -14
- pytest_flakefighters-0.3.0/src/pytest_flakefighters/main.py +147 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/plugin.py +191 -2
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0/src/pytest_flakefighters.egg-info}/PKG-INFO +2 -2
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters.egg-info/SOURCES.txt +5 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/flakefighters/test_deflaker.py +2 -4
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/flakefighters/test_traceback_matching.py +2 -2
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/test_database_management.py +77 -0
- pytest_flakefighters-0.3.0/tests/test_end_2_end.py +277 -0
- pytest_flakefighters-0.2.2/src/pytest_flakefighters/main.py +0 -167
- pytest_flakefighters-0.2.2/tests/test_end_2_end.py +0 -96
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/.github/workflows/ci-mega-linter.yml +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/.github/workflows/publish-pypi.yaml +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/.gitignore +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/.mega-linter.yaml +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/.pre-commit-config.yaml +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/.readthedocs.yaml +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/LICENSE +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/codecov.yml +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/Makefile +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/make.bat +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/source/_static/images/favicon.png +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/source/_static/images/logo.png +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/source/acknowlegements.rst +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/source/conf.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/source/dev/actions_and_webhooks.rst +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/source/dev/documentation.rst +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/source/dev/version_release.rst +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/source/glossary.rst +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/docs/source/requirements.txt +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/pyproject.toml +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/setup.cfg +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/__init__.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/flakefighters/__init__.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/flakefighters/abstract_flakefighter.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/flakefighters/coverage_independence.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/function_coverage.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters/rerun_strategies.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters.egg-info/dependency_links.txt +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters.egg-info/entry_points.txt +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters.egg-info/requires.txt +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/src/pytest_flakefighters.egg-info/top_level.txt +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/__init__.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/conftest.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/flakefighters/test_coverage_independence.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/resources/deflaker_broken.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/resources/deflaker_example.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/resources/flaky_reruns.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/resources/pass_fail_flaky.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/resources/test.txt +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/resources/triangle.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/test_function_coverage.py +0 -0
- {pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/tests/test_rerun_strategies.py +0 -0
{pytest_flakefighters-0.2.2 → pytest_flakefighters-0.3.0}/.github/workflows/ci-tests-drafts.yaml
RENAMED
|
@@ -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
|
{pytest_flakefighters-0.2.2/src/pytest_flakefighters.egg-info → pytest_flakefighters-0.3.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-flakefighters
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -42,7 +42,7 @@ Dynamic: license-file
|
|
|
42
42
|
|
|
43
43
|
[](https://www.repostatus.org/#active)
|
|
44
44
|
[](https://pypi.org/project/pytest-flakefighters)
|
|
45
|
-
[](https://pypi.org/project/pytest-flakefighters)
|
|
46
46
|

|
|
47
47
|
[](https://codecov.io/gh/test-flare/pytest-flakefighters)
|
|
48
48
|
[](https://causal-testing-framework.readthedocs.io/en/latest/?badge=latest)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
[](https://www.repostatus.org/#active)
|
|
5
5
|
[](https://pypi.org/project/pytest-flakefighters)
|
|
6
|
-
[](https://pypi.org/project/pytest-flakefighters)
|
|
7
7
|

|
|
8
8
|
[](https://codecov.io/gh/test-flare/pytest-flakefighters)
|
|
9
9
|
[](https://causal-testing-framework.readthedocs.io/en/latest/?badge=latest)
|
|
Binary file
|
|
@@ -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:`<
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
24
|
+
Pytest Extension
|
|
25
|
+
----------------
|
|
26
26
|
|
|
27
|
-
Flakefighters is a pytest extension
|
|
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
|
|
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.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.3.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
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
|
-
#
|
|
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.
|
|
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
|
-
|
|
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.
|
|
137
|
-
|
|
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)
|