Skip to content

Commit 6ca8c37

Browse files
committed
add support for pytest-xdist
1 parent 319666b commit 6ca8c37

File tree

4 files changed

+63
-13
lines changed

4 files changed

+63
-13
lines changed

.github/workflows/test_and_publish.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ jobs:
3131
- macos: py311-test-mpl38
3232
- windows: py311-test-mpl38
3333
# Test newest configurations
34-
- linux: py313-test-mpl310
35-
- macos: py313-test-mpl310
36-
- windows: py313-test-mpl310
34+
- linux: py313-test-mpl310-xdist
35+
- macos: py313-test-mpl310-xdist
36+
- windows: py313-test-mpl310-xdist
3737
# Test intermediate SPEC 0 configurations on Linux
3838
- linux: py311-test-mpl39
3939
- linux: py312-test-mpl39

pytest_mpl/plugin.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import io
3232
import os
3333
import json
34+
import uuid
3435
import shutil
3536
import hashlib
3637
import logging
@@ -216,6 +217,12 @@ def pytest_addoption(parser):
216217
parser.addini(option, help=msg)
217218

218219

220+
class XdistPlugin:
221+
def pytest_configure_node(self, node):
222+
node.workerinput["pytest_mpl_uid"] = node.config.pytest_mpl_uid
223+
node.workerinput["pytest_mpl_results_dir"] = node.config.pytest_mpl_results_dir
224+
225+
219226
def pytest_configure(config):
220227

