Skip to content

Commit 94d732c

Browse files
authored
Merge pull request #108 from Cadair/always_compare
Implement always compare mode for hash library
2 parents b145560 + 0f65c8e commit 94d732c

12 files changed

+187
-45
lines changed

README.rst

+25-7
Original file line numberDiff line numberDiff line change
@@ -84,19 +84,19 @@ The hash library can be generated with
8484
``--mpl-generate-hash-library=path_to_file.json``. The hash library to be used
8585
can either be specified via the ``--mpl-hash-library=`` command line argument,
8686
or via the ``hash_library=`` keyword argument to the
87-
``@pytest.mark.mpl_image_comapre`` decorator.
87+
``@pytest.mark.mpl_image_compare`` decorator.
8888

8989

9090
Hybrid Mode: Hashes and Images
9191
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9292

9393
It is possible to configure both hashes and baseline images. In this scenario
94-
the hashes will be compared first, and the baseline images used if the hash
94+
the hashes will be compared first, with the baseline images only used if the hash
9595
comparison fails.
9696

9797
This is especially useful if the baseline images are external to the repository
98-
with the tests in, and can be accessed remotely. In this situation if the hashes
99-
match the baseline images wont be retrieved, saving time and bandwidth. Also it
98+
containing the tests, and are accessed via HTTP. In this situation, if the hashes
99+
match, the baseline images won't be retrieved, saving time and bandwidth. Also, it
100100
allows the tests to be modified and the hashes updated to reflect the changes
101101
without having to modify the external images.
102102

@@ -111,16 +111,16 @@ against, the tests can be run with::
111111

112112
and the tests will pass if the images are the same. If you omit the
113113
``--mpl`` option, the tests will run but will only check that the code
114-
runs without checking the output images.
114+
runs, without checking the output images.
115115

116116

117117
Generating a Failure Summary
118118
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
119119

120-
By specifying the ``--mpl-generate-summary=html`` CLI argument a HTML summary
120+
By specifying the ``--mpl-generate-summary=html`` CLI argument, a HTML summary
121121
page will be generated showing the baseline, diff and result image for each
122122
failing test. If no baseline images are configured, just the result images will
123-
be displayed.
123+
be displayed. (See also, the **Results always** section below.)
124124

125125
Options
126126
-------
@@ -182,6 +182,24 @@ are run. In addition, if both this option and the ``baseline_dir``
182182
option in the ``mpl_image_compare`` decorator are used, the one in the
183183
decorator takes precedence.
184184

185+
Results always
186+
^^^^^^^^^^^^^^
187+
188+
By default, result images are only generated for tests that fail.
189+
Passing ``--mpl-results-always`` to pytest will force result images
190+
to be generated for all tests, even for tests that pass.
191+
If a baseline image exists, a diff image will also be generated.
192+
All of these images will be shown in the summary (if requested).
193+
194+
This option is useful for always *comparing* the result images again
195+
the baseline images, while only *assessing* the tests against the
196+
hash library.
197+
If you only update your baseline images after merging a PR, this
198+
option means that the generated summary will always show how the
199+
PR affects the baseline images, with the success status of each
200+
test (based on the hash library) also shown in the generated
201+
summary.
202+
185203
Base style
186204
^^^^^^^^^^
187205

pytest_mpl/plugin.py

+66-25
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ def pytest_addoption(parser):
155155
results_path_help = "directory for test results, relative to location where py.test is run"
156156
group.addoption('--mpl-results-path', help=results_path_help, action='store')
157157
parser.addini('mpl-results-path', help=results_path_help)
158+
159+
results_always_help = "Always generate result images, not just for failed tests."
160+
group.addoption('--mpl-results-always', action='store_true',
161+
help=results_always_help)
162+
parser.addini('mpl-results-always', help=results_always_help)
163+
158164
parser.addini('mpl-use-full-test-name', help="use fully qualified test name as the filename.",
159165
type='bool')
160166

@@ -175,6 +181,8 @@ def pytest_configure(config):
175181
results_dir = config.getoption("--mpl-results-path") or config.getini("mpl-results-path")
176182
hash_library = config.getoption("--mpl-hash-library")
177183
generate_summary = config.getoption("--mpl-generate-summary")
184+
results_always = (config.getoption("--mpl-results-always") or
185+
config.getini("mpl-results-always"))
178186

