Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,20 @@ those fixtures are shared between threads.
adding support for Python 3.14 to a library that already
runs tests under pytest-run-parallel on Python 3.13 or
older.
- `--mark-hypothesis-as-unsafe`, to always skip runing tests that
- `--mark-hypothesis-as-unsafe` to always skip runing tests that
use [hypothesis](https://github.com/hypothesisworks/hypothesis).
While newer version of Hypothesis are thread-safe, and versions
which are not are automatically skipped by `pytest-run-parallel`,
this flag is an escape hatch in case you run into thread-safety
problems caused by Hypothesis, or in tests that happen to use
hypothesis and were skipped in older versions of pytest-run-parallel.
- `--ignore-gil-enabled`, to ignore the RuntimeWarning generated
- `--forever` to keep running tests in an endless loop starting
from the top when completing a test run, until a test crashes or
the user explicitly ends the process with Ctrl-C. This is especially
helpful when trying to reproduce thread safety bugs that might only
occur rarely. Note that pytest's progress indicator will keep showing
100% forever after the first pass of the test suite.
- `--ignore-gil-enabled` to ignore the RuntimeWarning generated
when the GIL is enabled at runtime on the free-threaded build
and run the tests despite the fact that the GIL is enabled.
This option has no effect if pytest is configured to treat warnings
Expand Down
60 changes: 60 additions & 0 deletions src/pytest_run_parallel/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def __init__(self, config):
self.mark_ctypes_as_unsafe = config.option.mark_ctypes_as_unsafe
self.mark_hypothesis_as_unsafe = config.option.mark_hypothesis_as_unsafe
self.ignore_gil_enabled = config.option.ignore_gil_enabled
self.forever = config.option.forever

skipped_functions = [
x.split(".") for x in config.getini("thread_unsafe_functions")
Expand Down Expand Up @@ -169,6 +170,56 @@ def _is_thread_unsafe(self, item):
self.mark_hypothesis_as_unsafe,
)

@pytest.hookimpl(tryfirst=True)
def pytest_runtestloop(self, session: pytest.Session):
"""
Based on the default implementation in pytest, but also adds support
for running the tests in an endless loop.
"""

if (
session.testsfailed
and not session.config.option.continue_on_collection_errors
):
raise session.Interrupted(
"%d errors during collection" % session.testsfailed
)

if session.config.option.collectonly:
return True

number_of_items = len(session.items)
iter_number = 0
idx = 0
next_idx = idx + 1
if self.forever:
next_idx = next_idx % number_of_items

while idx < number_of_items:
if idx == 0 and self.forever:
print("\n\n", end="")
print("==========================================================")
print("You ran the test suite with 'forever' mode enabled.")
print(f"Running the tests again. This is iteration #{iter_number}.")
print("==========================================================")
iter_number += 1

item = session.items[idx]
nextitem = session.items[next_idx] if next_idx < number_of_items else None

item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
if session.shouldfail:
raise session.Failed(session.shouldfail)
if session.shouldstop:
raise session.Interrupted(session.shouldstop)

idx = next_idx
next_idx = idx + 1
if self.forever:
next_idx = next_idx % number_of_items

return True

@pytest.hookimpl(trylast=True)
def pytest_itemcollected(self, item):
if not hasattr(item, "obj"):
Expand Down Expand Up @@ -419,6 +470,15 @@ def pytest_addoption(parser):
"re-enabled at runtime, pytest will exit with a non-zero exit code. This option has no "
"effect for non-free-threaded builds.",
)
group.addoption(
"--forever",
action="store_true",
dest="forever",
default=False,
help="Run the test loop forever (starting from the top when all the tests have been run), "
"until one crashes or the user explicitly stops the process with Ctrl-C. This is especially "
"helpful for hitting thread safety bugs that only occur rarely.",
)
parser.addini(
"thread_unsafe_fixtures",
"list of thread-unsafe fixture names that cause a test to "
Expand Down
19 changes: 3 additions & 16 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.