diff --git a/README.rst b/README.rst index 76194c7..c29dc9c 100644 --- a/README.rst +++ b/README.rst @@ -128,6 +128,19 @@ would only rerun those errors that does not match with ``AssertionError`` or ``O $ pytest --reruns 5 --rerun-except AssertionError --rerun-except OSError +Exclude test paths from re-runs +-------------------------------- + +To exclude a test file or all tests under a directory from re-runs, use the +``--rerun-exclude-path`` option. Relative paths are interpreted relative to +pytest's root directory: + +.. code-block:: bash + + $ pytest --reruns 3 --rerun-exclude-path docs + +The option may be passed multiple times to exclude multiple paths. + Re-run individual failures -------------------------- diff --git a/changes/232.feature.rst b/changes/232.feature.rst new file mode 100644 index 0000000..4cb9839 --- /dev/null +++ b/changes/232.feature.rst @@ -0,0 +1 @@ +Add a repeatable ``--rerun-exclude-path`` option to exclude test files or directories from reruns. diff --git a/src/pytest_rerunfailures.py b/src/pytest_rerunfailures.py index 69c04c1..456854e 100644 --- a/src/pytest_rerunfailures.py +++ b/src/pytest_rerunfailures.py @@ -121,6 +121,16 @@ def pytest_addoption(parser): "regex provided. Pass this flag multiple times to accumulate a list " "of regexes to match", ) + group._addoption( + "--rerun-exclude-path", + action="append", + dest="rerun_exclude_path", + type=str, + default=None, + help="If passed, do not rerun tests in the path provided. " + "Pass this flag multiple times to accumulate a list of paths " + "to exclude", + ) group._addoption( "--reruns-mode", action="store", @@ -847,6 +857,14 @@ def _restore_suspended_finalizers(item): suspended_finalizers.clear() +def _is_rerun_path_excluded(item): + excluded_paths = item.config.getoption("rerun_exclude_path") or [] + return any( + item.path.is_relative_to(item.config.rootpath / excluded_path) + for excluded_path in excluded_paths + ) + + def pytest_runtest_teardown(item, nextitem): reruns = get_reruns_count(item) if reruns is None: @@ -918,6 +936,9 @@ def pytest_runtest_protocol(item, nextitem): Note: when teardown fails, two reports are generated for the case, one for the test case and the other for the teardown error. """ + if _is_rerun_path_excluded(item): + return + reruns = get_reruns_count(item) if reruns is None: # global setting is not specified, and this test is not marked with diff --git a/tests/test_pytest_rerunfailures.py b/tests/test_pytest_rerunfailures.py index 24b92f3..4787438 100644 --- a/tests/test_pytest_rerunfailures.py +++ b/tests/test_pytest_rerunfailures.py @@ -920,6 +920,162 @@ def test_rerun_except_flag(testdir, rerun_except_texts, should_rerun): ) +def test_rerun_exclude_path_prevents_reruns_in_directory(testdir): + testdir.makeconftest( + """ + from pathlib import Path + + def pytest_runtest_call(item): + with Path("executions").open("a") as execution_log: + execution_log.write(item.name + "\\n") + """ + ) + testdir.makepyfile( + test_normal=""" + def test_normal_failure(): + assert False + """ + ) + excluded_dir = testdir.mkdir("excluded") + excluded_dir.join("test_excluded.py").write( + """ +def test_excluded_failure(): + assert False +""" + ) + + result = testdir.runpytest("--reruns", "2", "--rerun-exclude-path", "excluded") + + assert_outcomes(result, passed=0, failed=2, rerun=2) + executions = testdir.tmpdir.join("executions").read().splitlines() + assert executions.count("test_normal_failure") == 3 + assert executions.count("test_excluded_failure") == 1 + + +def test_rerun_exclude_path_prevents_reruns_for_file(testdir): + testdir.makeconftest( + """ + from pathlib import Path + + def pytest_runtest_call(item): + with Path("executions").open("a") as execution_log: + execution_log.write(item.name + "\\n") + """ + ) + testdir.makepyfile( + test_normal=""" + def test_normal_failure(): + assert False + """ + ) + excluded_dir = testdir.mkdir("excluded") + excluded_dir.join("test_excluded.py").write( + """ +def test_excluded_failure(): + assert False +""" + ) + + result = testdir.runpytest( + "--reruns", + "2", + "--rerun-exclude-path", + "excluded/test_excluded.py", + ) + + assert_outcomes(result, passed=0, failed=2, rerun=2) + executions = testdir.tmpdir.join("executions").read().splitlines() + assert executions.count("test_normal_failure") == 3 + assert executions.count("test_excluded_failure") == 1 + + +def test_rerun_exclude_path_accepts_multiple_paths(testdir): + testdir.makeconftest( + """ + from pathlib import Path + + def pytest_runtest_call(item): + with Path("executions").open("a") as execution_log: + execution_log.write(item.name + "\\n") + """ + ) + testdir.makepyfile( + test_normal=""" + def test_normal_failure(): + assert False + """ + ) + excluded_a_dir = testdir.mkdir("excluded_a") + excluded_a_dir.join("test_excluded_a.py").write( + """ +def test_excluded_a_failure(): + assert False +""" + ) + excluded_b_dir = testdir.mkdir("excluded_b") + excluded_b_dir.join("test_excluded_b.py").write( + """ +def test_excluded_b_failure(): + assert False +""" + ) + + result = testdir.runpytest( + "--reruns", + "2", + "--rerun-exclude-path", + "excluded_a", + "--rerun-exclude-path", + "excluded_b", + ) + + assert_outcomes(result, passed=0, failed=3, rerun=2) + executions = testdir.tmpdir.join("executions").read().splitlines() + assert executions.count("test_normal_failure") == 3 + assert executions.count("test_excluded_a_failure") == 1 + assert executions.count("test_excluded_b_failure") == 1 + + +def test_rerun_exclude_path_prevents_reruns_for_doctest(testdir): + testdir.makeconftest( + """ + from pathlib import Path + + def pytest_runtest_call(item): + with Path("executions").open("a") as execution_log: + execution_log.write(item.name + "\\n") + """ + ) + testdir.makepyfile( + test_normal=""" + def test_normal_failure(): + assert False + """ + ) + docs_dir = testdir.mkdir("docs") + docs_dir.join("guide.txt").write( + """ +This example fails: + + >>> 1 + 1 + 3 +""" + ) + + result = testdir.runpytest( + "--doctest-glob=*.txt", + "--reruns", + "2", + "--rerun-exclude-path", + "docs/guide.txt", + ) + + assert_outcomes(result, passed=0, failed=2, rerun=2) + executions = testdir.tmpdir.join("executions").read().splitlines() + assert executions.count("test_normal_failure") == 3 + assert executions.count("guide.txt") == 1 + + @pytest.mark.parametrize( "only_rerun_texts, rerun_except_texts, should_rerun", [