-
Notifications
You must be signed in to change notification settings - Fork 106
Add pytest.ini support for only_rerun #369
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Allow configuring ``only_rerun`` regular expressions in ``pytest.ini`` files. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,36 @@ Below are the ``pytest.ini`` options supported by the plugin: | |
| [pytest] | ||
| reruns_delay = 2.5 | ||
|
|
||
| ``only_rerun`` | ||
| ^^^^^^^^^^^^^^ | ||
|
|
||
| - **Description**: Sets regular expressions for errors that should be rerun. Add one expression per line. The ``--only-rerun`` command-line flag replaces this list rather than adding to it, and a project-wide ``only_rerun`` also applies to tests carrying a plain ``@pytest.mark.flaky(reruns=...)`` marker — a marked test raising a non-matching error stops rerunning. | ||
| - **Type**: List of strings | ||
| - **Default**: Not set (all errors are eligible for reruns). | ||
| - **Example**: | ||
|
|
||
| .. code-block:: ini | ||
|
|
||
| [pytest] | ||
| only_rerun = | ||
| AssertionError | ||
| ValueError | ||
|
|
||
| ``rerun_except`` | ||
| ^^^^^^^^^^^^^^^^ | ||
|
|
||
| - **Description**: Sets regular expressions for errors that should not be rerun. Add one expression per line. The ``--rerun-except`` command-line flag replaces this list rather than adding to it. | ||
| - **Type**: List of strings | ||
| - **Default**: Not set (all errors are eligible for reruns). | ||
| - **Example**: | ||
|
|
||
| .. code-block:: ini | ||
|
|
||
| [pytest] | ||
| rerun_except = | ||
| AssertionError | ||
| ValueError | ||
|
|
||
| Example | ||
| ------- | ||
|
|
||
|
|
@@ -45,11 +75,13 @@ To configure your test environment for consistent retries and delays, add the fo | |
| [pytest] | ||
| reruns = 3 | ||
| reruns_delay = 2.0 | ||
| only_rerun = AssertionError | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The example gained a third key, but the lead-in above ("for consistent retries and delays") and the "This setup ensures that:" list below still cover only two. A third bullet would help, especially since — Comment created by Claude |
||
|
|
||
| This setup ensures that: | ||
|
|
||
| - Failed tests will be retried up to 3 times. | ||
| - There will be a 2-second delay between each retry. | ||
| - Only failures matching ``AssertionError`` are retried; other errors fail without rerunning. | ||
|
|
||
| Overriding ``pytest.ini`` Options | ||
| --------------------------------- | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,16 @@ def works_with_current_xdist(): | |
| "exponential backoff (delay * factor ** (attempt - 1)). defaults to 1.0, " | ||
| "i.e. a constant delay." | ||
| ) | ||
| ONLY_RERUN_DESC = ( | ||
| "If passed, only rerun errors matching the regex provided. " | ||
| "Pass this flag multiple times (or list one regex per line in the ini " | ||
| "file) to accumulate a list of regexes to match" | ||
| ) | ||
| RERUN_EXCEPT_DESC = ( | ||
| "If passed, only rerun errors other than matching the regex provided. " | ||
| "Pass this flag multiple times (or list one regex per line in the ini " | ||
| "file) to accumulate a list of regexes to match" | ||
| ) | ||
|
|
||
|
|
||
| # command line options | ||
|
|
@@ -99,9 +109,7 @@ def pytest_addoption(parser): | |
| dest="only_rerun", | ||
| type=str, | ||
| default=None, | ||
| help="If passed, only rerun errors matching the regex provided. " | ||
| "Pass this flag multiple times to accumulate a list of regexes " | ||
| "to match", | ||
| help=ONLY_RERUN_DESC, | ||
| ) | ||
| group._addoption( | ||
| "--reruns", | ||
|
|
@@ -130,9 +138,7 @@ def pytest_addoption(parser): | |
| dest="rerun_except", | ||
| type=str, | ||
| default=None, | ||
| help="If passed, only rerun errors other than matching the " | ||
| "regex provided. Pass this flag multiple times to accumulate a list " | ||
| "of regexes to match", | ||
| help=RERUN_EXCEPT_DESC, | ||
| ) | ||
| group._addoption( | ||
| "--rerun-exclude-path", | ||
|
|
@@ -189,6 +195,16 @@ def pytest_addoption(parser): | |
| RERUNS_DELAY_BACKOFF_FACTOR_DESC, | ||
| type=arg_type, | ||
| ) | ||
| parser.addini( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only — Comment created by Claude |
||
| "only_rerun", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not new to this PR, but worth considering now that the pattern can live in a checked-in file: an invalid regex such as — Comment created by Claude |
||
| ONLY_RERUN_DESC, | ||
| type="linelist", | ||
| ) | ||
| parser.addini( | ||
| "rerun_except", | ||
| RERUN_EXCEPT_DESC, | ||
| type="linelist", | ||
| ) | ||
|
|
||
|
|
||
| def _get_global_reruns(config): | ||
|
|
@@ -214,6 +230,15 @@ def check_options(config): | |
| if config.option.usepdb: # a core option | ||
| raise pytest.UsageError("--reruns incompatible with --pdb") | ||
|
|
||
| for name in ("only_rerun", "rerun_except"): | ||
| for pattern in getattr(config.option, name) or config.getini(name): | ||
| try: | ||
| re.compile(pattern) | ||
| except re.error as error: | ||
| raise pytest.UsageError( | ||
| f"invalid regular expression for {name}: {pattern!r} ({error})" | ||
| ) from error | ||
|
|
||
|
|
||
| def _get_marker(item): | ||
| return item.get_closest_marker("flaky") | ||
|
|
@@ -591,6 +616,8 @@ def _get_rerun_filter_regex(item, regex_name): | |
| regex = [regex] | ||
| else: | ||
| regex = getattr(item.session.config.option, regex_name) | ||
| if regex is None: | ||
| regex = item.session.config.getini(regex_name) | ||
|
|
||
| return regex | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1107,6 +1107,176 @@ def test_only_rerun2(): | |
| ) | ||
|
|
||
|
|
||
| def test_only_rerun_ini(testdir): | ||
|
LouisDeconinck marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The central claim of the PR, that the CLI still overrides the ini, has no test. The file pairs — Comment created by Claude |
||
| testdir.makepyfile( | ||
| """ | ||
| def test_assertion_error(): | ||
| raise AssertionError("ERR") | ||
|
|
||
| def test_value_error(): | ||
| raise ValueError("ERR") | ||
| """ | ||
| ) | ||
| testdir.makeini( | ||
| """ | ||
| [pytest] | ||
| reruns = 1 | ||
| only_rerun = AssertionError | ||
| """ | ||
| ) | ||
|
|
||
| result = testdir.runpytest() | ||
| assert_outcomes(result, passed=0, failed=2, rerun=1) | ||
|
|
||
|
|
||
| def test_only_rerun_ini_multiple(testdir): | ||
| testdir.makepyfile( | ||
| """ | ||
| def test_assertion_error(): | ||
| raise AssertionError("ERR") | ||
|
|
||
| def test_value_error(): | ||
| raise ValueError("ERR") | ||
|
|
||
| def test_key_error(): | ||
| raise KeyError("ERR") | ||
| """ | ||
| ) | ||
| testdir.makeini( | ||
| """ | ||
| [pytest] | ||
| reruns = 1 | ||
| only_rerun = | ||
| AssertionError | ||
| ValueError | ||
| """ | ||
| ) | ||
|
|
||
| result = testdir.runpytest() | ||
| assert_outcomes(result, passed=0, failed=3, rerun=2) | ||
|
|
||
|
|
||
| def test_only_rerun_ini_override(testdir): | ||
| testdir.makepyfile( | ||
| """ | ||
| def test_assertion_error(): | ||
| raise AssertionError("ERR") | ||
|
|
||
| def test_value_error(): | ||
| raise ValueError("ERR") | ||
| """ | ||
| ) | ||
| testdir.makeini( | ||
| """ | ||
| [pytest] | ||
| reruns = 1 | ||
| only_rerun = AssertionError | ||
| """ | ||
| ) | ||
|
|
||
| result = testdir.runpytest("--only-rerun", "ValueError") | ||
| assert_outcomes(result, passed=0, failed=2, rerun=1) | ||
| # test_assertion_error fails outright; only test_value_error is rerun, | ||
| # so the progress line must read F-R-F in collection order. | ||
| result.stdout.fnmatch_lines(["test_only_rerun_ini_override.py FRF*"]) | ||
|
|
||
|
|
||
| def test_only_rerun_ini_marker_overrides(testdir): | ||
| testdir.makepyfile( | ||
| """ | ||
| import pytest | ||
|
|
||
| @pytest.mark.flaky(reruns=1, only_rerun="AssertionError") | ||
| def test_assertion_error(): | ||
| raise AssertionError("ERR") | ||
|
|
||
| def test_value_error(): | ||
| raise ValueError("ERR") | ||
| """ | ||
| ) | ||
| testdir.makeini( | ||
| """ | ||
| [pytest] | ||
| reruns = 1 | ||
| only_rerun = ValueError | ||
| """ | ||
| ) | ||
|
|
||
| result = testdir.runpytest() | ||
| assert_outcomes(result, passed=0, failed=2, rerun=2) | ||
|
|
||
|
|
||
| def test_only_rerun_ini_with_rerun_except_flag(testdir): | ||
| testdir.makepyfile( | ||
| """ | ||
| def test_assertion_error(): | ||
| raise AssertionError("ERR") | ||
|
|
||
| def test_value_error(): | ||
| raise ValueError("ERR") | ||
|
|
||
| def test_os_error(): | ||
| raise OSError("ERR") | ||
| """ | ||
| ) | ||
| testdir.makeini( | ||
| """ | ||
| [pytest] | ||
| reruns = 1 | ||
| only_rerun = | ||
| AssertionError | ||
| ValueError | ||
| """ | ||
| ) | ||
|
|
||
| result = testdir.runpytest("--rerun-except", "ValueError") | ||
| assert_outcomes(result, passed=0, failed=3, rerun=1) | ||
|
|
||
|
|
||
| def test_rerun_except_ini(testdir): | ||
| testdir.makepyfile( | ||
| """ | ||
| def test_assertion_error(): | ||
| raise AssertionError("ERR") | ||
|
|
||
| def test_value_error(): | ||
| raise ValueError("ERR") | ||
| """ | ||
| ) | ||
| testdir.makeini( | ||
| """ | ||
| [pytest] | ||
| reruns = 1 | ||
| rerun_except = ValueError | ||
| """ | ||
| ) | ||
|
|
||
| result = testdir.runpytest() | ||
| assert_outcomes(result, passed=0, failed=2, rerun=1) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("option_name", ["only_rerun", "rerun_except"]) | ||
| def test_rerun_filter_ini_invalid_regex(testdir, option_name): | ||
| testdir.makepyfile( | ||
| """ | ||
| def test_foo(): | ||
| raise AssertionError("ERR") | ||
| """ | ||
| ) | ||
| testdir.makeini( | ||
| f""" | ||
| [pytest] | ||
| reruns = 1 | ||
| {option_name} = [unclosed | ||
| """ | ||
| ) | ||
|
|
||
| result = testdir.runpytest() | ||
| result.stderr.fnmatch_lines_random( | ||
| f"*invalid regular expression for {option_name}*" | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "only_rerun,should_rerun", | ||
| [ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
README.rstis also the PyPI long description (seereadmeinpyproject.toml), and it is not touched here: the "Re-run all failures matching certain expressions" section still presents--only-rerunas CLI-only and the "Priority" section shows thepytest.inilayer holding onlyreruns = 3. Leaving it stale reproduces the confusion that led to #165. Previous option additions (e.g.--reruns-delay-backoff-factor) updated README together with the docs.— Comment created by Claude