3131import io
3232import os
3333import json
34+ import uuid
3435import shutil
3536import hashlib
3637import 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+
219226def 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
@@ -932,27 +949,65 @@ def pytest_runtest_call(self, item): # noqa
932949 result ._result = None
933950 result ._excinfo = (type (e ), e , e .__traceback__ )
934951
952+ def generate_hash_library_json (self ):
953+ if hasattr (self .config , "workerinput" ):
954+ uid = self .config .pytest_mpl_uid
955+ worker_id = os .environ .get ("PYTEST_XDIST_WORKER" )
956+ json_file = self .results_dir / f"generated-hashes-xdist-{ uid } -{ worker_id } .json"
957+ else :
958+ json_file = Path (self .config .rootdir ) / self .generate_hash_library
959+ json_file .parent .mkdir (parents = True , exist_ok = True )
960+ with open (json_file , 'w' ) as f :
961+ json .dump (self ._generated_hash_library , f , indent = 2 )
962+ return json_file
963+
935964 def generate_summary_json (self ):
936- json_file = self .results_dir / 'results.json'
965+ filename = "results.json"
966+ if hasattr (self .config , "workerinput" ):
967+ uid = self .config .pytest_mpl_uid
968+ worker_id = os .environ .get ("PYTEST_XDIST_WORKER" )
969+ filename = f"results-xdist-{ uid } -{ worker_id } .json"
970+ json_file = self .results_dir / filename
937971 with open (json_file , 'w' ) as f :
938972 json .dump (self ._test_results , f , indent = 2 )
939973 return json_file
940974
941- def pytest_unconfigure (self , config ):
975+ def pytest_sessionfinish (self , session ):
942976 """
943977 Save out the hash library at the end of the run.
944978 """
979+ config = session .config
980+ try :
981+ import xdist
982+ is_xdist_controller = xdist .is_xdist_controller (session )
983+ is_xdist_worker = xdist .is_xdist_worker (session )
984+ except ImportError :
985+ is_xdist_controller = False
986+ is_xdist_worker = False
987+ except Exception as e :
988+ if "xdist" not in session .config .option :
989+ is_xdist_controller = False
990+ is_xdist_worker = False
991+ else :
992+ raise e
993+
994+ if is_xdist_controller : # Merge results from workers
995+ uid = config .pytest_mpl_uid
996+ for worker_hashes in self .results_dir .glob (f"generated-hashes-xdist-{ uid } -*.json" ):
997+ with worker_hashes .open () as f :
998+ self ._generated_hash_library .update (json .load (f ))
999+ for worker_results in self .results_dir .glob (f"results-xdist-{ uid } -*.json" ):
1000+ with worker_results .open () as f :
1001+ self ._test_results .update (json .load (f ))
1002+
9451003 result_hash_library = self .results_dir / (self .results_hash_library_name or "temp.json" )
9461004 if self .generate_hash_library is not None :
947- hash_library_path = Path (config .rootdir ) / self .generate_hash_library
948- hash_library_path .parent .mkdir (parents = True , exist_ok = True )
949- with open (hash_library_path , "w" ) as fp :
950- json .dump (self ._generated_hash_library , fp , indent = 2 )
951- if self .results_always : # Make accessible in results directory
1005+ hash_library_path = self .generate_hash_library_json ()
1006+ if self .results_always and not is_xdist_worker : # Make accessible in results directory
9521007 # Use same name as generated
9531008 result_hash_library = self .results_dir / hash_library_path .name
9541009 shutil .copy (hash_library_path , result_hash_library )
955- elif self .results_always and self .results_hash_library_name :
1010+ elif self .results_always and self .results_hash_library_name and not is_xdist_worker :
9561011 result_hashes = {k : v ['result_hash' ] for k , v in self ._test_results .items ()
9571012 if v ['result_hash' ]}
9581013 if len (result_hashes ) > 0 : # At least one hash comparison test
@@ -964,6 +1019,8 @@ def pytest_unconfigure(self, config):
9641019 if 'json' in self .generate_summary :
9651020 summary = self .generate_summary_json ()
9661021 print (f"A JSON report can be found at: { summary } " )
1022+ if is_xdist_worker :
1023+ return
9671024 if result_hash_library .exists (): # link to it in the HTML
9681025 kwargs ["hash_library" ] = result_hash_library .name
9691026 if 'html' in self .generate_summary :
0 commit comments