31
31
import io
32
32
import os
33
33
import json
34
+ import uuid
34
35
import shutil
35
36
import hashlib
36
37
import logging
@@ -216,6 +217,12 @@ def pytest_addoption(parser):
216
217
parser .addini (option , help = msg )
217
218
218
219
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
+
219
226
def pytest_configure (config ):
220
227
221
228
config .addinivalue_line (
@@ -288,12 +295,20 @@ def get_cli_or_ini(name, default=None):
288
295
if not _hash_library_from_cli :
289
296
hash_library = os .path .abspath (hash_library )
290
297
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
+
291
307
plugin = ImageComparison (
292
308
config ,
293
309
baseline_dir = baseline_dir ,
294
310
baseline_relative_dir = baseline_relative_dir ,
295
311
generate_dir = generate_dir ,
296
- results_dir = results_dir ,
297
312
hash_library = hash_library ,
298
313
generate_hash_library = generate_hash_lib ,
299
314
generate_summary = generate_summary ,
@@ -356,7 +371,6 @@ def __init__(
356
371
baseline_dir = None ,
357
372
baseline_relative_dir = None ,
358
373
generate_dir = None ,
359
- results_dir = None ,
360
374
hash_library = None ,
361
375
generate_hash_library = None ,
362
376
generate_summary = None ,
@@ -372,7 +386,7 @@ def __init__(
372
386
self .baseline_dir = baseline_dir
373
387
self .baseline_relative_dir = path_is_not_none (baseline_relative_dir )
374
388
self .generate_dir = path_is_not_none (generate_dir )
375
- self .results_dir = path_is_not_none ( results_dir )
389
+ self .results_dir = None
376
390
self .hash_library = path_is_not_none (hash_library )
377
391
self ._hash_library_from_cli = _hash_library_from_cli # for backwards compatibility
378
392
self .generate_hash_library = path_is_not_none (generate_hash_library )
@@ -394,11 +408,6 @@ def __init__(
394
408
self .deterministic = deterministic
395
409
self .default_backend = default_backend
396
410
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
-
402
411
# Decide what to call the downloadable results hash library
403
412
if self .hash_library is not None :
404
413
self .results_hash_library_name = self .hash_library .name
@@ -411,6 +420,14 @@ def __init__(
411
420
self ._test_stats = None
412
421
self .return_value = {}
413
422
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
+
414
431
def get_logger (self ):
415
432
# configure a separate logger for this pluggin which is independent
416
433
# of the options that are configured for pytest or for the code that
@@ -932,27 +949,65 @@ def pytest_runtest_call(self, item): # noqa
932
949
result ._result = None
933
950
result ._excinfo = (type (e ), e , e .__traceback__ )
934
951
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
+
935
964
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
937
971
with open (json_file , 'w' ) as f :
938
972
json .dump (self ._test_results , f , indent = 2 )
939
973
return json_file
940
974
941
- def pytest_unconfigure (self , config ):
975
+ def pytest_sessionfinish (self , session ):
942
976
"""
943
977
Save out the hash library at the end of the run.
944
978
"""
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
+
945
1003
result_hash_library = self .results_dir / (self .results_hash_library_name or "temp.json" )
946
1004
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
952
1007
# Use same name as generated
953
1008
result_hash_library = self .results_dir / hash_library_path .name
954
1009
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 :
956
1011
result_hashes = {k : v ['result_hash' ] for k , v in self ._test_results .items ()
957
1012
if v ['result_hash' ]}
958
1013
if len (result_hashes ) > 0 : # At least one hash comparison test
@@ -964,6 +1019,8 @@ def pytest_unconfigure(self, config):
964
1019
if 'json' in self .generate_summary :
965
1020
summary = self .generate_summary_json ()
966
1021
print (f"A JSON report can be found at: { summary } " )
1022
+ if is_xdist_worker :
1023
+ return
967
1024
if result_hash_library .exists (): # link to it in the HTML
968
1025
kwargs ["hash_library" ] = result_hash_library .name
969
1026
if 'html' in self .generate_summary :
0 commit comments