diff --git a/Pipfile b/Pipfile index cd097b5..6639705 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,7 @@ name = "pypi" [packages] pytest = ">=3.0.0" tblib = "*" +six = "*" [dev-packages] pylint = "*" diff --git a/README.md b/README.md index d0507bf..d09aebb 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,19 @@ pytest --tests-per-worker auto pytest --workers 2 --tests-per-worker auto ``` +## Marking some tests to run in a single process + +Sometimes you might have one or more tests which should not be run in parallel. +In this case, you might mark them with `no_parallel` marker: + +```python +@pytest.mark.no_parallel +def test_sequential(driver): + do_something() +``` + +Such tests will be runned in the main process in a serialized manner. + ## Notice Beginning with Python 3.8, forking behavior is forced on macOS at the expense of safety. diff --git a/pytest_parallel/__init__.py b/pytest_parallel/__init__.py index 5702a64..a8ad7be 100644 --- a/pytest_parallel/__init__.py +++ b/pytest_parallel/__init__.py @@ -93,8 +93,6 @@ def run(self): run_test(self.session, item, None) except BaseException: import pickle - import sys - self.errors.put((self.name, pickle.dumps(sys.exc_info()))) finally: try: @@ -110,6 +108,10 @@ def pytest_configure(config): if not config.option.collectonly and (workers or tests_per_worker): config.pluginmanager.register(ParallelRunner(config), 'parallelrunner') + config.addinivalue_line( + "markers", "no_parallel: mark test to run in a single process" + ) + class ThreadLocalEnviron(os._Environ): def __init__(self, env): @@ -271,8 +273,15 @@ def pytest_runtestloop(self, session): # This way, report generators like JUnitXML will work as expected. self.responses_queue = queue_cls() - for i in range(len(session.items)): - queue.put(i) + # Current process is not a worker. + # This flag will be changed after the worker's fork. + self._config.parallel_worker = False + + for idx, item in enumerate(session.items): + if has_no_parallel_marker(item): + run_test(session, item, None) + else: + queue.put(idx) # Now we need to put stopping sentinels, so that worker # processes will know, there is time to finish the work. @@ -292,10 +301,6 @@ def wait_for_responses_processor(): processes = [] - # Current process is not a worker. - # This flag will be changed after the worker's fork. - self._config.parallel_worker = False - args = (self._config, queue, session, tests_per_worker, errors) for _ in range(self.workers): process = Process(target=process_with_threads, args=args) @@ -363,3 +368,8 @@ def process_responses(self, queue): queue.task_done() except ConnectionRefusedError: pass + + +def has_no_parallel_marker(item): + markers = list(item.iter_markers(name='no_parallel')) + return len(markers) > 0 diff --git a/tests/test_no_parallel.py b/tests/test_no_parallel.py new file mode 100644 index 0000000..d50ec8a --- /dev/null +++ b/tests/test_no_parallel.py @@ -0,0 +1,27 @@ +import pytest +import re + + +def test_no_parallel_marker(testdir): + testdir.makepyfile(""" + import os + import pytest + + # Remember the PID of a process + # which loaded the module. + ppid = os.getpid() + + def test_parallel(): + # This test should be executed in a subprocess + assert ppid != os.getpid() + assert ppid == os.getppid() + + @pytest.mark.no_parallel + def test_no_parallel(): + # And this one is in the main process + assert ppid == os.getpid() + + """) + result = testdir.runpytest('--workers=2') + result.assert_outcomes(passed=2) + assert result.ret == 0