4747]
4848
4949
50+ def xdist_args (n_workers ):
51+ try :
52+ import xdist
53+ if n_workers is None :
54+ return ["-p" , "no:xdist" ]
55+ else :
56+ return ["-n" , str (n_workers )]
57+ except ImportError :
58+ return []
59+
60+
5061def run_subtest (baseline_summary_name , tmp_path , args , summaries = None , xfail = True ,
51- has_result_hashes = False , generating_hashes = False , testing_hashes = False ,
62+ has_result_hashes = False , generating_hashes = False , testing_hashes = False , n_xdist_workers = None ,
5263 update_baseline = UPDATE_BASELINE , update_summary = UPDATE_SUMMARY ):
5364 """ Run pytest (within pytest) and check JSON summary report.
5465
@@ -72,6 +83,9 @@ def run_subtest(baseline_summary_name, tmp_path, args, summaries=None, xfail=Tru
7283 both of `--mpl-hash-library` and `hash_library=` were not.
7384 testing_hashes : bool, optional, default=False
7485 Whether the subtest is comparing hashes and therefore needs baseline hashes generated.
86+ n_xdist_workers : str or int, optional, default=None
87+ Number of xdist workers to use, or "auto" to use all available cores.
88+ None will disable xdist. If pytest-xdist is not installed, this will be ignored.
7589 """
7690 if update_baseline and update_summary :
7791 raise ValueError ("Cannot enable both `update_baseline` and `update_summary`." )
@@ -109,6 +123,8 @@ def run_subtest(baseline_summary_name, tmp_path, args, summaries=None, xfail=Tru
109123 shutil .copy (expected_result_hash_library , baseline_hash_library )
110124 transform_hashes (baseline_hash_library )
111125
126+ pytest_args .extend (xdist_args (n_xdist_workers ))
127+
112128 # Run the test and record exit status
113129 status = subprocess .call (pytest_args + mpl_args + args )
114130
@@ -201,23 +217,50 @@ def test_html(tmp_path):
201217 run_subtest ('test_results_always' , tmp_path ,
202218 [HASH_LIBRARY_FLAG , BASELINE_IMAGES_FLAG_ABS ], summaries = ['html' ],
203219 has_result_hashes = True )
204- assert (tmp_path / 'results' / 'fig_comparison.html' ).exists ()
220+ html_path = tmp_path / 'results' / 'fig_comparison.html'
221+ assert html_path .exists ()
222+ assert html_path .stat ().st_size > 200_000
223+ assert "Baseline image differs" in html_path .read_text ()
224+ assert (tmp_path / 'results' / 'extra.js' ).exists ()
225+ assert (tmp_path / 'results' / 'styles.css' ).exists ()
226+
227+
228+ @pytest .mark .parametrize ("num_workers" , [None , 0 , 1 , 2 ])
229+ def test_html_xdist (request , tmp_path , num_workers ):
230+ if not request .config .pluginmanager .hasplugin ("xdist" ):
231+ pytest .skip ("Skipping: pytest-xdist is not installed" )
232+ run_subtest ('test_results_always' , tmp_path ,
233+ [HASH_LIBRARY_FLAG , BASELINE_IMAGES_FLAG_ABS ], summaries = ['html' ],
234+ has_result_hashes = True , n_xdist_workers = num_workers )
235+ html_path = tmp_path / 'results' / 'fig_comparison.html'
236+ assert html_path .exists ()
237+ assert html_path .stat ().st_size > 200_000
238+ assert "Baseline image differs" in html_path .read_text ()
205239 assert (tmp_path / 'results' / 'extra.js' ).exists ()
206240 assert (tmp_path / 'results' / 'styles.css' ).exists ()
241+ if num_workers is not None :
242+ assert len (list ((tmp_path / 'results' ).glob ('generated-hashes-xdist-*-*.json' ))) == 0
243+ assert len (list ((tmp_path / 'results' ).glob ('results-xdist-*-*.json' ))) == num_workers
207244
208245
209246def test_html_hashes_only (tmp_path ):
210247 run_subtest ('test_html_hashes_only' , tmp_path ,
211248 [HASH_LIBRARY_FLAG , * HASH_COMPARISON_MODE ],
212249 summaries = ['html' ], has_result_hashes = True )
213- assert (tmp_path / 'results' / 'fig_comparison.html' ).exists ()
250+ html_path = tmp_path / 'results' / 'fig_comparison.html'
251+ assert html_path .exists ()
252+ assert html_path .stat ().st_size > 100_000
253+ assert "Baseline hash differs" in html_path .read_text ()
214254 assert (tmp_path / 'results' / 'extra.js' ).exists ()
215255 assert (tmp_path / 'results' / 'styles.css' ).exists ()
216256
217257
218258def test_html_images_only (tmp_path ):
219259 run_subtest ('test_html_images_only' , tmp_path , [* IMAGE_COMPARISON_MODE ], summaries = ['html' ])
220- assert (tmp_path / 'results' / 'fig_comparison.html' ).exists ()
260+ html_path = tmp_path / 'results' / 'fig_comparison.html'
261+ assert html_path .exists ()
262+ assert html_path .stat ().st_size > 200_000
263+ assert "Baseline image differs" in html_path .read_text ()
221264 assert (tmp_path / 'results' / 'extra.js' ).exists ()
222265 assert (tmp_path / 'results' / 'styles.css' ).exists ()
223266
@@ -226,7 +269,10 @@ def test_basic_html(tmp_path):
226269 run_subtest ('test_results_always' , tmp_path ,
227270 [HASH_LIBRARY_FLAG , * BASELINE_IMAGES_FLAG_REL ], summaries = ['basic-html' ],
228271 has_result_hashes = True )
229- assert (tmp_path / 'results' / 'fig_comparison_basic.html' ).exists ()
272+ html_path = tmp_path / 'results' / 'fig_comparison_basic.html'
273+ assert html_path .exists ()
274+ assert html_path .stat ().st_size > 20_000
275+ assert "hash comparison, although" in html_path .read_text ()
230276
231277
232278def test_generate (tmp_path ):
@@ -257,23 +303,53 @@ def test_html_generate(tmp_path):
257303 rf'--mpl-generate-hash-library={ tmp_path / "test_hashes.json" } ' ],
258304 summaries = ['html' ], xfail = False , has_result_hashes = "test_hashes.json" ,
259305 generating_hashes = True )
260- assert (tmp_path / 'results' / 'fig_comparison.html' ).exists ()
306+ html_path = tmp_path / 'results' / 'fig_comparison.html'
307+ assert html_path .exists ()
308+ assert html_path .stat ().st_size > 100_000
309+ assert "Baseline image was generated" in html_path .read_text ()
310+
311+
312+ @pytest .mark .parametrize ("num_workers" , [None , 0 , 1 , 2 ])
313+ def test_html_generate_xdist (request , tmp_path , num_workers ):
314+ # generating hashes and images; no testing
315+ if not request .config .pluginmanager .hasplugin ("xdist" ):
316+ pytest .skip ("Skipping: pytest-xdist is not installed" )
317+ run_subtest ('test_html_generate' , tmp_path ,
318+ [rf'--mpl-generate-path={ tmp_path } ' ,
319+ rf'--mpl-generate-hash-library={ tmp_path / "test_hashes.json" } ' ],
320+ summaries = ['html' ], xfail = False , has_result_hashes = "test_hashes.json" ,
321+ generating_hashes = True , n_xdist_workers = num_workers )
322+ html_path = tmp_path / 'results' / 'fig_comparison.html'
323+ assert html_path .exists ()
324+ assert html_path .stat ().st_size > 100_000
325+ assert "Baseline image was generated" in html_path .read_text ()
326+ assert (tmp_path / 'results' / 'extra.js' ).exists ()
327+ assert (tmp_path / 'results' / 'styles.css' ).exists ()
328+ if num_workers is not None :
329+ assert len (list ((tmp_path / 'results' ).glob ('generated-hashes-xdist-*-*.json' ))) == num_workers
330+ assert len (list ((tmp_path / 'results' ).glob ('results-xdist-*-*.json' ))) == num_workers
261331
262332
263333def test_html_generate_images_only (tmp_path ):
264334 # generating images; no testing
265335 run_subtest ('test_html_generate_images_only' , tmp_path ,
266336 [rf'--mpl-generate-path={ tmp_path } ' , * IMAGE_COMPARISON_MODE ],
267337 summaries = ['html' ], xfail = False )
268- assert (tmp_path / 'results' / 'fig_comparison.html' ).exists ()
338+ html_path = tmp_path / 'results' / 'fig_comparison.html'
339+ assert html_path .exists ()
340+ assert html_path .stat ().st_size > 100_000
341+ assert "Baseline image was generated" in html_path .read_text ()
269342
270343
271344def test_html_generate_hashes_only (tmp_path ):
272345 # generating hashes; testing images
273346 run_subtest ('test_html_generate_hashes_only' , tmp_path ,
274347 [rf'--mpl-generate-hash-library={ tmp_path / "test_hashes.json" } ' ],
275348 summaries = ['html' ], has_result_hashes = "test_hashes.json" , generating_hashes = True )
276- assert (tmp_path / 'results' / 'fig_comparison.html' ).exists ()
349+ html_path = tmp_path / 'results' / 'fig_comparison.html'
350+ assert html_path .exists ()
351+ assert html_path .stat ().st_size > 200_000
352+ assert "Baseline hash was generated" in html_path .read_text ()
277353
278354
279355def test_html_run_generate_hashes_only (tmp_path ):
@@ -282,9 +358,28 @@ def test_html_run_generate_hashes_only(tmp_path):
282358 [rf'--mpl-generate-hash-library={ tmp_path / "test_hashes.json" } ' ,
283359 HASH_LIBRARY_FLAG , * HASH_COMPARISON_MODE ],
284360 summaries = ['html' ], has_result_hashes = "test_hashes.json" )
285- assert (tmp_path / 'results' / 'fig_comparison.html' ).exists ()
361+ html_path = tmp_path / 'results' / 'fig_comparison.html'
362+ assert html_path .exists ()
363+ assert html_path .stat ().st_size > 100_000
364+ assert "Baseline hash differs" in html_path .read_text ()
286365
287366
288367# Run a hybrid mode test last so if generating hash libraries, it includes all the hashes.
289368def test_hybrid (tmp_path ):
290369 run_subtest ('test_hybrid' , tmp_path , [HASH_LIBRARY_FLAG , BASELINE_IMAGES_FLAG_ABS ], testing_hashes = True )
370+
371+
372+ @pytest .mark .parametrize ("num_workers" , [None , 0 , 1 , 2 ])
373+ def test_html_no_json (tmp_path , num_workers ):
374+ # Previous tests require JSON summary to be generated to function correctly.
375+ # This test ensures HTML summary generation works without JSON summary.
376+ results_path = tmp_path / 'results'
377+ results_path .mkdir ()
378+ mpl_args = ['--mpl' , rf'--mpl-results-path={ results_path .as_posix ()} ' ,
379+ '--mpl-generate-summary=html' , * xdist_args (num_workers )]
380+ subprocess .call ([sys .executable , '-m' , 'pytest' , str (TEST_FILE ), * mpl_args ])
381+ assert not (tmp_path / 'results' / 'results.json' ).exists ()
382+ html_path = tmp_path / 'results' / 'fig_comparison.html'
383+ assert html_path .exists ()
384+ assert html_path .stat ().st_size > 200_000
385+ assert "Baseline image differs" in html_path .read_text ()
0 commit comments