221228
config.addinivalue_line(
@@ -288,12 +295,20 @@ def get_cli_or_ini(name, default=None):
288295
if not _hash_library_from_cli:
289296
hash_library = os.path.abspath(hash_library)
290297

298+
if not hasattr(config, "workerinput"):
299+
uid = uuid.uuid4().hex
300+
results_dir_path = results_dir or tempfile.mkdtemp()
301+
config.pytest_mpl_uid = uid
302+
config.pytest_mpl_results_dir = results_dir_path
303+
304+
if config.pluginmanager.hasplugin("xdist"):
305+
config.pluginmanager.register(XdistPlugin(), name="pytest_mpl_xdist_plugin")
306+
291307
plugin = ImageComparison(
292308
config,
293309
baseline_dir=baseline_dir,
294310
baseline_relative_dir=baseline_relative_dir,
295311
generate_dir=generate_dir,
296-
results_dir=results_dir,
297312
hash_library=hash_library,
298313
generate_hash_library=generate_hash_lib,
299314
generate_summary=generate_summary,
@@ -356,7 +371,6 @@ def __init__(
356371
baseline_dir=None,
357372
baseline_relative_dir=None,
358373
generate_dir=None,
359-
results_dir=None,
360374
hash_library=None,
361375
generate_hash_library=None,
362376
generate_summary=None,
@@ -372,7 +386,7 @@ def __init__(
372386
self.baseline_dir = baseline_dir
373387
self.baseline_relative_dir = path_is_not_none(baseline_relative_dir)
374388
self.generate_dir = path_is_not_none(generate_dir)
375-
self.results_dir = path_is_not_none(results_dir)
389+
self.results_dir = None
376390
self.hash_library = path_is_not_none(hash_library)
377391
self._hash_library_from_cli = _hash_library_from_cli # for backwards compatibility
378392
self.generate_hash_library = path_is_not_none(generate_hash_library)
@@ -394,11 +408,6 @@ def __init__(
394408
self.deterministic = deterministic
395409
self.default_backend = default_backend
396410

397-
# Generate the containing dir for all test results
398-
if not self.results_dir:
399-
self.results_dir = Path(tempfile.mkdtemp(dir=self.results_dir))
400-
self.results_dir.mkdir(parents=True, exist_ok=True)
401-
402411
# Decide what to call the downloadable results hash library
403412
if self.hash_library is not None:
404413
self.results_hash_library_name = self.hash_library.name
@@ -411,6 +420,14 @@ def __init__(
411420
self._test_stats = None
412421
self.return_value = {}
413422

423+
def pytest_sessionstart(self, session):
424+
config = session.config
425+
if hasattr(config, "workerinput"):
426+
config.pytest_mpl_uid = config.workerinput["pytest_mpl_uid"]
427+
config.pytest_mpl_results_dir = config.workerinput["pytest_mpl_results_dir"]
428+
self.results_dir = Path(config.pytest_mpl_results_dir)
429+
self.results_dir.mkdir(parents=True, exist_ok=True)
430+
414431
def get_logger(self):
415432
# configure a separate logger for this pluggin which is independent
416433
# of the options that are configured for pytest or for the code that
@@ -933,15 +950,20 @@ def pytest_runtest_call(self, item): # noqa
933950
result._excinfo = (type(e), e, e.__traceback__)
934951

935952
def generate_summary_json(self):
936-
json_file = self.results_dir / 'results.json'
953+
filename = "results.json"
954+
if hasattr(self.config, "workerinput"):
955+
worker_id = os.environ.get("PYTEST_XDIST_WORKER")
956+
filename = f"results-xdist-{self.config.pytest_mpl_uid}-{worker_id}.json"
957+
json_file = self.results_dir / filename
937958
with open(json_file, 'w') as f:
938959
json.dump(self._test_results, f, indent=2)
939960
return json_file
940961

941-
def pytest_unconfigure(self, config):
962+
def pytest_sessionfinish(self, session):
942963
"""
943964
Save out the hash library at the end of the run.
944965
"""
966+
config = session.config
945967
result_hash_library = self.results_dir / (self.results_hash_library_name or "temp.json")
946968
if self.generate_hash_library is not None:
947969
hash_library_path = Path(config.rootdir) / self.generate_hash_library
@@ -960,10 +982,24 @@ def pytest_unconfigure(self, config):
960982
json.dump(result_hashes, fp, indent=2)
961983

962984
if self.generate_summary:
985+
try:
986+
import xdist
987+
is_xdist_controller = xdist.is_xdist_controller(session)
988+
is_xdist_worker = xdist.is_xdist_worker(session)
989+
except ImportError:
990+
is_xdist_controller = False
991+
is_xdist_worker = False
963992
kwargs = {}
964993
if 'json' in self.generate_summary:
994+
if is_xdist_controller:
995+
uid = config.pytest_mpl_uid
996+
for worker_results in self.results_dir.glob(f"results-xdist-{uid}-*.json"):
997+
with worker_results.open() as f:
998+
self._test_results.update(json.load(f))
965999
summary = self.generate_summary_json()
9661000
print(f"A JSON report can be found at: {summary}")
1001+
if is_xdist_worker:
1002+
return
9671003
if result_hash_library.exists(): # link to it in the HTML
9681004
kwargs["hash_library"] = result_hash_library.name
9691005
if 'html' in self.generate_summary:

tests/subtests/test_subtest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,19 @@ def test_html(tmp_path):
206206
assert (tmp_path / 'results' / 'styles.css').exists()
207207

208208

209+
@pytest.mark.parametrize("num_workers", [0, 1, 2])
210+
def test_html_xdist(request, tmp_path, num_workers):
211+
if not request.config.pluginmanager.hasplugin("xdist"):
212+
pytest.skip("Skipping: pytest-xdist is not installed")
213+
run_subtest('test_results_always', tmp_path,
214+
[HASH_LIBRARY_FLAG, BASELINE_IMAGES_FLAG_ABS, f"-n{num_workers}"], summaries=['html'],
215+
has_result_hashes=True)
216+
assert (tmp_path / 'results' / 'fig_comparison.html').exists()
217+
assert (tmp_path / 'results' / 'extra.js').exists()
218+
assert (tmp_path / 'results' / 'styles.css').exists()
219+
assert len(list((tmp_path / 'results').glob('results-xdist-*-*.json'))) == num_workers
220+
221+
209222
def test_html_hashes_only(tmp_path):
210223
run_subtest('test_html_hashes_only', tmp_path,
211224
[HASH_LIBRARY_FLAG, *HASH_COMPARISON_MODE],

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ deps =
5151
pytest82: pytest==8.2.*
5252
pytest83: pytest==8.3.*
5353
pytestdev: git+https://github.com/pytest-dev/pytest.git#egg=pytest
54+
xdist: pytest-xdist
5455
extras =
5556
test
5657
commands =

0 commit comments

Comments
 (0)