179187
if config.getoption("--mpl-baseline-relative"):
180188
baseline_relative_dir = config.getoption("--mpl-baseline-path")
@@ -205,7 +213,8 @@ def pytest_configure(config):
205213
results_dir=results_dir,
206214
hash_library=hash_library,
207215
generate_hash_library=generate_hash_lib,
208-
generate_summary=generate_summary))
216+
generate_summary=generate_summary,
217+
results_always=results_always))
209218

210219
else:
211220

@@ -262,7 +271,8 @@ def __init__(self,
262271
results_dir=None,
263272
hash_library=None,
264273
generate_hash_library=None,
265-
generate_summary=None
274+
generate_summary=None,
275+
results_always=False
266276
):
267277
self.config = config
268278
self.baseline_dir = baseline_dir
@@ -274,6 +284,7 @@ def __init__(self,
274284
if generate_summary and generate_summary.lower() not in ("html",):
275285
raise ValueError(f"The mpl summary type '{generate_summary}' is not supported.")
276286
self.generate_summary = generate_summary
287+
self.results_always = results_always
277288

278289
# Generate the containing dir for all test results
279290
if not self.results_dir:
@@ -282,6 +293,7 @@ def __init__(self,
282293

283294
# We need global state to store all the hashes generated over the run
284295
self._generated_hash_library = {}
296+
self._test_results = {}
285297

286298
def get_compare(self, item):
287299
"""
@@ -389,7 +401,6 @@ def generate_baseline_image(self, item, fig):
389401
**savefig_kwargs)
390402

391403
close_mpl_figure(fig)
392-
pytest.skip("Skipping test, since generating image")
393404

394405
def generate_image_hash(self, item, fig):
395406
"""
@@ -455,6 +466,10 @@ def load_hash_library(self, library_path):
455466
return json.load(fp)
456467

457468
def compare_image_to_hash_library(self, item, fig, result_dir):
469+
new_test = False
470+
hash_comparison_pass = False
471+
baseline_image_path = None
472+
458473
compare = self.get_compare(item)
459474
savefig_kwargs = compare.kwargs.get('savefig_kwargs', {})
460475

@@ -470,41 +485,59 @@ def compare_image_to_hash_library(self, item, fig, result_dir):
470485
test_hash = self.generate_image_hash(item, fig)
471486

472487
if hash_name not in hash_library:
473-
return (f"Hash for test '{hash_name}' not found in {hash_library_filename}. "
474-
f"Generated hash is {test_hash}.")
488+
new_test = True
489+
error_message = (f"Hash for test '{hash_name}' not found in {hash_library_filename}. "
490+
f"Generated hash is {test_hash}.")
475491

476-
if test_hash == hash_library[hash_name]:
477-
return
492+
# Save the figure for later summary (will be removed later if not needed)
493+
test_image = (result_dir / "result.png").absolute()
494+
fig.savefig(str(test_image), **savefig_kwargs)
478495

479-
error_message = (f"Hash {test_hash} doesn't match hash "
480-
f"{hash_library[hash_name]} in library "
481-
f"{hash_library_filename} for test {hash_name}.")
496+
if not new_test:
497+
if test_hash == hash_library[hash_name]:
498+
hash_comparison_pass = True
499+
else:
500+
error_message = (f"Hash {test_hash} doesn't match hash "
501+
f"{hash_library[hash_name]} in library "
502+
f"{hash_library_filename} for test {hash_name}.")
482503

483504
# If the compare has only been specified with hash and not baseline
484505
# dir, don't attempt to find a baseline image at the default path.
485-
if not self.baseline_directory_specified(item):
486-
# Save the figure for later summary
487-
test_image = (result_dir / "result.png").absolute()
488-
fig.savefig(str(test_image), **savefig_kwargs)
506+
if not hash_comparison_pass and not self.baseline_directory_specified(item) or new_test:
489507
return error_message
490508

491-
try:
492-
baseline_image_path = self.obtain_baseline_image(item, result_dir)
493-
baseline_image = baseline_image_path
494-
baseline_image = None if not baseline_image.exists() else baseline_image
495-
except Exception:
496-
baseline_image = None
509+
# If this is not a new test try and get the baseline image.
510+
if not new_test:
511+
baseline_error = None
512+
# Ignore Errors here as it's possible the reference image dosen't exist yet.
513+
try:
514+
baseline_image_path = self.obtain_baseline_image(item, result_dir)
515+
baseline_image = baseline_image_path
516+
if baseline_image and not baseline_image.exists():
517+
baseline_image = None
518+
# Get the baseline and generate a diff image, always so that
519+
# --mpl-results-always can be respected.
520+
baseline_comparison = self.compare_image_to_baseline(item, fig, result_dir)
521+
except Exception as e:
522+
baseline_image = None
523+
baseline_error = e
524+
525+
# If the hash comparison passes then return
526+
if hash_comparison_pass:
527+
return
497528

498529
if baseline_image is None:
499530
error_message += f"\nUnable to find baseline image for {item}."
531+
if baseline_error:
532+
error_message += f"\n{baseline_error}"
500533
return error_message
501534

502535
# Override the tolerance (if not explicitly set) to 0 as the hashes are not forgiving
503536
tolerance = compare.kwargs.get('tolerance', None)
504537
if not tolerance:
505538
compare.kwargs['tolerance'] = 0
506539

507-
comparison_error = (self.compare_image_to_baseline(item, fig, result_dir) or
540+
comparison_error = (baseline_comparison or
508541
"\nHowever, the comparison to the baseline image succeeded.")
509542

510543
return f"{error_message}\n{comparison_error}"
@@ -548,14 +581,17 @@ def item_function_wrapper(*args, **kwargs):
548581
if remove_text:
549582
remove_ticks_and_titles(fig)
550583

584+
test_name = self.generate_test_name(item)
585+
551586
# What we do now depends on whether we are generating the
552587
# reference images or simply running the test.
553588
if self.generate_dir is not None:
554589
self.generate_baseline_image(item, fig)
590+
if self.generate_hash_library is None:
591+
pytest.skip("Skipping test, since generating image.")
555592

556593
if self.generate_hash_library is not None:
557-
hash_name = self.generate_test_name(item)
558-
self._generated_hash_library[hash_name] = self.generate_image_hash(item, fig)
594+
self._generated_hash_library[test_name] = self.generate_image_hash(item, fig)
559595

560596
# Only test figures if not generating images
561597
if self.generate_dir is None:
@@ -571,8 +607,11 @@ def item_function_wrapper(*args, **kwargs):
571607

572608
close_mpl_figure(fig)
573609

610+
self._test_results[str(pathify(test_name))] = msg or True
611+
574612
if msg is None:
575-
shutil.rmtree(result_dir)
613+
if not self.results_always:
614+
shutil.rmtree(result_dir)
576615
else:
577616
pytest.fail(msg, pytrace=False)
578617

@@ -592,8 +631,10 @@ def generate_summary_html(self, dir_list):
592631
f.write(HTML_INTRO)
593632

594633
for directory in dir_list:
634+
test_name = directory.parts[-1]
635+
test_result = 'passed' if self._test_results[test_name] is True else 'failed'
595636
f.write('<tr>'
596-
f'<td>{directory.parts[-1]}\n'
637+
f'<td>{test_name} ({test_result})\n'
597638
f'<td><img src="{directory / "baseline.png"}"></td>\n'
598639
f'<td><img src="{directory / "result-failed-diff.png"}"></td>\n'
599640
f'<td><img src="{directory / "result.png"}"></td>\n'
22.4 KB
Loading
16.5 KB
Loading

tests/baseline/hashes/mpl20_ft2104.json renamed to tests/baseline/hashes/mpl20_ft261.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@
88
"test_pytest_mpl.test_remove_text": "9c284d7bcbbb1d6c1362b417859e4ce842b573a2fe32c7ceaafcf328a1eb7057",
99
"test_pytest_mpl.test_parametrized[5]": "04c998af2d7932ca4a851d610e8a020d94a2f623d1301dbe9b59fe6efd28a5f7",
1010
"test_pytest_mpl.test_parametrized[50]": "937d986ab6b209e7d48eb30cc30e9db62c93bbc4c86768e276a5b454e63bca93",
11-
"test_pytest_mpl.test_parametrized[500]": "e39ed724b0762b8736879801e32dc0c1525afd03c0567a43b119435aaa608498"
12-
}
11+
"test_pytest_mpl.test_parametrized[500]": "e39ed724b0762b8736879801e32dc0c1525afd03c0567a43b119435aaa608498",
12+
"test_pytest_mpl.test_hash_succeeds": "480062c2239ed9d70e361d1a5b578dc2aa756971161ac6e7287b492ae6118c59",
13+
"test.test_modified": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b",
14+
"test.test_new": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b",
15+
"test.test_unmodified": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b"
16+
}

tests/baseline/hashes/mpl21_ft2104.json renamed to tests/baseline/hashes/mpl21_ft261.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@
88
"test_pytest_mpl.test_remove_text": "9c284d7bcbbb1d6c1362b417859e4ce842b573a2fe32c7ceaafcf328a1eb7057",
99
"test_pytest_mpl.test_parametrized[5]": "04c998af2d7932ca4a851d610e8a020d94a2f623d1301dbe9b59fe6efd28a5f7",
1010
"test_pytest_mpl.test_parametrized[50]": "937d986ab6b209e7d48eb30cc30e9db62c93bbc4c86768e276a5b454e63bca93",
11-
"test_pytest_mpl.test_parametrized[500]": "e39ed724b0762b8736879801e32dc0c1525afd03c0567a43b119435aaa608498"
12-
}
11+
"test_pytest_mpl.test_parametrized[500]": "e39ed724b0762b8736879801e32dc0c1525afd03c0567a43b119435aaa608498",
12+
"test_pytest_mpl.test_hash_succeeds": "17b65dd0247b0dfd8c1b4b079352414ae0fe03c0a3e79d63c8b8670d84d4098f",
13+
"test.test_modified": "14d326881467bc613e6504b87bd7d556a5e58668ff16b896fa3c15745cfb6336",
14+
"test.test_new": "14d326881467bc613e6504b87bd7d556a5e58668ff16b896fa3c15745cfb6336",
15+
"test.test_unmodified": "14d326881467bc613e6504b87bd7d556a5e58668ff16b896fa3c15745cfb6336"
16+
}

