Skip to content

Commit 3b618c3

Browse files
authored
Merge pull request #146 from ngoldbaum/thread-limit-decorator
Add a parallel_threads_limit mark
2 parents 058c918 + c1e0902 commit 3b618c3

File tree

4 files changed

+126
-13
lines changed

4 files changed

+126
-13
lines changed

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,15 @@ those fixtures are shared between threads.
9393
as errors.
9494

9595

96-
- Three corresponding markers:
97-
- `pytest.mark.parallel_threads(n)` to mark a single test to run
98-
in parallel in `n` threads
96+
- Four corresponding markers:
97+
- `pytest.mark.parallel_threads_limit(n)` to mark a single test to
98+
run in a maximum of `n` threads, even if the `--parallel-threads`
99+
command-line argument is set to a higher value. This is useful if a
100+
test uses resources that should be limited.
101+
- `pytest.mark.parallel_threads(n)` to mark a test to always run in `n`
102+
threads. Note that this implies that the test will be multi-threaded
103+
just because pytest-run-parallel is installed, even if
104+
`--parallel-threads` is not passed at the command-line.
99105
- `pytest.mark.thread_unsafe` to mark a single test to run in a
100106
single thread. It is equivalent to using
101107
`pytest.mark.parallel_threads(1)`

src/pytest_run_parallel/plugin.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ def pytest_itemcollected(self, item):
270270
raise ValueError("parallel-threads cannot be negative")
271271

272272
if n_workers == 1 and parallel_threads_marker_used:
273-
self._mark_test_thread_unsafe(item, "uses the parallel_threads(1) marker")
273+
self._mark_test_thread_unsafe(item, "test is marked as single-threaded")
274274

275275
if n_workers > 1:
276276
thread_unsafe, reason = self._is_thread_unsafe(item)
@@ -418,7 +418,15 @@ def pytest_configure(config):
418418
config.addinivalue_line(
419419
"markers",
420420
"parallel_threads(n): run the given test function in parallel "
421-
"using `n` threads.",
421+
"using `n` threads. Note that if n is greater than 1, the test "
422+
"run with this many threads even if the --parallel-threads "
423+
"command-line argument is not passed. Use parallel_threads_limit "
424+
"instead if you want to avoid this pitfall.",
425+
)
426+
config.addinivalue_line(
427+
"markers",
428+
"parallel_threads_limit(n): run the given test function in parallel "
429+
"using a maximum of `n` threads.",
422430
)
423431
config.addinivalue_line(
424432
"markers",

src/pytest_run_parallel/utils.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,30 @@ def get_configured_num_workers(config):
1111
return n_workers
1212

1313

14+
def auto_or_int(val):
15+
if val == "auto":
16+
logical_cpus = get_logical_cpus()
17+
return logical_cpus if logical_cpus is not None else 1
18+
return int(val)
19+
20+
1421
def get_num_workers(item):
1522
n_workers = get_configured_num_workers(item.config)
23+
# TODO: deprecate in favor of parallel_threads_limit
24+
marker_used = False
1625
marker = item.get_closest_marker("parallel_threads")
1726
if marker is not None:
18-
val = marker.args[0]
19-
if val == "auto":
20-
logical_cpus = get_logical_cpus()
21-
n_workers = logical_cpus if logical_cpus is not None else 1
22-
else:
23-
n_workers = int(val)
24-
25-
return n_workers, marker is not None
27+
marker_used = True
28+
n_workers = auto_or_int(marker.args[0])
29+
limit_marker = item.get_closest_marker("parallel_threads_limit")
30+
if limit_marker is not None:
31+
val = auto_or_int(limit_marker.args[0])
32+
if val == 1:
33+
marker_used = True
34+
if n_workers > val:
35+
n_workers = val
36+
37+
return n_workers, marker_used
2638

2739

2840
def get_num_iterations(item):

tests/test_run_parallel.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,93 @@ def test_single_threaded(num_parallel_threads):
305305
)
306306

307307

308+
def test_parallel_threads_limit_fixture(pytester):
309+
"""Test that the num_parallel_threads fixture works as expected."""
310+
311+
# create a temporary pytest test module
312+
pytester.makepyfile("""
313+
import pytest
314+
315+
def test_should_yield_global_threads(num_parallel_threads):
316+
assert num_parallel_threads == 10
317+
318+
@pytest.mark.parallel_threads_limit(20)
319+
def test_unaffected_by_thread_limit(num_parallel_threads):
320+
assert num_parallel_threads == 10
321+
322+
@pytest.mark.parallel_threads_limit(5)
323+
def test_less_than_thread_limit(num_parallel_threads):
324+
assert num_parallel_threads == 5
325+
326+
@pytest.mark.parallel_threads_limit(1)
327+
def test_single_threaded(num_parallel_threads):
328+
assert num_parallel_threads == 1
329+
""")
330+
331+
# run pytest with the following cmd args
332+
result = pytester.runpytest("--parallel-threads=10", "-v")
333+
334+
# fnmatch_lines does an assertion internally
335+
result.stdout.fnmatch_lines(
336+
[
337+
"*::test_unaffected_by_thread_limit PARALLEL PASSED*",
338+
"*::test_less_than_thread_limit PARALLEL PASSED*",
339+
"*::test_single_threaded PASSED*",
340+
"*1 test was not run in parallel because of use of "
341+
"thread-unsafe functionality, to list the tests that "
342+
"were not run in parallel, re-run while setting PYTEST_RUN_PARALLEL_VERBOSE=1"
343+
" in your shell environment",
344+
]
345+
)
346+
347+
# Re-run with verbose output
348+
orig = os.environ.get("PYTEST_RUN_PARALLEL_VERBOSE", "0")
349+
os.environ["PYTEST_RUN_PARALLEL_VERBOSE"] = "1"
350+
351+
result = pytester.runpytest("--parallel-threads=10", "-v")
352+
os.environ["PYTEST_RUN_PARALLEL_VERBOSE"] = orig
353+
354+
result.stdout.fnmatch_lines(
355+
["*pytest-run-parallel report*", "*::test_single_threaded*"],
356+
consecutive=True,
357+
)
358+
359+
360+
def test_parallel_threads_limit_one_thread(pytester):
361+
"""Test that the num_parallel_threads fixture works as expected."""
362+
363+
# create a temporary pytest test module
364+
pytester.makepyfile("""
365+
import pytest
366+
367+
def test_should_yield_global_threads(num_parallel_threads):
368+
assert num_parallel_threads == 1
369+
370+
@pytest.mark.parallel_threads_limit(5)
371+
def test_marker_threads_five(num_parallel_threads):
372+
assert num_parallel_threads == 1
373+
374+
@pytest.mark.parallel_threads_limit(2)
375+
def test_marker_threads_two(num_parallel_threads):
376+
assert num_parallel_threads == 1
377+
378+
@pytest.mark.parallel_threads_limit(1)
379+
def test_marker_threads_one(num_parallel_threads):
380+
assert num_parallel_threads == 1
381+
""")
382+
383+
# run pytest with the following cmd args
384+
result = pytester.runpytest("-v")
385+
# fnmatch_lines does an assertion internally
386+
result.stdout.fnmatch_lines(
387+
[
388+
"*::test_marker_threads_five PASSED [[]???%[]]",
389+
"*::test_marker_threads_two PASSED [[]???%[]]",
390+
"*::test_marker_threads_one PASSED [[]thread-unsafe[]]*",
391+
]
392+
)
393+
394+
308395
def test_iterations_marker_one_thread(pytester):
309396
# create a temporary pytest test module
310397
pytester.makepyfile("""

0 commit comments

Comments
 (0)