49
49
Actual shape: {actual_shape}
50
50
{actual_path}"""
51
51
52
+ HTML_INTRO = """
53
+ <!DOCTYPE html>
54
+ <html>
55
+ <head>
56
+ <style>
57
+ table, th, td {
58
+ border: 1px solid black;
59
+ }
60
+ </style>
61
+ </head>
62
+ <body>
63
+ <h2>Image test comparison</h2>
64
+ <table>
65
+ <tr>
66
+ <th>Test Name</th>
67
+ <th>Baseline image</th>
68
+ <th>Diff</th>
69
+ <th>New image</th>
70
+ </tr>
71
+ """
72
+
52
73
53
74
def _download_file (baseline , filename ):
54
75
# Note that baseline can be a comma-separated list of URLs that we can
@@ -82,6 +103,20 @@ def _hash_file(in_stream):
82
103
return hasher .hexdigest ()
83
104
84
105
106
+ def pathify (path ):
107
+ """
108
+ Remove non-path safe characters.
109
+ """
110
+ path = Path (path )
111
+ ext = path .suffix
112
+ path = str (path ).split (ext )[0 ]
113
+ path = path .replace ('[' , '_' ).replace (']' , '_' )
114
+ path = path .replace ('/' , '_' )
115
+ if path .endswith ('_' ):
116
+ path = path [:- 2 ]
117
+ return Path (path + ext )
118
+
119
+
85
120
def pytest_report_header (config , startdir ):
86
121
import matplotlib
87
122
import matplotlib .ft2font
@@ -116,6 +151,8 @@ def pytest_addoption(parser):
116
151
results_path_help = "directory for test results, relative to location where py.test is run"
117
152
group .addoption ('--mpl-results-path' , help = results_path_help , action = 'store' )
118
153
parser .addini ('mpl-results-path' , help = results_path_help )
154
+ parser .addini ('mpl-use-full-test-name' , help = "use fully qualified test name as the filename." ,
155
+ type = 'bool' )
119
156
120
157
121
158
def pytest_configure (config ):
@@ -211,7 +248,7 @@ def path_is_not_none(apath):
211
248
return Path (apath ) if apath is not None else apath
212
249
213
250
214
- class ImageComparison ( object ) :
251
+ class ImageComparison :
215
252
216
253
def __init__ (self ,
217
254
config ,
@@ -250,17 +287,16 @@ def generate_filename(self, item):
250
287
"""
251
288
Given a pytest item, generate the figure filename.
252
289
"""
253
- return self .generate_test_name (item ) + '.png'
254
- compare = self .get_compare (item )
255
- # Find test name to use as plot name
256
- filename = compare .kwargs .get ('filename' , None )
257
- if filename is None :
258
- filename = item .name + '.png'
259
- filename = filename .replace ('[' , '_' ).replace (']' , '_' )
260
- filename = filename .replace ('/' , '_' )
261
- filename = filename .replace ('_.png' , '.png' )
290
+ if self .config .getini ('mpl-use-full-test-name' ):
291
+ filename = self .generate_test_name (item ) + '.png'
292
+ else :
293
+ compare = self .get_compare (item )
294
+ # Find test name to use as plot name
295
+ filename = compare .kwargs .get ('filename' , None )
296
+ if filename is None :
297
+ filename = item .name + '.png'
262
298
263
- return filename
299
+ return str ( pathify ( filename ))
264
300
265
301
def generate_test_name (self , item ):
266
302
"""
@@ -413,6 +449,7 @@ def load_hash_library(self, library_path):
413
449
414
450
def compare_image_to_hash_library (self , item , fig , result_dir ):
415
451
compare = self .get_compare (item )
452
+ savefig_kwargs = compare .kwargs .get ('savefig_kwargs' , {})
416
453
417
454
hash_library_filename = self .hash_library or compare .kwargs .get ('hash_library' , None )
418
455
hash_library_filename = (Path (item .fspath ).parent / hash_library_filename ).absolute ()
@@ -431,13 +468,16 @@ def compare_image_to_hash_library(self, item, fig, result_dir):
431
468
if test_hash == hash_library [hash_name ]:
432
469
return
433
470
434
- error_message = (f"hash { test_hash } doesn't match hash "
471
+ error_message = (f"Hash { test_hash } doesn't match hash "
435
472
f"{ hash_library [hash_name ]} in library "
436
473
f"{ hash_library_filename } for test { hash_name } ." )
437
474
438
475
# If the compare has only been specified with hash and not baseline
439
476
# dir, don't attempt to find a baseline image at the default path.
440
477
if not self .baseline_directory_specified (item ):
478
+ # Save the figure for later summary
479
+ test_image = (result_dir / "result.png" ).absolute ()
480
+ fig .savefig (str (test_image ), ** savefig_kwargs )
441
481
return error_message
442
482
443
483
baseline_image_path = self .obtain_baseline_image (item , result_dir )
@@ -449,10 +489,13 @@ def compare_image_to_hash_library(self, item, fig, result_dir):
449
489
450
490
if baseline_image is None :
451
491
error_message += f"\n Unable to find baseline image { baseline_image_path } ."
452
-
453
- if baseline_image is None :
454
492
return error_message
455
493
494
+ # Override the tolerance (if not explicitly set) to 0 as the hashes are not forgiving
495
+ tolerance = compare .kwargs .get ('tolerance' , None )
496
+ if not tolerance :
497
+ compare .kwargs ['tolerance' ] = 0
498
+
456
499
comparison_error = (self .compare_image_to_baseline (item , fig , result_dir ) or
457
500
"\n However, the comparison to the baseline image succeeded." )
458
501
@@ -532,6 +575,28 @@ def item_function_wrapper(*args, **kwargs):
532
575
else :
533
576
item .obj = item_function_wrapper
534
577
578
+ def generate_summary_html (self , dir_list ):
579
+ """
580
+ Generate a simple HTML table of the failed test results
581
+ """
582
+ html_file = self .results_dir / 'fig_comparison.html'
583
+ with open (html_file , 'w' ) as f :
584
+ f .write (HTML_INTRO )
585
+
586
+ for directory in dir_list :
587
+ f .write ('<tr>'
588
+ f'<td>{ directory .parts [- 1 ]} \n '
589
+ f'<td><img src="{ directory / "baseline.png" } "></td>\n '
590
+ f'<td><img src="{ directory / "result-failed-diff.png" } "></td>\n '
591
+ f'<td><img src="{ directory / "result.png" } "></td>\n '
592
+ '</tr>\n \n ' )
593
+
594
+ f .write ('</table>\n ' )
595
+ f .write ('</body>\n ' )
596
+ f .write ('</html>' )
597
+
598
+ return html_file
599
+
535
600
def pytest_unconfigure (self , config ):
536
601
"""
537
602
Save out the hash library at the end of the run.
@@ -543,10 +608,14 @@ def pytest_unconfigure(self, config):
543
608
json .dump (self ._generated_hash_library , fp , indent = 2 )
544
609
545
610
if self .generate_summary :
546
- breakpoint ()
611
+ # Generate a list of test directories
612
+ dir_list = [p .relative_to (self .results_dir )
613
+ for p in self .results_dir .iterdir () if p .is_dir ()]
614
+ html_summary = self .generate_summary_html (dir_list )
615
+ print (f"A summary of the failed tests can be found at: { html_summary } " )
547
616
548
617
549
- class FigureCloser ( object ) :
618
+ class FigureCloser :
550
619
"""
551
620
This is used in place of ImageComparison when the --mpl option is not used,
552
621
to make sure that we still close figures returned by tests.
0 commit comments