tests/baseline/hashes/mpl22_ft2104.json renamed to tests/baseline/hashes/mpl22_ft261.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@
88
"test_pytest_mpl.test_remove_text": "9c284d7bcbbb1d6c1362b417859e4ce842b573a2fe32c7ceaafcf328a1eb7057",
99
"test_pytest_mpl.test_parametrized[5]": "04c998af2d7932ca4a851d610e8a020d94a2f623d1301dbe9b59fe6efd28a5f7",
1010
"test_pytest_mpl.test_parametrized[50]": "937d986ab6b209e7d48eb30cc30e9db62c93bbc4c86768e276a5b454e63bca93",
11-
"test_pytest_mpl.test_parametrized[500]": "e39ed724b0762b8736879801e32dc0c1525afd03c0567a43b119435aaa608498"
12-
}
11+
"test_pytest_mpl.test_parametrized[500]": "e39ed724b0762b8736879801e32dc0c1525afd03c0567a43b119435aaa608498",
12+
"test_pytest_mpl.test_hash_succeeds": "e80557c8784fb920fb79b03b26dc072649a98811f00a8c212df8761e4351acde",
13+
"test.test_modified": "80e0ee6df7cf7d9d9407395a25af30beb8763e98820a7be972764899246d2cd7",
14+
"test.test_new": "80e0ee6df7cf7d9d9407395a25af30beb8763e98820a7be972764899246d2cd7",
15+
"test.test_unmodified": "80e0ee6df7cf7d9d9407395a25af30beb8763e98820a7be972764899246d2cd7"
16+
}

