Skip to content

Commit 54510c5

Browse files
authored
Merge pull request #138 from ConorMacBride/html-hashes-download
Downloadable hash library in HTML summary
2 parents 0ce475f + 0b63114 commit 54510c5

26 files changed

+1520
-100
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
0.15 (unreleased)
22
-----------------
33

4+
- An updated hash library will be saved to the results directory when
5+
generating a HTML summary page or when the `--mpl-results-always` flag is
6+
set. A button to download this file is included in the HTML summary.
7+
Various bugfixes, test improvements and documentation updates. [#138]
48

59
0.14 (2022-02-09)
610
-----------------

README.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ can either be specified via the ``--mpl-hash-library=`` command line argument,
8686
or via the ``hash_library=`` keyword argument to the
8787
``@pytest.mark.mpl_image_compare`` decorator.
8888

89+
When generating a hash library, the tests will also be run as usual against the
90+
existing hash library specified by ``--mpl-hash-library`` or the keyword argument.
91+
However, generating baseline images will always result in the tests being skipped.
92+
8993

9094
Hybrid Mode: Hashes and Images
9195
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -225,6 +229,12 @@ test (based on the hash library) also shown in the generated
225229
summary. This option is applied automatically when generating
226230
a HTML summary.
227231

232+
When the ``--mpl-results-always`` option is active, and some hash
233+
comparison tests are performed, a hash library containing all the
234+
result hashes will also be saved to the root of the results directory.
235+
The filename will be extracted from ``--mpl-generate-hash-library``,
236+
``--mpl-hash-library`` or ``hash_library=`` in that order.
237+
228238
Base style
229239
^^^^^^^^^^
230240

pytest_mpl/plugin.py

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,6 @@ def pytest_configure(config):
181181
if generate_dir is not None:
182182
if baseline_dir is not None:
183183
warnings.warn("Ignoring --mpl-baseline-path since --mpl-generate-path is set")
184-
if results_dir is not None and generate_dir is not None:
185-
warnings.warn("Ignoring --mpl-result-path since --mpl-generate-path is set")
186184

187185
if baseline_dir is not None and not baseline_dir.startswith(("https", "http")):
188186
baseline_dir = os.path.abspath(baseline_dir)
@@ -283,6 +281,12 @@ def __init__(self,
283281
self.results_dir = Path(tempfile.mkdtemp(dir=self.results_dir))
284282
self.results_dir.mkdir(parents=True, exist_ok=True)
285283

284+
# Decide what to call the downloadable results hash library
285+
if self.hash_library is not None:
286+
self.results_hash_library_name = self.hash_library.name
287+
else: # Use the first filename encountered in a `hash_library=` kwarg
288+
self.results_hash_library_name = None
289+
286290
# We need global state to store all the hashes generated over the run
287291
self._generated_hash_library = {}
288292
self._test_results = {}
@@ -390,11 +394,14 @@ def generate_baseline_image(self, item, fig):
390394
if not os.path.exists(self.generate_dir):
391395
os.makedirs(self.generate_dir)
392396

393-
fig.savefig(str((self.generate_dir / self.generate_filename(item)).absolute()),
394-
**savefig_kwargs)
397+
baseline_filename = self.generate_filename(item)
398+
baseline_path = (self.generate_dir / baseline_filename).absolute()
399+
fig.savefig(str(baseline_path), **savefig_kwargs)
395400

396401
close_mpl_figure(fig)
397402

403+
return baseline_path
404+
398405
def generate_image_hash(self, item, fig):
399406
"""
400407
For a `matplotlib.figure.Figure`, returns the SHA256 hash as a hexadecimal
@@ -435,6 +442,7 @@ def compare_image_to_baseline(self, item, fig, result_dir, summary=None):
435442

436443
if not os.path.exists(baseline_image_ref):
437444
summary['status'] = 'failed'
445+
summary['image_status'] = 'missing'
438446
error_message = ("Image file not found for comparison test in: \n\t"
439447
f"{self.get_baseline_directory(item)}\n"
440448
"(This is expected for new tests.)\n"
@@ -456,6 +464,7 @@ def compare_image_to_baseline(self, item, fig, result_dir, summary=None):
456464
actual_shape = imread(str(test_image)).shape[:2]
457465
if expected_shape != actual_shape:
458466
summary['status'] = 'failed'
467+
summary['image_status'] = 'diff'
459468
error_message = SHAPE_MISMATCH_ERROR.format(expected_path=baseline_image,
460469
expected_shape=expected_shape,
461470
actual_path=test_image,
@@ -467,10 +476,12 @@ def compare_image_to_baseline(self, item, fig, result_dir, summary=None):
467476
summary['tolerance'] = tolerance
468477
if results is None:
469478
summary['status'] = 'passed'
479+
summary['image_status'] = 'match'
470480
summary['status_msg'] = 'Image comparison passed.'
471481
return None
472482
else:
473483
summary['status'] = 'failed'
484+
summary['image_status'] = 'diff'
474485
summary['rms'] = results['rms']
475486
diff_image = (result_dir / 'result-failed-diff.png').absolute()
476487
summary['diff_image'] = diff_image.relative_to(self.results_dir).as_posix()
@@ -496,6 +507,10 @@ def compare_image_to_hash_library(self, item, fig, result_dir, summary=None):
496507
compare = self.get_compare(item)
497508
savefig_kwargs = compare.kwargs.get('savefig_kwargs', {})
498509

510+
if not self.results_hash_library_name:
511+
# Use hash library name of current test as results hash library name
512+
self.results_hash_library_name = Path(compare.kwargs.get("hash_library", "")).name
513+
499514
hash_library_filename = self.hash_library or compare.kwargs.get('hash_library', None)
500515
hash_library_filename = (Path(item.fspath).parent / hash_library_filename).absolute()
501516

@@ -512,14 +527,17 @@ def compare_image_to_hash_library(self, item, fig, result_dir, summary=None):
512527

513528
if baseline_hash is None: # hash-missing
514529
summary['status'] = 'failed'
530+
summary['hash_status'] = 'missing'
515531
summary['status_msg'] = (f"Hash for test '{hash_name}' not found in {hash_library_filename}. "
516532
f"Generated hash is {test_hash}.")
517533
elif test_hash == baseline_hash: # hash-match
518534
hash_comparison_pass = True
519535
summary['status'] = 'passed'
536+
summary['hash_status'] = 'match'
520537
summary['status_msg'] = 'Test hash matches baseline hash.'
521538
else: # hash-diff
522539
summary['status'] = 'failed'
540+
summary['hash_status'] = 'diff'
523541
summary['status_msg'] = (f"Hash {test_hash} doesn't match hash "
524542
f"{baseline_hash} in library "
525543
f"{hash_library_filename} for test {hash_name}.")
@@ -544,7 +562,8 @@ def compare_image_to_hash_library(self, item, fig, result_dir, summary=None):
544562
except Exception as baseline_error: # Append to test error later
545563
baseline_comparison = str(baseline_error)
546564
else: # Update main summary
547-
for k in ['baseline_image', 'diff_image', 'rms', 'tolerance', 'result_image']:
565+
for k in ['image_status', 'baseline_image', 'diff_image',
566+
'rms', 'tolerance', 'result_image']:
548567
summary[k] = summary[k] or baseline_summary.get(k)
549568

550569
# Append the log from image comparison
@@ -597,9 +616,12 @@ def item_function_wrapper(*args, **kwargs):
597616
remove_ticks_and_titles(fig)
598617

599618
test_name = self.generate_test_name(item)
619+
result_dir = self.make_test_results_dir(item)
600620

601621
summary = {
602622
'status': None,
623+
'image_status': None,
624+
'hash_status': None,
603625
'status_msg': None,
604626
'baseline_image': None,
605627
'diff_image': None,
@@ -614,21 +636,23 @@ def item_function_wrapper(*args, **kwargs):
614636
# reference images or simply running the test.
615637
if self.generate_dir is not None:
616638
summary['status'] = 'skipped'
639+
summary['image_status'] = 'generated'
617640
summary['status_msg'] = 'Skipped test, since generating image.'
618-
self.generate_baseline_image(item, fig)
619-
if self.generate_hash_library is None:
620-
self._test_results[str(pathify(test_name))] = summary
621-
pytest.skip("Skipping test, since generating image.")
641+
generate_image = self.generate_baseline_image(item, fig)
642+
if self.results_always: # Make baseline image available in HTML
643+
result_image = (result_dir / "baseline.png").absolute()
644+
shutil.copy(generate_image, result_image)
645+
summary['baseline_image'] = \
646+
result_image.relative_to(self.results_dir).as_posix()
622647

623648
if self.generate_hash_library is not None:
649+
summary['hash_status'] = 'generated'
624650
image_hash = self.generate_image_hash(item, fig)
625651
self._generated_hash_library[test_name] = image_hash
626-
summary['result_hash'] = image_hash
652+
summary['baseline_hash'] = image_hash
627653

628654
# Only test figures if not generating images
629655
if self.generate_dir is None:
630-
result_dir = self.make_test_results_dir(item)
631-
632656
# Compare to hash library
633657
if self.hash_library or compare.kwargs.get('hash_library', None):
634658
msg = self.compare_image_to_hash_library(item, fig, result_dir, summary=summary)
@@ -645,12 +669,15 @@ def item_function_wrapper(*args, **kwargs):
645669
for image_type in ['baseline_image', 'diff_image', 'result_image']:
646670
summary[image_type] = None # image no longer exists
647671
else:
648-
self._test_results[str(pathify(test_name))] = summary
672+
self._test_results[test_name] = summary
649673
pytest.fail(msg, pytrace=False)
650674

651675
close_mpl_figure(fig)
652676

653-
self._test_results[str(pathify(test_name))] = summary
677+
self._test_results[test_name] = summary
678+
679+
if summary['status'] == 'skipped':
680+
pytest.skip(summary['status_msg'])
654681

655682
if item.cls is not None:
656683
setattr(item.cls, item.function.__name__, item_function_wrapper)
@@ -667,21 +694,36 @@ def pytest_unconfigure(self, config):
667694
"""
668695
Save out the hash library at the end of the run.
669696
"""
697+
result_hash_library = self.results_dir / (self.results_hash_library_name or "temp.json")
670698
if self.generate_hash_library is not None:
671699
hash_library_path = Path(config.rootdir) / self.generate_hash_library
672700
hash_library_path.parent.mkdir(parents=True, exist_ok=True)
673701
with open(hash_library_path, "w") as fp:
674702
json.dump(self._generated_hash_library, fp, indent=2)
703+
if self.results_always: # Make accessible in results directory
704+
# Use same name as generated
705+
result_hash_library = self.results_dir / hash_library_path.name
706+
shutil.copy(hash_library_path, result_hash_library)
707+
elif self.results_always and self.results_hash_library_name:
708+
result_hashes = {k: v['result_hash'] for k, v in self._test_results.items()
709+
if v['result_hash']}
710+
if len(result_hashes) > 0: # At least one hash comparison test
711+
with open(result_hash_library, "w") as fp:
712+
json.dump(result_hashes, fp, indent=2)
675713

676714
if self.generate_summary:
715+
kwargs = {}
677716
if 'json' in self.generate_summary:
678717
summary = self.generate_summary_json()
679718
print(f"A JSON report can be found at: {summary}")
719+
if result_hash_library.exists(): # link to it in the HTML
720+
kwargs["hash_library"] = result_hash_library.name
680721
if 'html' in self.generate_summary:
681-
summary = generate_summary_html(self._test_results, self.results_dir)
722+
summary = generate_summary_html(self._test_results, self.results_dir, **kwargs)
682723
print(f"A summary of the failed tests can be found at: {summary}")
683724
if 'basic-html' in self.generate_summary:
684-
summary = generate_summary_basic_html(self._test_results, self.results_dir)
725+
summary = generate_summary_basic_html(self._test_results, self.results_dir,
726+
**kwargs)
685727
print(f"A summary of the failed tests can be found at: {summary}")
686728

687729

0 commit comments

Comments
 (0)