tests/baseline/hashes/mpl30_ft2104.json renamed to tests/baseline/hashes/mpl30_ft261.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@
88
"test_pytest_mpl.test_remove_text": "9c284d7bcbbb1d6c1362b417859e4ce842b573a2fe32c7ceaafcf328a1eb7057",
99
"test_pytest_mpl.test_parametrized[5]": "04c998af2d7932ca4a851d610e8a020d94a2f623d1301dbe9b59fe6efd28a5f7",
1010
"test_pytest_mpl.test_parametrized[50]": "937d986ab6b209e7d48eb30cc30e9db62c93bbc4c86768e276a5b454e63bca93",
11-
"test_pytest_mpl.test_parametrized[500]": "e39ed724b0762b8736879801e32dc0c1525afd03c0567a43b119435aaa608498"
12-
}
11+
"test_pytest_mpl.test_parametrized[500]": "e39ed724b0762b8736879801e32dc0c1525afd03c0567a43b119435aaa608498",
12+
"test_pytest_mpl.test_hash_succeeds": "4e1157a93733cdb327f1741afdb0525f4d0e3f12e60b54f72c93db9f9c9ae27f",
13+
"test.test_modified": "6e2e4ba7b77caf62df24f6b92d6fc51ab1b837bf98039750334f65c0a6c5d898",
14+
"test.test_new": "6e2e4ba7b77caf62df24f6b92d6fc51ab1b837bf98039750334f65c0a6c5d898",
15+
"test.test_unmodified": "6e2e4ba7b77caf62df24f6b92d6fc51ab1b837bf98039750334f65c0a6c5d898"
16+
}

tests/baseline/hashes/mpl31_ft261.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@
99
"test_pytest_mpl.test_parametrized[5]": "be7dc9de64a5d6fd458c1f930d4aa56cf8196ddb0e8b5b07ab79a1f0ea9eb820",
1010
"test_pytest_mpl.test_parametrized[50]": "a8ae2427337803dc864784d88c4428a6af5a3e47d2bfc84c98b68b25fde75704",
1111
"test_pytest_mpl.test_parametrized[500]": "590ef42388378173e293bd37e95ff22d8e753d53327d1fb5d6bdf2bac4f84d01",
12-
"test_pytest_mpl.test_hash_succeeds": "2a4da3a36b384df539f3f47d476f67a918f5eee1df360dbab9469b96260df78f"
13-
}
12+
"test_pytest_mpl.test_hash_succeeds": "2a4da3a36b384df539f3f47d476f67a918f5eee1df360dbab9469b96260df78f",
13+
"test.test_modified": "3675e5a48388e8cc341580e9b41115d3cf63d2465cf11eeed3faa23e84030fc2",
14+
"test.test_new": "3675e5a48388e8cc341580e9b41115d3cf63d2465cf11eeed3faa23e84030fc2",
15+
"test.test_unmodified": "3675e5a48388e8cc341580e9b41115d3cf63d2465cf11eeed3faa23e84030fc2"
16+
}

tests/baseline/hashes/mpl32_ft261.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@
99
"test_pytest_mpl.test_parametrized[5]": "9b2b5b1df784c8f9a5fc624840138fe7b4dbdd42cf592fe5733c9c825e5dda91",
1010
"test_pytest_mpl.test_parametrized[50]": "fcf0566ef5514674e2b4bf1e9b4c7f52451c6f98abdc75dc876f43c97a23bc32",
1111
"test_pytest_mpl.test_parametrized[500]": "38dccccfc980b44359bc1b325bef48471bc084db37ed622af00a553792a8b093",
12-
"test_pytest_mpl.test_hash_succeeds": "8b8ff9ce044bc9075876278781667a708414460209bba25a39d8262ed73d0f04"
13-
}
12+
"test_pytest_mpl.test_hash_succeeds": "8b8ff9ce044bc9075876278781667a708414460209bba25a39d8262ed73d0f04",
13+
"test.test_modified": "3b7db65812fd59403d17a2fba3ebe1fd0abdfde8633df06636e4e1daea259da0",
14+
"test.test_new": "3b7db65812fd59403d17a2fba3ebe1fd0abdfde8633df06636e4e1daea259da0",
15+
"test.test_unmodified": "3b7db65812fd59403d17a2fba3ebe1fd0abdfde8633df06636e4e1daea259da0"
16+
}

tests/baseline/hashes/mpl33_ft261.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@
99
"test_pytest_mpl.test_parametrized[5]": "04c998af2d7932ca4a851d610e8a020d94a2f623d1301dbe9b59fe6efd28a5f7",
1010
"test_pytest_mpl.test_parametrized[50]": "937d986ab6b209e7d48eb30cc30e9db62c93bbc4c86768e276a5b454e63bca93",
1111
"test_pytest_mpl.test_parametrized[500]": "e39ed724b0762b8736879801e32dc0c1525afd03c0567a43b119435aaa608498",
12-
"test_pytest_mpl.test_hash_succeeds": "55ad74a44c99606f1df1e79f67a59a4986bddc2b48ea2b2d7ea8365db91dc7ca"
12+
"test_pytest_mpl.test_hash_succeeds": "55ad74a44c99606f1df1e79f67a59a4986bddc2b48ea2b2d7ea8365db91dc7ca",
13+
"test.test_modified": "ce07de6b726c3b01afb03aa7c9e939d584bc71a54b9737d69853a0d915cd6181",
14+
"test.test_new": "ce07de6b726c3b01afb03aa7c9e939d584bc71a54b9737d69853a0d915cd6181",
15+
"test.test_unmodified": "ce07de6b726c3b01afb03aa7c9e939d584bc71a54b9737d69853a0d915cd6181"
1316
}

0 commit comments

Comments
 (0)