From 03cf9e42bad7baf3043255238be546ec52d96f57 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 30 Mar 2023 11:07:34 +0100 Subject: [PATCH 01/20] Added support for EPS, PDF, and SVG image comparison --- pytest_mpl/plugin.py | 60 +- tests/baseline/2.0.x/test_format_eps.eps | 780 +++++++++++++++++++++++ tests/baseline/2.0.x/test_format_pdf.pdf | Bin 0 -> 5236 bytes tests/baseline/2.0.x/test_format_png.png | Bin 0 -> 18087 bytes tests/baseline/2.0.x/test_format_svg.svg | 430 +++++++++++++ tests/test_pytest_mpl.py | 47 ++ 6 files changed, 1297 insertions(+), 20 deletions(-) create mode 100644 tests/baseline/2.0.x/test_format_eps.eps create mode 100644 tests/baseline/2.0.x/test_format_pdf.pdf create mode 100644 tests/baseline/2.0.x/test_format_png.png create mode 100644 tests/baseline/2.0.x/test_format_svg.svg diff --git a/pytest_mpl/plugin.py b/pytest_mpl/plugin.py index 13cced37..58783173 100644 --- a/pytest_mpl/plugin.py +++ b/pytest_mpl/plugin.py @@ -52,6 +52,12 @@ Actual shape: {actual_shape} {actual_path}""" +# The following are the subsets of formats supported by the Matplotlib image +# comparison machinery +RASTER_IMAGE_FORMATS = ['png'] +VECTOR_IMAGE_FORMATS = ['eps', 'pdf', 'svg'] +ALL_IMAGE_FORMATS = RASTER_IMAGE_FORMATS + VECTOR_IMAGE_FORMATS + def _hash_file(in_stream): """ @@ -70,8 +76,8 @@ def pathify(path): """ path = Path(path) ext = '' - if path.suffixes[-1] == '.png': - ext = '.png' + if path.suffixes[-1][1:] in ALL_IMAGE_FORMATS: + ext = path.suffixes[-1] path = str(path).split(ext)[0] path = str(path) path = path.replace('[', '_').replace(']', '_') @@ -315,18 +321,24 @@ def __init__(self, self.logger.setLevel(level) self.logger.addHandler(handler) + def _file_extension(self, item): + compare = get_compare(item) + savefig_kwargs = compare.kwargs.get('savefig_kwargs', {}) + return savefig_kwargs.get('format', 'png') + def generate_filename(self, item): """ Given a pytest item, generate the figure filename. """ + ext = self._file_extension(item) if self.config.getini('mpl-use-full-test-name'): - filename = generate_test_name(item) + '.png' + filename = generate_test_name(item) + f'.{ext}' else: compare = get_compare(item) # Find test name to use as plot name filename = compare.kwargs.get('filename', None) if filename is None: - filename = item.name + '.png' + filename = item.name + f'.{ext}' filename = str(pathify(filename)) return filename @@ -465,9 +477,11 @@ def compare_image_to_baseline(self, item, fig, result_dir, summary=None): tolerance = compare.kwargs.get('tolerance', 2) savefig_kwargs = compare.kwargs.get('savefig_kwargs', {}) + ext = self._file_extension(item) + baseline_image_ref = self.obtain_baseline_image(item, result_dir) - test_image = (result_dir / "result.png").absolute() + test_image = (result_dir / f"result.{ext}").absolute() fig.savefig(str(test_image), **savefig_kwargs) summary['result_image'] = test_image.relative_to(self.results_dir).as_posix() @@ -484,24 +498,26 @@ def compare_image_to_baseline(self, item, fig, result_dir, summary=None): # setuptools may put the baseline images in non-accessible places, # copy to our tmpdir to be sure to keep them in case of failure - baseline_image = (result_dir / "baseline.png").absolute() + baseline_image = (result_dir / f"baseline.{ext}").absolute() shutil.copyfile(baseline_image_ref, baseline_image) summary['baseline_image'] = baseline_image.relative_to(self.results_dir).as_posix() # Compare image size ourselves since the Matplotlib # exception is a bit cryptic in this case and doesn't show - # the filenames - expected_shape = imread(str(baseline_image)).shape[:2] - actual_shape = imread(str(test_image)).shape[:2] - if expected_shape != actual_shape: - summary['status'] = 'failed' - summary['image_status'] = 'diff' - error_message = SHAPE_MISMATCH_ERROR.format(expected_path=baseline_image, - expected_shape=expected_shape, - actual_path=test_image, - actual_shape=actual_shape) - summary['status_msg'] = error_message - return error_message + # the filenames. However imread won't work for vector graphics so we + # only do this for raster files. + if ext in RASTER_IMAGE_FORMATS: + expected_shape = imread(str(baseline_image)).shape[:2] + actual_shape = imread(str(test_image)).shape[:2] + if expected_shape != actual_shape: + summary['status'] = 'failed' + summary['image_status'] = 'diff' + error_message = SHAPE_MISMATCH_ERROR.format(expected_path=baseline_image, + expected_shape=expected_shape, + actual_path=test_image, + actual_shape=actual_shape) + summary['status_msg'] = error_message + return error_message results = compare_images(str(baseline_image), str(test_image), tol=tolerance, in_decorator=True) summary['tolerance'] = tolerance @@ -538,6 +554,8 @@ def compare_image_to_hash_library(self, item, fig, result_dir, summary=None): compare = get_compare(item) savefig_kwargs = compare.kwargs.get('savefig_kwargs', {}) + ext = self._file_extension(item) + if not self.results_hash_library_name: # Use hash library name of current test as results hash library name self.results_hash_library_name = Path(compare.kwargs.get("hash_library", "")).name @@ -574,7 +592,7 @@ def compare_image_to_hash_library(self, item, fig, result_dir, summary=None): f"{hash_library_filename} for test {hash_name}.") # Save the figure for later summary (will be removed later if not needed) - test_image = (result_dir / "result.png").absolute() + test_image = (result_dir / f"result.{ext}").absolute() fig.savefig(str(test_image), **savefig_kwargs) summary['result_image'] = test_image.relative_to(self.results_dir).as_posix() @@ -627,6 +645,8 @@ def pytest_runtest_call(self, item): # noqa remove_text = compare.kwargs.get('remove_text', False) backend = compare.kwargs.get('backend', 'agg') + ext = self._file_extension(item) + with plt.style.context(style, after_reset=True), switch_backend(backend): # Run test and get figure object @@ -665,7 +685,7 @@ def pytest_runtest_call(self, item): # noqa summary['status_msg'] = 'Skipped test, since generating image.' generate_image = self.generate_baseline_image(item, fig) if self.results_always: # Make baseline image available in HTML - result_image = (result_dir / "baseline.png").absolute() + result_image = (result_dir / f"baseline.{ext}").absolute() shutil.copy(generate_image, result_image) summary['baseline_image'] = \ result_image.relative_to(self.results_dir).as_posix() diff --git a/tests/baseline/2.0.x/test_format_eps.eps b/tests/baseline/2.0.x/test_format_eps.eps new file mode 100644 index 00000000..d8438114 --- /dev/null +++ b/tests/baseline/2.0.x/test_format_eps.eps @@ -0,0 +1,780 @@ +%!PS-Adobe-3.0 EPSF-3.0 +%%Title: test_format_eps.eps +%%Creator: Matplotlib v3.6.3, https://matplotlib.org/ +%%CreationDate: Thu Mar 30 10:58:11 2023 +%%Orientation: portrait +%%BoundingBox: 18 180 594 612 +%%HiResBoundingBox: 18.000000 180.000000 594.000000 612.000000 +%%EndComments +%%BeginProlog +/mpldict 11 dict def +mpldict begin +/_d { bind def } bind def +/m { moveto } _d +/l { lineto } _d +/r { rlineto } _d +/c { curveto } _d +/cl { closepath } _d +/ce { closepath eofill } _d +/box { + m + 1 index 0 r + 0 exch r + neg 0 r + cl + } _d +/clipbox { + box + clip + newpath + } _d +/sc { setcachedevice } _d +%!PS-Adobe-3.0 Resource-Font +%%Creator: Converted from TrueType to Type 3 by Matplotlib. +10 dict begin +/FontName /DejaVuSans def +/PaintType 0 def +/FontMatrix [0.00048828125 0 0 0.00048828125 0 0] def +/FontBBox [-2090 -948 3673 2524] def +/FontType 3 def +/Encoding [/period /zero /one /two /three /five] def +/CharStrings 7 dict dup begin +/.notdef 0 def +/period{651 0 219 0 430 254 sc +219 254 m +430 254 l +430 0 l +219 0 l +219 254 l + +ce} _d +/zero{1303 0 135 -29 1167 1520 sc +651 1360 m +547 1360 469 1309 416 1206 c +364 1104 338 950 338 745 c +338 540 364 387 416 284 c +469 182 547 131 651 131 c +756 131 834 182 886 284 c +939 387 965 540 965 745 c +965 950 939 1104 886 1206 c +834 1309 756 1360 651 1360 c + +651 1520 m +818 1520 946 1454 1034 1321 c +1123 1189 1167 997 1167 745 c +1167 494 1123 302 1034 169 c +946 37 818 -29 651 -29 c +484 -29 356 37 267 169 c +179 302 135 494 135 745 c +135 997 179 1189 267 1321 c +356 1454 484 1520 651 1520 c + +ce} _d +/one{1303 0 225 0 1114 1493 sc +254 170 m +584 170 l +584 1309 l +225 1237 l +225 1421 l +582 1493 l +784 1493 l +784 170 l +1114 170 l +1114 0 l +254 0 l +254 170 l + +ce} _d +/two{1303 0 150 0 1098 1520 sc +393 170 m +1098 170 l +1098 0 l +150 0 l +150 170 l +227 249 331 356 463 489 c +596 623 679 709 713 748 c +778 821 823 882 848 932 c +874 983 887 1032 887 1081 c +887 1160 859 1225 803 1275 c +748 1325 675 1350 586 1350 c +523 1350 456 1339 385 1317 c +315 1295 240 1262 160 1217 c +160 1421 l +241 1454 317 1478 388 1495 c +459 1512 523 1520 582 1520 c +737 1520 860 1481 952 1404 c +1044 1327 1090 1223 1090 1094 c +1090 1033 1078 974 1055 919 c +1032 864 991 800 930 725 c +913 706 860 650 771 557 c +682 465 556 336 393 170 c + +ce} _d +/three{1303 0 156 -29 1139 1520 sc +831 805 m +928 784 1003 741 1057 676 c +1112 611 1139 530 1139 434 c +1139 287 1088 173 987 92 c +886 11 742 -29 555 -29 c +492 -29 428 -23 361 -10 c +295 2 227 20 156 45 c +156 240 l +212 207 273 183 340 166 c +407 149 476 141 549 141 c +676 141 772 166 838 216 c +905 266 938 339 938 434 c +938 522 907 591 845 640 c +784 690 698 715 588 715 c +414 715 l +414 881 l +596 881 l +695 881 771 901 824 940 c +877 980 903 1037 903 1112 c +903 1189 876 1247 821 1288 c +767 1329 689 1350 588 1350 c +533 1350 473 1344 410 1332 c +347 1320 277 1301 201 1276 c +201 1456 l +278 1477 349 1493 416 1504 c +483 1515 547 1520 606 1520 c +759 1520 881 1485 970 1415 c +1059 1346 1104 1252 1104 1133 c +1104 1050 1080 980 1033 923 c +986 866 918 827 831 805 c + +ce} _d +/five{1303 0 158 -29 1124 1493 sc +221 1493 m +1014 1493 l +1014 1323 l +406 1323 l +406 957 l +435 967 465 974 494 979 c +523 984 553 987 582 987 c +749 987 881 941 978 850 c +1075 759 1124 635 1124 479 c +1124 318 1074 193 974 104 c +874 15 733 -29 551 -29 c +488 -29 424 -24 359 -13 c +294 -2 227 14 158 35 c +158 238 l +218 205 280 181 344 165 c +408 149 476 141 547 141 c +662 141 754 171 821 232 c +888 293 922 375 922 479 c +922 583 888 665 821 726 c +754 787 662 817 547 817 c +493 817 439 811 385 799 c +332 787 277 768 221 743 c +221 1493 l + +ce} _d +end readonly def + +/BuildGlyph { + exch begin + CharStrings exch + 2 copy known not {pop /.notdef} if + true 3 1 roll get exec + end +} _d + +/BuildChar { + 1 index /Encoding get exch get + 1 index /BuildGlyph get exec +} _d + +FontName currentdict end definefont pop +end +%%EndProlog +mpldict begin +18 180 translate +576 432 0 0 clipbox +gsave +0 0 m +576 0 l +576 432 l +0 432 l +cl +1.000 setgray +fill +grestore +gsave +72 43.2 m +518.4 43.2 l +518.4 388.8 l +72 388.8 l +cl +1.000 setgray +fill +grestore +1.000 setlinewidth +1 setlinejoin +2 setlinecap +[] 0 setdash +0.000 0.000 1.000 setrgbcolor +gsave +446.4 345.6 72 43.2 clipbox +72 43.2 m +295.2 216 l +518.4 388.8 l +stroke +grestore +0 setlinejoin +0.000 setgray +gsave +72 43.2 m +72 388.8 l +stroke +grestore +gsave +518.4 43.2 m +518.4 388.8 l +stroke +grestore +gsave +72 43.2 m +518.4 43.2 l +stroke +grestore +gsave +72 388.8 m +518.4 388.8 l +stroke +grestore +0.500 setlinewidth +1 setlinejoin +0 setlinecap +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 4 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +72 43.2 o +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 -4 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +72 388.8 o +grestore +/DejaVuSans 12.000 selectfont +gsave + +62.4531 30.075 translate +0 rotate +0 0 m /zero glyphshow +7.63477 0 m /period glyphshow +11.4492 0 m /zero glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 4 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +183.6 43.2 o +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 -4 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +183.6 388.8 o +grestore +/DejaVuSans 12.000 selectfont +gsave + +174.053 30.075 translate +0 rotate +0 0 m /zero glyphshow +7.63477 0 m /period glyphshow +11.4492 0 m /five glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 4 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +295.2 43.2 o +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 -4 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +295.2 388.8 o +grestore +/DejaVuSans 12.000 selectfont +gsave + +285.653 30.075 translate +0 rotate +0 0 m /one glyphshow +7.63477 0 m /period glyphshow +11.4492 0 m /zero glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 4 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +406.8 43.2 o +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 -4 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +406.8 388.8 o +grestore +/DejaVuSans 12.000 selectfont +gsave + +397.253 30.075 translate +0 rotate +0 0 m /one glyphshow +7.63477 0 m /period glyphshow +11.4492 0 m /five glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 4 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +518.4 43.2 o +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 -4 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +518.4 388.8 o +grestore +/DejaVuSans 12.000 selectfont +gsave + +508.853 30.075 translate +0 rotate +0 0 m /two glyphshow +7.63477 0 m /period glyphshow +11.4492 0 m /zero glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +4 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +72 43.2 o +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +-0 0 m +-4 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +518.4 43.2 o +grestore +/DejaVuSans 12.000 selectfont +gsave + +48.9062 39.8875 translate +0 rotate +0 0 m /one glyphshow +7.63477 0 m /period glyphshow +11.4492 0 m /zero glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +4 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +72 129.6 o +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +-0 0 m +-4 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +518.4 129.6 o +grestore +/DejaVuSans 12.000 selectfont +gsave + +48.9062 126.288 translate +0 rotate +0 0 m /one glyphshow +7.63477 0 m /period glyphshow +11.4492 0 m /five glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +4 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +72 216 o +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +-0 0 m +-4 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +518.4 216 o +grestore +/DejaVuSans 12.000 selectfont +gsave + +48.9062 212.688 translate +0 rotate +0 0 m /two glyphshow +7.63477 0 m /period glyphshow +11.4492 0 m /zero glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +4 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +72 302.4 o +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +-0 0 m +-4 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +518.4 302.4 o +grestore +/DejaVuSans 12.000 selectfont +gsave + +48.9062 299.087 translate +0 rotate +0 0 m /two glyphshow +7.63477 0 m /period glyphshow +11.4492 0 m /five glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +4 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +72 388.8 o +grestore +gsave +/o { +gsave +newpath +translate +0.5 setlinewidth +1 setlinejoin + +0 setlinecap + +-0 0 m +-4 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +518.4 388.8 o +grestore +/DejaVuSans 12.000 selectfont +gsave + +48.9062 385.488 translate +0 rotate +0 0 m /three glyphshow +7.63477 0 m /period glyphshow +11.4492 0 m /zero glyphshow +grestore + +end +showpage diff --git a/tests/baseline/2.0.x/test_format_pdf.pdf b/tests/baseline/2.0.x/test_format_pdf.pdf new file mode 100644 index 0000000000000000000000000000000000000000..04d2a9f13dae2c7e3df0ce8055b2b9aa16f22982 GIT binary patch literal 5236 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 N1QoRdiTe$F{ue%>@NobD literal 0 HcmV?d00001 diff --git a/tests/baseline/2.0.x/test_format_svg.svg b/tests/baseline/2.0.x/test_format_svg.svg new file mode 100644 index 00000000..a43cbac4 --- /dev/null +++ b/tests/baseline/2.0.x/test_format_svg.svg @@ -0,0 +1,430 @@ + + + + + + + + 2023-03-30T10:58:11.674243 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index a9d2cba1..88385b32 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -670,3 +670,50 @@ def test_raises(): result = pytester.runpytest(*runpytest_args) result.assert_outcomes(failed=1) result.stdout.fnmatch_lines("FAILED*ValueError*User code*") + + +@pytest.mark.parametrize("file_format", ['eps', 'pdf', 'png', 'eps']) +def test_formats(pytester, file_format): + """ + Note that we don't test all possible formats as some do not compress well + and would bloat the baseline directory. + """ + pytester.makepyfile( + f""" + import pytest + import matplotlib.pyplot as plt + @pytest.mark.mpl_image_compare(baseline_dir='{baseline_dir_abs}', + tolerance={DEFAULT_TOLERANCE}, + savefig_kwargs={{'format': '{file_format}'}}) + def test_format_{file_format}(): + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + ax.plot([1, 2, 3]) + return fig + """ + ) + result = pytester.runpytest('--mpl') + result.assert_outcomes(passed=1) + + +@pytest.mark.parametrize("file_format", ['eps', 'pdf', 'png', 'eps']) +def test_formats_check_fail(pytester, file_format): + """ + As for test_formats but make sure the tests fail if there are differences + """ + pytester.makepyfile( + f""" + import pytest + import matplotlib.pyplot as plt + @pytest.mark.mpl_image_compare(baseline_dir='{baseline_dir_abs}', + tolerance={DEFAULT_TOLERANCE}, + savefig_kwargs={{'format': '{file_format}'}}) + def test_format_{file_format}(): + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + ax.plot([3, 2, 3]) + return fig + """ + ) + result = pytester.runpytest('--mpl') + result.assert_outcomes(failed=1) From 30d1135f8af411217da868563a42b60978de9d2a Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 30 Mar 2023 11:13:57 +0100 Subject: [PATCH 02/20] Show reason for test being skipped --- tests/test_pytest_mpl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index 88385b32..9608be7c 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -692,7 +692,7 @@ def test_format_{file_format}(): return fig """ ) - result = pytester.runpytest('--mpl') + result = pytester.runpytest('--mpl', '-rs') result.assert_outcomes(passed=1) @@ -715,5 +715,5 @@ def test_format_{file_format}(): return fig """ ) - result = pytester.runpytest('--mpl') + result = pytester.runpytest('--mpl', '-rs') result.assert_outcomes(failed=1) From d5d54be7bb6b93d98d6866bcd63254419abcaf67 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 30 Mar 2023 13:01:36 +0100 Subject: [PATCH 03/20] Fix typo in formats --- tests/test_pytest_mpl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index 9608be7c..eaf7d79f 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -672,7 +672,7 @@ def test_raises(): result.stdout.fnmatch_lines("FAILED*ValueError*User code*") -@pytest.mark.parametrize("file_format", ['eps', 'pdf', 'png', 'eps']) +@pytest.mark.parametrize("file_format", ['eps', 'pdf', 'png', 'svg']) def test_formats(pytester, file_format): """ Note that we don't test all possible formats as some do not compress well @@ -696,7 +696,7 @@ def test_format_{file_format}(): result.assert_outcomes(passed=1) -@pytest.mark.parametrize("file_format", ['eps', 'pdf', 'png', 'eps']) +@pytest.mark.parametrize("file_format", ['eps', 'pdf', 'png', 'svg']) def test_formats_check_fail(pytester, file_format): """ As for test_formats but make sure the tests fail if there are differences From c1eaec033557ae83e0280386d14639f30fed57e1 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 30 Mar 2023 13:03:47 +0100 Subject: [PATCH 04/20] Install ghostscript and inkscape on Linux CI --- .github/workflows/test_and_publish.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test_and_publish.yml b/.github/workflows/test_and_publish.yml index f8fb97b7..ab3dc3a1 100644 --- a/.github/workflows/test_and_publish.yml +++ b/.github/workflows/test_and_publish.yml @@ -17,6 +17,10 @@ jobs: test: uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 with: + libraries: | + apt: + - ghostscript + - inkscape envs: | # Test the oldest and newest configuration on Mac and Windows - macos: py36-test-mpl20 From af15e86a6a586920fe258728be0552413003e778 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 30 Mar 2023 13:26:40 +0100 Subject: [PATCH 05/20] Make testing more robust to presence of gs/inkscape and Matplotlib version --- tests/test_pytest_mpl.py | 46 +++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index eaf7d79f..d4c47a24 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -11,6 +11,8 @@ import pytest from packaging.version import Version +from matplotlib.testing.compare import converter + MPL_VERSION = Version(matplotlib.__version__) baseline_dir = 'baseline' @@ -672,48 +674,44 @@ def test_raises(): result.stdout.fnmatch_lines("FAILED*ValueError*User code*") +@pytest.mark.parametrize('use_hash_library', (False, True)) +@pytest.mark.parametrize('passes', (False, True)) @pytest.mark.parametrize("file_format", ['eps', 'pdf', 'png', 'svg']) -def test_formats(pytester, file_format): +def test_formats(pytester, use_hash_library, passes, file_format): """ Note that we don't test all possible formats as some do not compress well and would bloat the baseline directory. """ - pytester.makepyfile( - f""" - import pytest - import matplotlib.pyplot as plt - @pytest.mark.mpl_image_compare(baseline_dir='{baseline_dir_abs}', - tolerance={DEFAULT_TOLERANCE}, - savefig_kwargs={{'format': '{file_format}'}}) - def test_format_{file_format}(): - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) - ax.plot([1, 2, 3]) - return fig - """ - ) - result = pytester.runpytest('--mpl', '-rs') - result.assert_outcomes(passed=1) + if file_format == 'svg' and MPL_VERSION < Version('3.3'): + pytest.skip('SVG comparison is only supported in Matplotlib 3.3 and above') + + if file_format != 'png' and file_format not in converter: + if file_format == 'svg': + pytest.skip('Comparing SVG files requires inkscape to be installed') + else: + pytest.skip('Comparing EPS and PDF files requires ghostscript to be installed') + + if use_hash_library: + pytest.skip('Using the hash library does not currently work because the hashes are not deterministic') -@pytest.mark.parametrize("file_format", ['eps', 'pdf', 'png', 'svg']) -def test_formats_check_fail(pytester, file_format): - """ - As for test_formats but make sure the tests fail if there are differences - """ pytester.makepyfile( f""" import pytest import matplotlib.pyplot as plt @pytest.mark.mpl_image_compare(baseline_dir='{baseline_dir_abs}', + {f'hash_library="{hash_library}",' if use_hash_library else ''} tolerance={DEFAULT_TOLERANCE}, savefig_kwargs={{'format': '{file_format}'}}) def test_format_{file_format}(): fig = plt.figure() ax = fig.add_subplot(1, 1, 1) - ax.plot([3, 2, 3]) + ax.plot([{1 if passes else 3}, 2, 3]) return fig """ ) result = pytester.runpytest('--mpl', '-rs') - result.assert_outcomes(failed=1) + if passes: + result.assert_outcomes(passed=1) + else: + result.assert_outcomes(failed=1) From 90d91493a97c591ac87ffca4552aee18bd8d6aba Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 30 Mar 2023 13:29:44 +0100 Subject: [PATCH 06/20] Fix pre-commit --- tests/baseline/2.0.x/test_format_svg.svg | 274 +++++++++++------------ tests/test_pytest_mpl.py | 3 +- 2 files changed, 138 insertions(+), 139 deletions(-) diff --git a/tests/baseline/2.0.x/test_format_svg.svg b/tests/baseline/2.0.x/test_format_svg.svg index a43cbac4..4ea32f65 100644 --- a/tests/baseline/2.0.x/test_format_svg.svg +++ b/tests/baseline/2.0.x/test_format_svg.svg @@ -21,54 +21,54 @@ - - - - - - - - @@ -77,8 +77,8 @@ L 0 -4 - @@ -89,32 +89,32 @@ L 0 4 - - @@ -139,29 +139,29 @@ z - @@ -186,18 +186,18 @@ z - @@ -242,28 +242,28 @@ z - @@ -278,8 +278,8 @@ z - @@ -288,8 +288,8 @@ L 4 0 - @@ -380,36 +380,36 @@ L -4 0 - diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index d4c47a24..9100f401 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -9,9 +9,8 @@ import matplotlib.ft2font import matplotlib.pyplot as plt import pytest -from packaging.version import Version - from matplotlib.testing.compare import converter +from packaging.version import Version MPL_VERSION = Version(matplotlib.__version__) From da8364ae994e27dd624ffcb02b9315d78996b9b3 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 30 Mar 2023 19:42:20 +0100 Subject: [PATCH 07/20] Use raw string to fix Windows failures --- tests/test_pytest_mpl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index 9100f401..8abd8e6e 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -698,8 +698,8 @@ def test_formats(pytester, use_hash_library, passes, file_format): f""" import pytest import matplotlib.pyplot as plt - @pytest.mark.mpl_image_compare(baseline_dir='{baseline_dir_abs}', - {f'hash_library="{hash_library}",' if use_hash_library else ''} + @pytest.mark.mpl_image_compare(baseline_dir=r"{baseline_dir_abs}", + {f'hash_library=r"{hash_library}",' if use_hash_library else ''} tolerance={DEFAULT_TOLERANCE}, savefig_kwargs={{'format': '{file_format}'}}) def test_format_{file_format}(): From 90dde3fb57170af1094d48cd933f5eb435775c6a Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 30 Mar 2023 21:40:51 +0100 Subject: [PATCH 08/20] Convert files to PNG before computing hash --- pytest_mpl/plugin.py | 19 ++++++++++++++----- tests/test_pytest_mpl.py | 3 --- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pytest_mpl/plugin.py b/pytest_mpl/plugin.py index 58783173..2c017e1c 100644 --- a/pytest_mpl/plugin.py +++ b/pytest_mpl/plugin.py @@ -453,12 +453,21 @@ def generate_image_hash(self, item, fig): compare = get_compare(item) savefig_kwargs = compare.kwargs.get('savefig_kwargs', {}) - imgdata = io.BytesIO() - - fig.savefig(imgdata, **savefig_kwargs) + ext = self._file_extension(item) - out = _hash_file(imgdata) - imgdata.close() + if ext == 'png': + imgdata = io.BytesIO() + fig.savefig(imgdata, **savefig_kwargs) + out = _hash_file(imgdata) + imgdata.close() + else: + # Always convert to PNG to compute hash as some vector graphics + # outputs cannot be made deterministic + from matplotlib.testing.compare import convert as convert_to_png + img_filename = tempfile.mktemp() + f'.{ext}' + fig.savefig(img_filename, **savefig_kwargs) + png_filename = convert_to_png(img_filename, cache=True) + out = _hash_file(open(png_filename, 'rb')) close_mpl_figure(fig) return out diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index 8abd8e6e..25c4ad0f 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -691,9 +691,6 @@ def test_formats(pytester, use_hash_library, passes, file_format): else: pytest.skip('Comparing EPS and PDF files requires ghostscript to be installed') - if use_hash_library: - pytest.skip('Using the hash library does not currently work because the hashes are not deterministic') - pytester.makepyfile( f""" import pytest From e35bb47ec0a2c566f0e412874c2f345e3dea0edc Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 30 Mar 2023 22:31:56 +0100 Subject: [PATCH 09/20] Update hashes and try and make PNG output not include Matplotlib version --- tests/baseline/hashes/mpl20_ft261.json | 5 ++++- tests/baseline/hashes/mpl21_ft261.json | 5 ++++- tests/baseline/hashes/mpl22_ft261.json | 5 ++++- tests/baseline/hashes/mpl30_ft261.json | 5 ++++- tests/baseline/hashes/mpl31_ft261.json | 5 ++++- tests/baseline/hashes/mpl32_ft261.json | 5 ++++- tests/baseline/hashes/mpl33_ft261.json | 6 +++++- tests/test_pytest_mpl.py | 3 ++- 8 files changed, 31 insertions(+), 8 deletions(-) diff --git a/tests/baseline/hashes/mpl20_ft261.json b/tests/baseline/hashes/mpl20_ft261.json index 76322063..d2cfe010 100644 --- a/tests/baseline/hashes/mpl20_ft261.json +++ b/tests/baseline/hashes/mpl20_ft261.json @@ -12,5 +12,8 @@ "test_pytest_mpl.test_hash_succeeds": "480062c2239ed9d70e361d1a5b578dc2aa756971161ac6e7287b492ae6118c59", "test.test_modified": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b", "test.test_new": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b", - "test.test_unmodified": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b" + "test.test_unmodified": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b", + "test_formats.test_format_eps": "da67357559501ff4afae72acede897d75d968ba0c4573dcd7ff992cfa72e49d4", + "test_formats.test_format_pdf": "9098ce368ec6e20951e9305fcb7da05a3f10169d270c84d8da3331371c6596bc", + "test_formats.test_format_png": "480062c2239ed9d70e361d1a5b578dc2aa756971161ac6e7287b492ae6118c59" } diff --git a/tests/baseline/hashes/mpl21_ft261.json b/tests/baseline/hashes/mpl21_ft261.json index 8b2beb5a..ed592685 100644 --- a/tests/baseline/hashes/mpl21_ft261.json +++ b/tests/baseline/hashes/mpl21_ft261.json @@ -12,5 +12,8 @@ "test_pytest_mpl.test_hash_succeeds": "17b65dd0247b0dfd8c1b4b079352414ae0fe03c0a3e79d63c8b8670d84d4098f", "test.test_modified": "14d326881467bc613e6504b87bd7d556a5e58668ff16b896fa3c15745cfb6336", "test.test_new": "14d326881467bc613e6504b87bd7d556a5e58668ff16b896fa3c15745cfb6336", - "test.test_unmodified": "14d326881467bc613e6504b87bd7d556a5e58668ff16b896fa3c15745cfb6336" + "test.test_unmodified": "14d326881467bc613e6504b87bd7d556a5e58668ff16b896fa3c15745cfb6336", + "test_formats.test_format_eps": "da67357559501ff4afae72acede897d75d968ba0c4573dcd7ff992cfa72e49d4", + "test_formats.test_format_pdf": "9098ce368ec6e20951e9305fcb7da05a3f10169d270c84d8da3331371c6596bc", + "test_formats.test_format_png": "17b65dd0247b0dfd8c1b4b079352414ae0fe03c0a3e79d63c8b8670d84d4098f" } diff --git a/tests/baseline/hashes/mpl22_ft261.json b/tests/baseline/hashes/mpl22_ft261.json index 08c26f4d..c4399308 100644 --- a/tests/baseline/hashes/mpl22_ft261.json +++ b/tests/baseline/hashes/mpl22_ft261.json @@ -12,5 +12,8 @@ "test_pytest_mpl.test_hash_succeeds": "e80557c8784fb920fb79b03b26dc072649a98811f00a8c212df8761e4351acde", "test.test_modified": "80e0ee6df7cf7d9d9407395a25af30beb8763e98820a7be972764899246d2cd7", "test.test_new": "80e0ee6df7cf7d9d9407395a25af30beb8763e98820a7be972764899246d2cd7", - "test.test_unmodified": "80e0ee6df7cf7d9d9407395a25af30beb8763e98820a7be972764899246d2cd7" + "test.test_unmodified": "80e0ee6df7cf7d9d9407395a25af30beb8763e98820a7be972764899246d2cd7", + "test_formats.test_format_eps": "da67357559501ff4afae72acede897d75d968ba0c4573dcd7ff992cfa72e49d4", + "test_formats.test_format_pdf": "9098ce368ec6e20951e9305fcb7da05a3f10169d270c84d8da3331371c6596bc", + "test_formats.test_format_png": "e80557c8784fb920fb79b03b26dc072649a98811f00a8c212df8761e4351acde" } diff --git a/tests/baseline/hashes/mpl30_ft261.json b/tests/baseline/hashes/mpl30_ft261.json index 313078ea..4d892daf 100644 --- a/tests/baseline/hashes/mpl30_ft261.json +++ b/tests/baseline/hashes/mpl30_ft261.json @@ -12,5 +12,8 @@ "test_pytest_mpl.test_hash_succeeds": "4e1157a93733cdb327f1741afdb0525f4d0e3f12e60b54f72c93db9f9c9ae27f", "test.test_modified": "6e2e4ba7b77caf62df24f6b92d6fc51ab1b837bf98039750334f65c0a6c5d898", "test.test_new": "6e2e4ba7b77caf62df24f6b92d6fc51ab1b837bf98039750334f65c0a6c5d898", - "test.test_unmodified": "6e2e4ba7b77caf62df24f6b92d6fc51ab1b837bf98039750334f65c0a6c5d898" + "test.test_unmodified": "6e2e4ba7b77caf62df24f6b92d6fc51ab1b837bf98039750334f65c0a6c5d898", + "test_formats.test_format_eps": "8ad5b5795b65e16790d1b57889540597fe18f164ddc38f50086f29e725b5c223", + "test_formats.test_format_pdf": "18858c67f1362e6d53975b66ffc221bda0c9f714185851fbf714bd73f0a80729", + "test_formats.test_format_png": "4e1157a93733cdb327f1741afdb0525f4d0e3f12e60b54f72c93db9f9c9ae27f" } diff --git a/tests/baseline/hashes/mpl31_ft261.json b/tests/baseline/hashes/mpl31_ft261.json index dbde4d54..29e4d9bc 100644 --- a/tests/baseline/hashes/mpl31_ft261.json +++ b/tests/baseline/hashes/mpl31_ft261.json @@ -12,5 +12,8 @@ "test_pytest_mpl.test_hash_succeeds": "2a4da3a36b384df539f3f47d476f67a918f5eee1df360dbab9469b96260df78f", "test.test_modified": "3675e5a48388e8cc341580e9b41115d3cf63d2465cf11eeed3faa23e84030fc2", "test.test_new": "3675e5a48388e8cc341580e9b41115d3cf63d2465cf11eeed3faa23e84030fc2", - "test.test_unmodified": "3675e5a48388e8cc341580e9b41115d3cf63d2465cf11eeed3faa23e84030fc2" + "test.test_unmodified": "3675e5a48388e8cc341580e9b41115d3cf63d2465cf11eeed3faa23e84030fc2", + "test_formats.test_format_eps": "8ad5b5795b65e16790d1b57889540597fe18f164ddc38f50086f29e725b5c223", + "test_formats.test_format_pdf": "18858c67f1362e6d53975b66ffc221bda0c9f714185851fbf714bd73f0a80729", + "test_formats.test_format_png": "2a4da3a36b384df539f3f47d476f67a918f5eee1df360dbab9469b96260df78f" } diff --git a/tests/baseline/hashes/mpl32_ft261.json b/tests/baseline/hashes/mpl32_ft261.json index c39964b0..ef97789d 100644 --- a/tests/baseline/hashes/mpl32_ft261.json +++ b/tests/baseline/hashes/mpl32_ft261.json @@ -12,5 +12,8 @@ "test_pytest_mpl.test_hash_succeeds": "8b8ff9ce044bc9075876278781667a708414460209bba25a39d8262ed73d0f04", "test.test_modified": "3b7db65812fd59403d17a2fba3ebe1fd0abdfde8633df06636e4e1daea259da0", "test.test_new": "3b7db65812fd59403d17a2fba3ebe1fd0abdfde8633df06636e4e1daea259da0", - "test.test_unmodified": "3b7db65812fd59403d17a2fba3ebe1fd0abdfde8633df06636e4e1daea259da0" + "test.test_unmodified": "3b7db65812fd59403d17a2fba3ebe1fd0abdfde8633df06636e4e1daea259da0", + "test_formats.test_format_eps": "8ad5b5795b65e16790d1b57889540597fe18f164ddc38f50086f29e725b5c223", + "test_formats.test_format_pdf": "18858c67f1362e6d53975b66ffc221bda0c9f714185851fbf714bd73f0a80729", + "test_formats.test_format_png": "8b8ff9ce044bc9075876278781667a708414460209bba25a39d8262ed73d0f04" } diff --git a/tests/baseline/hashes/mpl33_ft261.json b/tests/baseline/hashes/mpl33_ft261.json index 77c5f03f..30f71940 100644 --- a/tests/baseline/hashes/mpl33_ft261.json +++ b/tests/baseline/hashes/mpl33_ft261.json @@ -12,5 +12,9 @@ "test_pytest_mpl.test_hash_succeeds": "55ad74a44c99606f1df1e79f67a59a4986bddc2b48ea2b2d7ea8365db91dc7ca", "test.test_modified": "ce07de6b726c3b01afb03aa7c9e939d584bc71a54b9737d69853a0d915cd6181", "test.test_new": "ce07de6b726c3b01afb03aa7c9e939d584bc71a54b9737d69853a0d915cd6181", - "test.test_unmodified": "ce07de6b726c3b01afb03aa7c9e939d584bc71a54b9737d69853a0d915cd6181" + "test.test_unmodified": "ce07de6b726c3b01afb03aa7c9e939d584bc71a54b9737d69853a0d915cd6181", + "test_formats.test_format_eps": "8ad5b5795b65e16790d1b57889540597fe18f164ddc38f50086f29e725b5c223", + "test_formats.test_format_pdf": "18858c67f1362e6d53975b66ffc221bda0c9f714185851fbf714bd73f0a80729", + "test_formats.test_format_png": "55ad74a44c99606f1df1e79f67a59a4986bddc2b48ea2b2d7ea8365db91dc7ca", + "test_formats.test_format_svg": "f21b81fdc6163a13caf7fa675d42b996ab7f0070c063a287a00d13d50c5b38f1" } diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index 25c4ad0f..1da90582 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -698,7 +698,8 @@ def test_formats(pytester, use_hash_library, passes, file_format): @pytest.mark.mpl_image_compare(baseline_dir=r"{baseline_dir_abs}", {f'hash_library=r"{hash_library}",' if use_hash_library else ''} tolerance={DEFAULT_TOLERANCE}, - savefig_kwargs={{'format': '{file_format}'}}) + savefig_kwargs={{'format': '{file_format}', + 'metadata': {{"Software": None}}}}) def test_format_{file_format}(): fig = plt.figure() ax = fig.add_subplot(1, 1, 1) From 761f1e737ca6bf1dc271fc7adb321923c31dd591 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 30 Mar 2023 22:46:23 +0100 Subject: [PATCH 10/20] Updated PNG hashes and only set metdata for PNG output --- tests/baseline/hashes/mpl21_ft261.json | 2 +- tests/baseline/hashes/mpl22_ft261.json | 2 +- tests/baseline/hashes/mpl30_ft261.json | 2 +- tests/baseline/hashes/mpl31_ft261.json | 2 +- tests/baseline/hashes/mpl32_ft261.json | 2 +- tests/baseline/hashes/mpl33_ft261.json | 2 +- tests/test_pytest_mpl.py | 7 ++++++- 7 files changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/baseline/hashes/mpl21_ft261.json b/tests/baseline/hashes/mpl21_ft261.json index ed592685..d66db851 100644 --- a/tests/baseline/hashes/mpl21_ft261.json +++ b/tests/baseline/hashes/mpl21_ft261.json @@ -15,5 +15,5 @@ "test.test_unmodified": "14d326881467bc613e6504b87bd7d556a5e58668ff16b896fa3c15745cfb6336", "test_formats.test_format_eps": "da67357559501ff4afae72acede897d75d968ba0c4573dcd7ff992cfa72e49d4", "test_formats.test_format_pdf": "9098ce368ec6e20951e9305fcb7da05a3f10169d270c84d8da3331371c6596bc", - "test_formats.test_format_png": "17b65dd0247b0dfd8c1b4b079352414ae0fe03c0a3e79d63c8b8670d84d4098f" + "test_formats.test_format_png": "d577a3c8c7550413d8d50bc26a68f3e8d9c35d4763c52cbcc15df4f61c8406b2" } diff --git a/tests/baseline/hashes/mpl22_ft261.json b/tests/baseline/hashes/mpl22_ft261.json index c4399308..d857166f 100644 --- a/tests/baseline/hashes/mpl22_ft261.json +++ b/tests/baseline/hashes/mpl22_ft261.json @@ -15,5 +15,5 @@ "test.test_unmodified": "80e0ee6df7cf7d9d9407395a25af30beb8763e98820a7be972764899246d2cd7", "test_formats.test_format_eps": "da67357559501ff4afae72acede897d75d968ba0c4573dcd7ff992cfa72e49d4", "test_formats.test_format_pdf": "9098ce368ec6e20951e9305fcb7da05a3f10169d270c84d8da3331371c6596bc", - "test_formats.test_format_png": "e80557c8784fb920fb79b03b26dc072649a98811f00a8c212df8761e4351acde" + "test_formats.test_format_png": "d577a3c8c7550413d8d50bc26a68f3e8d9c35d4763c52cbcc15df4f61c8406b2" } diff --git a/tests/baseline/hashes/mpl30_ft261.json b/tests/baseline/hashes/mpl30_ft261.json index 4d892daf..03ae2345 100644 --- a/tests/baseline/hashes/mpl30_ft261.json +++ b/tests/baseline/hashes/mpl30_ft261.json @@ -15,5 +15,5 @@ "test.test_unmodified": "6e2e4ba7b77caf62df24f6b92d6fc51ab1b837bf98039750334f65c0a6c5d898", "test_formats.test_format_eps": "8ad5b5795b65e16790d1b57889540597fe18f164ddc38f50086f29e725b5c223", "test_formats.test_format_pdf": "18858c67f1362e6d53975b66ffc221bda0c9f714185851fbf714bd73f0a80729", - "test_formats.test_format_png": "4e1157a93733cdb327f1741afdb0525f4d0e3f12e60b54f72c93db9f9c9ae27f" + "test_formats.test_format_png": "d577a3c8c7550413d8d50bc26a68f3e8d9c35d4763c52cbcc15df4f61c8406b2" } diff --git a/tests/baseline/hashes/mpl31_ft261.json b/tests/baseline/hashes/mpl31_ft261.json index 29e4d9bc..65d73113 100644 --- a/tests/baseline/hashes/mpl31_ft261.json +++ b/tests/baseline/hashes/mpl31_ft261.json @@ -15,5 +15,5 @@ "test.test_unmodified": "3675e5a48388e8cc341580e9b41115d3cf63d2465cf11eeed3faa23e84030fc2", "test_formats.test_format_eps": "8ad5b5795b65e16790d1b57889540597fe18f164ddc38f50086f29e725b5c223", "test_formats.test_format_pdf": "18858c67f1362e6d53975b66ffc221bda0c9f714185851fbf714bd73f0a80729", - "test_formats.test_format_png": "2a4da3a36b384df539f3f47d476f67a918f5eee1df360dbab9469b96260df78f" + "test_formats.test_format_png": "d577a3c8c7550413d8d50bc26a68f3e8d9c35d4763c52cbcc15df4f61c8406b2" } diff --git a/tests/baseline/hashes/mpl32_ft261.json b/tests/baseline/hashes/mpl32_ft261.json index ef97789d..fdc15c9f 100644 --- a/tests/baseline/hashes/mpl32_ft261.json +++ b/tests/baseline/hashes/mpl32_ft261.json @@ -15,5 +15,5 @@ "test.test_unmodified": "3b7db65812fd59403d17a2fba3ebe1fd0abdfde8633df06636e4e1daea259da0", "test_formats.test_format_eps": "8ad5b5795b65e16790d1b57889540597fe18f164ddc38f50086f29e725b5c223", "test_formats.test_format_pdf": "18858c67f1362e6d53975b66ffc221bda0c9f714185851fbf714bd73f0a80729", - "test_formats.test_format_png": "8b8ff9ce044bc9075876278781667a708414460209bba25a39d8262ed73d0f04" + "test_formats.test_format_png": "d577a3c8c7550413d8d50bc26a68f3e8d9c35d4763c52cbcc15df4f61c8406b2" } diff --git a/tests/baseline/hashes/mpl33_ft261.json b/tests/baseline/hashes/mpl33_ft261.json index 30f71940..bdb8b9f0 100644 --- a/tests/baseline/hashes/mpl33_ft261.json +++ b/tests/baseline/hashes/mpl33_ft261.json @@ -15,6 +15,6 @@ "test.test_unmodified": "ce07de6b726c3b01afb03aa7c9e939d584bc71a54b9737d69853a0d915cd6181", "test_formats.test_format_eps": "8ad5b5795b65e16790d1b57889540597fe18f164ddc38f50086f29e725b5c223", "test_formats.test_format_pdf": "18858c67f1362e6d53975b66ffc221bda0c9f714185851fbf714bd73f0a80729", - "test_formats.test_format_png": "55ad74a44c99606f1df1e79f67a59a4986bddc2b48ea2b2d7ea8365db91dc7ca", + "test_formats.test_format_png": "e73d228183ddfdced366191399cdecef9685d1248b852162f179750fc7b8b904", "test_formats.test_format_svg": "f21b81fdc6163a13caf7fa675d42b996ab7f0070c063a287a00d13d50c5b38f1" } diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index 1da90582..ec4b2f4f 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -691,6 +691,11 @@ def test_formats(pytester, use_hash_library, passes, file_format): else: pytest.skip('Comparing EPS and PDF files requires ghostscript to be installed') + if file_format == 'png': + metadata = '{"Software": None}' + else: + metadata = '{}' + pytester.makepyfile( f""" import pytest @@ -699,7 +704,7 @@ def test_formats(pytester, use_hash_library, passes, file_format): {f'hash_library=r"{hash_library}",' if use_hash_library else ''} tolerance={DEFAULT_TOLERANCE}, savefig_kwargs={{'format': '{file_format}', - 'metadata': {{"Software": None}}}}) + 'metadata': {metadata}}}) def test_format_{file_format}(): fig = plt.figure() ax = fig.add_subplot(1, 1, 1) From 65f54bb1c654d8d04dde779feb55089f4d2be2da Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 30 Mar 2023 22:57:23 +0100 Subject: [PATCH 11/20] Skip hash library tests for Matplotlib >=3.4 --- tests/test_pytest_mpl.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index ec4b2f4f..916c51fc 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -685,6 +685,9 @@ def test_formats(pytester, use_hash_library, passes, file_format): if file_format == 'svg' and MPL_VERSION < Version('3.3'): pytest.skip('SVG comparison is only supported in Matplotlib 3.3 and above') + if use_hash_library and MPL_VERSION >= Version('3.4'): + pytest.skip('No hash library for Matplotlib >= 3.4') + if file_format != 'png' and file_format not in converter: if file_format == 'svg': pytest.skip('Comparing SVG files requires inkscape to be installed') From c95621ed9483d8036af50b37020e86ec8ad25b84 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 30 Mar 2023 23:04:16 +0100 Subject: [PATCH 12/20] Fix SVG hash --- tests/baseline/hashes/mpl33_ft261.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/baseline/hashes/mpl33_ft261.json b/tests/baseline/hashes/mpl33_ft261.json index bdb8b9f0..eaedd74e 100644 --- a/tests/baseline/hashes/mpl33_ft261.json +++ b/tests/baseline/hashes/mpl33_ft261.json @@ -16,5 +16,5 @@ "test_formats.test_format_eps": "8ad5b5795b65e16790d1b57889540597fe18f164ddc38f50086f29e725b5c223", "test_formats.test_format_pdf": "18858c67f1362e6d53975b66ffc221bda0c9f714185851fbf714bd73f0a80729", "test_formats.test_format_png": "e73d228183ddfdced366191399cdecef9685d1248b852162f179750fc7b8b904", - "test_formats.test_format_svg": "f21b81fdc6163a13caf7fa675d42b996ab7f0070c063a287a00d13d50c5b38f1" + "test_formats.test_format_svg": "0c0515081a578b3673aafb99ac389e292d125793fa1e3c848524c4fe1484dc2c" } From adcce3c8fe6c0dedc8067da86281352e81de92a2 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 30 Mar 2023 23:06:33 +0100 Subject: [PATCH 13/20] Skip hash comparison for vector graphics not on Linux --- tests/test_pytest_mpl.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index 916c51fc..85828fa7 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -688,6 +688,9 @@ def test_formats(pytester, use_hash_library, passes, file_format): if use_hash_library and MPL_VERSION >= Version('3.4'): pytest.skip('No hash library for Matplotlib >= 3.4') + if use_hash_library and file_format != 'png' and not sys.platform.startswith('linux'): + pytest.skip('Hashes for vector graphics are only provided in the hash library for Linux') + if file_format != 'png' and file_format not in converter: if file_format == 'svg': pytest.skip('Comparing SVG files requires inkscape to be installed') From 28b171ee9d648c16b9e9f3fe2856b75265bf2466 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 31 Mar 2023 10:44:12 +0100 Subject: [PATCH 14/20] Don't convert to PNG for hash calculation --- pytest_mpl/plugin.py | 17 ++++------------- tests/test_pytest_mpl.py | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/pytest_mpl/plugin.py b/pytest_mpl/plugin.py index 2c017e1c..17cce480 100644 --- a/pytest_mpl/plugin.py +++ b/pytest_mpl/plugin.py @@ -455,19 +455,10 @@ def generate_image_hash(self, item, fig): ext = self._file_extension(item) - if ext == 'png': - imgdata = io.BytesIO() - fig.savefig(imgdata, **savefig_kwargs) - out = _hash_file(imgdata) - imgdata.close() - else: - # Always convert to PNG to compute hash as some vector graphics - # outputs cannot be made deterministic - from matplotlib.testing.compare import convert as convert_to_png - img_filename = tempfile.mktemp() + f'.{ext}' - fig.savefig(img_filename, **savefig_kwargs) - png_filename = convert_to_png(img_filename, cache=True) - out = _hash_file(open(png_filename, 'rb')) + imgdata = io.BytesIO() + fig.savefig(imgdata, **savefig_kwargs) + out = _hash_file(imgdata) + imgdata.close() close_mpl_figure(fig) return out diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index 85828fa7..fbe33d18 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -699,11 +699,16 @@ def test_formats(pytester, use_hash_library, passes, file_format): if file_format == 'png': metadata = '{"Software": None}' - else: - metadata = '{}' + elif file_format == 'pdf': + metadata = '{"Creator": None, "Producer": None, "CreationDate": None}' + elif file_format == 'eps': + metadata = '{"Creator": None}' + elif file_format == 'svg': + metadata = '{"Date": None}' pytester.makepyfile( f""" + import os import pytest import matplotlib.pyplot as plt @pytest.mark.mpl_image_compare(baseline_dir=r"{baseline_dir_abs}", @@ -712,6 +717,13 @@ def test_formats(pytester, use_hash_library, passes, file_format): savefig_kwargs={{'format': '{file_format}', 'metadata': {metadata}}}) def test_format_{file_format}(): + + # For reproducible EPS output + os.environ['SOURCE_DATE_EPOCH'] = '1680254601' + + # For reproducible SVG output + plt.rcParams['svg.hashsalt'] = 'test' + fig = plt.figure() ax = fig.add_subplot(1, 1, 1) ax.plot([{1 if passes else 3}, 2, 3]) From 28c0a0142c42d1cd269f85b5f069d75e655b3f6c Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 31 Mar 2023 10:58:40 +0100 Subject: [PATCH 15/20] Added docs about deterministic figures --- README.rst | 57 +++++++++++++++++++++++++- tests/baseline/hashes/mpl20_ft261.json | 4 +- tests/test_pytest_mpl.py | 2 +- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 21d02a62..a95e41b2 100644 --- a/README.rst +++ b/README.rst @@ -90,7 +90,6 @@ When generating a hash library, the tests will also be run as usual against the existing hash library specified by ``--mpl-hash-library`` or the keyword argument. However, generating baseline images will always result in the tests being skipped. - Hybrid Mode: Hashes and Images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -278,6 +277,62 @@ decorator: This will make the test insensitive to changes in e.g. the freetype library. +Supported formats and deterministic output +------------------------------------------ + +By default, pytest-mpl will save and compare figures in PNG format. However, +it is possible to set the format to use by setting e.g. ``savefig_kwargs={'format': 'pdf'}`` +in ``mpl_image_compare``. Supported formats are ``'eps'``, ``'pdf'``, ``'png'``, and ``'svg'``. +Note that Ghostscript is required to be installed for comparing PDF and EPS figures, while +Inkscape is required for SVG comparison. + +By default, Matplotlib does not produce deterministic output that will +have a consistent hash every time it is run, or over different Matplotlib versions. +In order to enforce that the output is deterministic, you will need to do the following: + +PNG +^^^ + +For PNG files, the output can be made deterministic by setting:: + + @pytest.mark.mpl_image_compare(savefig_kwargs={'metadata': {"Software": None}}) + +PDF +^^^ + +For PDF files, the output can be made deterministic by setting:: + + @pytest.mark.mpl_image_compare(savefig_kwargs={'format': 'pdf', + 'metadata': {"Creator": None, + "Producer": None, + "CreationDate": None}}) + +EPS +^^^ + +For PDF files, the output can be made deterministic by setting:: + + @pytest.mark.mpl_image_compare(savefig_kwargs={'format': 'pdf', + 'metadata': {"Creator": "test"}) + +and in addition you will need to set the SOURCE_DATE_EPOCH environment variable to +a constant value (this is a unit timestamp):: + + os.environ['SOURCE_DATE_EPOCH'] = '1680254601' + +You could do this inside the test. + +SVG +^^^ + +For SVG files, the output can be made deterministic by setting:: + + @pytest.mark.mpl_image_compare(savefig_kwargs={'metadata': '{"Date": None}}) + +and in addition, you should make sure the following rcParam is set to a constant string:: + + plt.rcParams['svg.hashsalt'] = 'test' + Test failure example -------------------- diff --git a/tests/baseline/hashes/mpl20_ft261.json b/tests/baseline/hashes/mpl20_ft261.json index d2cfe010..e13f564a 100644 --- a/tests/baseline/hashes/mpl20_ft261.json +++ b/tests/baseline/hashes/mpl20_ft261.json @@ -13,7 +13,7 @@ "test.test_modified": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b", "test.test_new": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b", "test.test_unmodified": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b", - "test_formats.test_format_eps": "da67357559501ff4afae72acede897d75d968ba0c4573dcd7ff992cfa72e49d4", - "test_formats.test_format_pdf": "9098ce368ec6e20951e9305fcb7da05a3f10169d270c84d8da3331371c6596bc", + "test_formats.test_format_eps": "396d67b9a51519a312903e817211abef713151225deeb6735cca6059ace3ca4e", + "test_formats.test_format_pdf": "a73ea1bd1a9edc68da280240a080c3b8ae11a3b1170d27f7229e95ba3019c098", "test_formats.test_format_png": "480062c2239ed9d70e361d1a5b578dc2aa756971161ac6e7287b492ae6118c59" } diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index fbe33d18..1496a0e8 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -702,7 +702,7 @@ def test_formats(pytester, use_hash_library, passes, file_format): elif file_format == 'pdf': metadata = '{"Creator": None, "Producer": None, "CreationDate": None}' elif file_format == 'eps': - metadata = '{"Creator": None}' + metadata = '{"Creator": "test"}' elif file_format == 'svg': metadata = '{"Date": None}' From faee7af7c293c16b7ef2ceae717b22c90de729fd Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 31 Mar 2023 11:04:53 +0100 Subject: [PATCH 16/20] Update some hashes --- tests/baseline/hashes/mpl31_ft261.json | 4 ++-- tests/baseline/hashes/mpl32_ft261.json | 4 ++-- tests/baseline/hashes/mpl33_ft261.json | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/baseline/hashes/mpl31_ft261.json b/tests/baseline/hashes/mpl31_ft261.json index 65d73113..18a1fabf 100644 --- a/tests/baseline/hashes/mpl31_ft261.json +++ b/tests/baseline/hashes/mpl31_ft261.json @@ -13,7 +13,7 @@ "test.test_modified": "3675e5a48388e8cc341580e9b41115d3cf63d2465cf11eeed3faa23e84030fc2", "test.test_new": "3675e5a48388e8cc341580e9b41115d3cf63d2465cf11eeed3faa23e84030fc2", "test.test_unmodified": "3675e5a48388e8cc341580e9b41115d3cf63d2465cf11eeed3faa23e84030fc2", - "test_formats.test_format_eps": "8ad5b5795b65e16790d1b57889540597fe18f164ddc38f50086f29e725b5c223", - "test_formats.test_format_pdf": "18858c67f1362e6d53975b66ffc221bda0c9f714185851fbf714bd73f0a80729", + "test_formats.test_format_eps": "108a341ce450cb5adef9f41e27175da1809fcdeb64f17b13f58d3eb0efc08006", + "test_formats.test_format_pdf": "8963ba9209080091c0961553bdf195cdcd0f2ba29081a122f9aad8e94c444aff", "test_formats.test_format_png": "d577a3c8c7550413d8d50bc26a68f3e8d9c35d4763c52cbcc15df4f61c8406b2" } diff --git a/tests/baseline/hashes/mpl32_ft261.json b/tests/baseline/hashes/mpl32_ft261.json index fdc15c9f..3cb208ef 100644 --- a/tests/baseline/hashes/mpl32_ft261.json +++ b/tests/baseline/hashes/mpl32_ft261.json @@ -13,7 +13,7 @@ "test.test_modified": "3b7db65812fd59403d17a2fba3ebe1fd0abdfde8633df06636e4e1daea259da0", "test.test_new": "3b7db65812fd59403d17a2fba3ebe1fd0abdfde8633df06636e4e1daea259da0", "test.test_unmodified": "3b7db65812fd59403d17a2fba3ebe1fd0abdfde8633df06636e4e1daea259da0", - "test_formats.test_format_eps": "8ad5b5795b65e16790d1b57889540597fe18f164ddc38f50086f29e725b5c223", - "test_formats.test_format_pdf": "18858c67f1362e6d53975b66ffc221bda0c9f714185851fbf714bd73f0a80729", + "test_formats.test_format_eps": "76d0a064029518d1e72c1ff5f159c5e9c3ca77ecb2c7fbab82229aacf61aa4fc", + "test_formats.test_format_pdf": "8963ba9209080091c0961553bdf195cdcd0f2ba29081a122f9aad8e94c444aff", "test_formats.test_format_png": "d577a3c8c7550413d8d50bc26a68f3e8d9c35d4763c52cbcc15df4f61c8406b2" } diff --git a/tests/baseline/hashes/mpl33_ft261.json b/tests/baseline/hashes/mpl33_ft261.json index eaedd74e..b8568f2c 100644 --- a/tests/baseline/hashes/mpl33_ft261.json +++ b/tests/baseline/hashes/mpl33_ft261.json @@ -13,8 +13,8 @@ "test.test_modified": "ce07de6b726c3b01afb03aa7c9e939d584bc71a54b9737d69853a0d915cd6181", "test.test_new": "ce07de6b726c3b01afb03aa7c9e939d584bc71a54b9737d69853a0d915cd6181", "test.test_unmodified": "ce07de6b726c3b01afb03aa7c9e939d584bc71a54b9737d69853a0d915cd6181", - "test_formats.test_format_eps": "8ad5b5795b65e16790d1b57889540597fe18f164ddc38f50086f29e725b5c223", - "test_formats.test_format_pdf": "18858c67f1362e6d53975b66ffc221bda0c9f714185851fbf714bd73f0a80729", + "test_formats.test_format_eps": "4a605a2cd24101b9292151f5ab6d6846ba1b9c856cfda2bee6a142380e257b04", + "test_formats.test_format_pdf": "34a9eb10372b35c0bd26472e8571a91031c055ab47cc3682ebc0c5e47c2b6cbd", "test_formats.test_format_png": "e73d228183ddfdced366191399cdecef9685d1248b852162f179750fc7b8b904", - "test_formats.test_format_svg": "0c0515081a578b3673aafb99ac389e292d125793fa1e3c848524c4fe1484dc2c" + "test_formats.test_format_svg": "b7f85f4b44e0c5871f5cc230b5a9042f2e73aa70384ab584d6cd8cde29344cd2" } From 6d4e40ecbf6692b15e23e0ad0619ae19e36c33ae Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 31 Mar 2023 11:13:45 +0100 Subject: [PATCH 17/20] Update remaining hashes --- tests/baseline/hashes/mpl20_ft261.json | 4 ++-- tests/baseline/hashes/mpl21_ft261.json | 4 ++-- tests/baseline/hashes/mpl22_ft261.json | 4 ++-- tests/baseline/hashes/mpl30_ft261.json | 4 ++-- tests/test_pytest_mpl.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/baseline/hashes/mpl20_ft261.json b/tests/baseline/hashes/mpl20_ft261.json index e13f564a..3c36dfc9 100644 --- a/tests/baseline/hashes/mpl20_ft261.json +++ b/tests/baseline/hashes/mpl20_ft261.json @@ -13,7 +13,7 @@ "test.test_modified": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b", "test.test_new": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b", "test.test_unmodified": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b", - "test_formats.test_format_eps": "396d67b9a51519a312903e817211abef713151225deeb6735cca6059ace3ca4e", - "test_formats.test_format_pdf": "a73ea1bd1a9edc68da280240a080c3b8ae11a3b1170d27f7229e95ba3019c098", + "test_formats.test_format_eps": "d43d276873ee3a46e4dfc86feba5b814a76fa72163dfc6e9624a2750ff625f2f", + "test_formats.test_format_pdf": "917a633ed43b288ecd4ccad64c2f61eee5f2a1b6383a808572dd5a63671298f6", "test_formats.test_format_png": "480062c2239ed9d70e361d1a5b578dc2aa756971161ac6e7287b492ae6118c59" } diff --git a/tests/baseline/hashes/mpl21_ft261.json b/tests/baseline/hashes/mpl21_ft261.json index d66db851..caa4f644 100644 --- a/tests/baseline/hashes/mpl21_ft261.json +++ b/tests/baseline/hashes/mpl21_ft261.json @@ -13,7 +13,7 @@ "test.test_modified": "14d326881467bc613e6504b87bd7d556a5e58668ff16b896fa3c15745cfb6336", "test.test_new": "14d326881467bc613e6504b87bd7d556a5e58668ff16b896fa3c15745cfb6336", "test.test_unmodified": "14d326881467bc613e6504b87bd7d556a5e58668ff16b896fa3c15745cfb6336", - "test_formats.test_format_eps": "da67357559501ff4afae72acede897d75d968ba0c4573dcd7ff992cfa72e49d4", - "test_formats.test_format_pdf": "9098ce368ec6e20951e9305fcb7da05a3f10169d270c84d8da3331371c6596bc", + "test_formats.test_format_eps": "f8a0fbb33dcd473ef5cfdd69317db6eb58d64a7f5f3b5072e0de69aa9e201224", + "test_formats.test_format_pdf": "82b2b58fc3a74591c85cdf2f06b2f72dfc154820fca98e7cfd5cb6904ed60b23", "test_formats.test_format_png": "d577a3c8c7550413d8d50bc26a68f3e8d9c35d4763c52cbcc15df4f61c8406b2" } diff --git a/tests/baseline/hashes/mpl22_ft261.json b/tests/baseline/hashes/mpl22_ft261.json index d857166f..92b28d02 100644 --- a/tests/baseline/hashes/mpl22_ft261.json +++ b/tests/baseline/hashes/mpl22_ft261.json @@ -13,7 +13,7 @@ "test.test_modified": "80e0ee6df7cf7d9d9407395a25af30beb8763e98820a7be972764899246d2cd7", "test.test_new": "80e0ee6df7cf7d9d9407395a25af30beb8763e98820a7be972764899246d2cd7", "test.test_unmodified": "80e0ee6df7cf7d9d9407395a25af30beb8763e98820a7be972764899246d2cd7", - "test_formats.test_format_eps": "da67357559501ff4afae72acede897d75d968ba0c4573dcd7ff992cfa72e49d4", - "test_formats.test_format_pdf": "9098ce368ec6e20951e9305fcb7da05a3f10169d270c84d8da3331371c6596bc", + "test_formats.test_format_eps": "f8a0fbb33dcd473ef5cfdd69317db6eb58d64a7f5f3b5072e0de69aa9e201224", + "test_formats.test_format_pdf": "8963ba9209080091c0961553bdf195cdcd0f2ba29081a122f9aad8e94c444aff", "test_formats.test_format_png": "d577a3c8c7550413d8d50bc26a68f3e8d9c35d4763c52cbcc15df4f61c8406b2" } diff --git a/tests/baseline/hashes/mpl30_ft261.json b/tests/baseline/hashes/mpl30_ft261.json index 03ae2345..198ed755 100644 --- a/tests/baseline/hashes/mpl30_ft261.json +++ b/tests/baseline/hashes/mpl30_ft261.json @@ -13,7 +13,7 @@ "test.test_modified": "6e2e4ba7b77caf62df24f6b92d6fc51ab1b837bf98039750334f65c0a6c5d898", "test.test_new": "6e2e4ba7b77caf62df24f6b92d6fc51ab1b837bf98039750334f65c0a6c5d898", "test.test_unmodified": "6e2e4ba7b77caf62df24f6b92d6fc51ab1b837bf98039750334f65c0a6c5d898", - "test_formats.test_format_eps": "8ad5b5795b65e16790d1b57889540597fe18f164ddc38f50086f29e725b5c223", - "test_formats.test_format_pdf": "18858c67f1362e6d53975b66ffc221bda0c9f714185851fbf714bd73f0a80729", + "test_formats.test_format_eps": "f8a0fbb33dcd473ef5cfdd69317db6eb58d64a7f5f3b5072e0de69aa9e201224", + "test_formats.test_format_pdf": "8963ba9209080091c0961553bdf195cdcd0f2ba29081a122f9aad8e94c444aff", "test_formats.test_format_png": "d577a3c8c7550413d8d50bc26a68f3e8d9c35d4763c52cbcc15df4f61c8406b2" } diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index 1496a0e8..9a69c7c9 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -688,7 +688,7 @@ def test_formats(pytester, use_hash_library, passes, file_format): if use_hash_library and MPL_VERSION >= Version('3.4'): pytest.skip('No hash library for Matplotlib >= 3.4') - if use_hash_library and file_format != 'png' and not sys.platform.startswith('linux'): + if use_hash_library and not sys.platform.startswith('linux'): pytest.skip('Hashes for vector graphics are only provided in the hash library for Linux') if file_format != 'png' and file_format not in converter: From 71ced71822f4ea4249d97be85f5fa33ffdcbebf4 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 31 Mar 2023 11:15:43 +0100 Subject: [PATCH 18/20] Fix syntax highlighting --- README.rst | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index a95e41b2..197effc4 100644 --- a/README.rst +++ b/README.rst @@ -293,14 +293,18 @@ In order to enforce that the output is deterministic, you will need to do the fo PNG ^^^ -For PNG files, the output can be made deterministic by setting:: +For PNG files, the output can be made deterministic by setting: + +.. code:: python @pytest.mark.mpl_image_compare(savefig_kwargs={'metadata': {"Software": None}}) PDF ^^^ -For PDF files, the output can be made deterministic by setting:: +For PDF files, the output can be made deterministic by setting: + +.. code:: python @pytest.mark.mpl_image_compare(savefig_kwargs={'format': 'pdf', 'metadata': {"Creator": None, @@ -310,13 +314,17 @@ For PDF files, the output can be made deterministic by setting:: EPS ^^^ -For PDF files, the output can be made deterministic by setting:: +For PDF files, the output can be made deterministic by setting: + +.. code:: python @pytest.mark.mpl_image_compare(savefig_kwargs={'format': 'pdf', 'metadata': {"Creator": "test"}) and in addition you will need to set the SOURCE_DATE_EPOCH environment variable to -a constant value (this is a unit timestamp):: +a constant value (this is a unit timestamp): + +.. code:: python os.environ['SOURCE_DATE_EPOCH'] = '1680254601' @@ -325,11 +333,15 @@ You could do this inside the test. SVG ^^^ -For SVG files, the output can be made deterministic by setting:: +For SVG files, the output can be made deterministic by setting: + +.. code:: python @pytest.mark.mpl_image_compare(savefig_kwargs={'metadata': '{"Date": None}}) -and in addition, you should make sure the following rcParam is set to a constant string:: +and in addition, you should make sure the following rcParam is set to a constant string: + +.. code:: python plt.rcParams['svg.hashsalt'] = 'test' From f1b6cb31124f0b4e50e6ed1d36bba03a8771ebf1 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 31 Mar 2023 11:22:47 +0100 Subject: [PATCH 19/20] Mention minimum Matplotlib versions for different formats and fix test suite --- README.rst | 13 ++++++++++--- tests/baseline/hashes/mpl20_ft261.json | 2 -- tests/test_pytest_mpl.py | 11 +++++++++-- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 197effc4..13e413e0 100644 --- a/README.rst +++ b/README.rst @@ -286,9 +286,10 @@ in ``mpl_image_compare``. Supported formats are ``'eps'``, ``'pdf'``, ``'png'``, Note that Ghostscript is required to be installed for comparing PDF and EPS figures, while Inkscape is required for SVG comparison. -By default, Matplotlib does not produce deterministic output that will -have a consistent hash every time it is run, or over different Matplotlib versions. -In order to enforce that the output is deterministic, you will need to do the following: +By default, Matplotlib does not produce deterministic output that will have a +consistent hash every time it is run, or over different Matplotlib versions. In +order to enforce that the output is deterministic, you will need to set metadata +as described in the following subsections. PNG ^^^ @@ -311,6 +312,8 @@ For PDF files, the output can be made deterministic by setting: "Producer": None, "CreationDate": None}}) +Note that deterministic PDF output can only be achieved with Matplotlib 2.1 and above + EPS ^^^ @@ -330,6 +333,8 @@ a constant value (this is a unit timestamp): You could do this inside the test. +Note that deterministic PDF output can only be achieved with Matplotlib 2.1 and above + SVG ^^^ @@ -345,6 +350,8 @@ and in addition, you should make sure the following rcParam is set to a constant plt.rcParams['svg.hashsalt'] = 'test' +Note that SVG files can only be used in pytest-mpl with Matplotlib 3.3 and above. + Test failure example -------------------- diff --git a/tests/baseline/hashes/mpl20_ft261.json b/tests/baseline/hashes/mpl20_ft261.json index 3c36dfc9..7fe1a4e8 100644 --- a/tests/baseline/hashes/mpl20_ft261.json +++ b/tests/baseline/hashes/mpl20_ft261.json @@ -13,7 +13,5 @@ "test.test_modified": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b", "test.test_new": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b", "test.test_unmodified": "54f6cf83d5b06fa2ecb7fa23d6e87898679178ef5d0dfdd2551a139f1932127b", - "test_formats.test_format_eps": "d43d276873ee3a46e4dfc86feba5b814a76fa72163dfc6e9624a2750ff625f2f", - "test_formats.test_format_pdf": "917a633ed43b288ecd4ccad64c2f61eee5f2a1b6383a808572dd5a63671298f6", "test_formats.test_format_png": "480062c2239ed9d70e361d1a5b578dc2aa756971161ac6e7287b492ae6118c59" } diff --git a/tests/test_pytest_mpl.py b/tests/test_pytest_mpl.py index 9a69c7c9..23cfe783 100644 --- a/tests/test_pytest_mpl.py +++ b/tests/test_pytest_mpl.py @@ -685,8 +685,15 @@ def test_formats(pytester, use_hash_library, passes, file_format): if file_format == 'svg' and MPL_VERSION < Version('3.3'): pytest.skip('SVG comparison is only supported in Matplotlib 3.3 and above') - if use_hash_library and MPL_VERSION >= Version('3.4'): - pytest.skip('No hash library for Matplotlib >= 3.4') + if use_hash_library: + + if file_format == 'pdf' and MPL_VERSION < Version('2.1'): + pytest.skip('PDF hashes are only deterministic in Matplotlib 2.1 and above') + elif file_format == 'eps' and MPL_VERSION < Version('2.1'): + pytest.skip('EPS hashes are only deterministic in Matplotlib 2.1 and above') + + if MPL_VERSION >= Version('3.4'): + pytest.skip('No hash library in test suite for Matplotlib >= 3.4') if use_hash_library and not sys.platform.startswith('linux'): pytest.skip('Hashes for vector graphics are only provided in the hash library for Linux') From 70b5d5539cfd9c8060755bbf278439733bb8741f Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 31 Mar 2023 21:28:54 +0100 Subject: [PATCH 20/20] Fix figures in HTML summary --- pytest_mpl/plugin.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pytest_mpl/plugin.py b/pytest_mpl/plugin.py index 17cce480..4128fd16 100644 --- a/pytest_mpl/plugin.py +++ b/pytest_mpl/plugin.py @@ -483,7 +483,11 @@ def compare_image_to_baseline(self, item, fig, result_dir, summary=None): test_image = (result_dir / f"result.{ext}").absolute() fig.savefig(str(test_image), **savefig_kwargs) - summary['result_image'] = test_image.relative_to(self.results_dir).as_posix() + + if ext in ['png', 'svg']: # Use original file + summary['result_image'] = test_image.relative_to(self.results_dir).as_posix() + else: + summary['result_image'] = (result_dir / f"result_{ext}.png").relative_to(self.results_dir).as_posix() if not os.path.exists(baseline_image_ref): summary['status'] = 'failed' @@ -500,7 +504,11 @@ def compare_image_to_baseline(self, item, fig, result_dir, summary=None): # copy to our tmpdir to be sure to keep them in case of failure baseline_image = (result_dir / f"baseline.{ext}").absolute() shutil.copyfile(baseline_image_ref, baseline_image) - summary['baseline_image'] = baseline_image.relative_to(self.results_dir).as_posix() + + if ext in ['png', 'svg']: # Use original file + summary['baseline_image'] = baseline_image.relative_to(self.results_dir).as_posix() + else: + summary['baseline_image'] = (result_dir / f"baseline_{ext}.png").relative_to(self.results_dir).as_posix() # Compare image size ourselves since the Matplotlib # exception is a bit cryptic in this case and doesn't show @@ -520,6 +528,7 @@ def compare_image_to_baseline(self, item, fig, result_dir, summary=None): return error_message results = compare_images(str(baseline_image), str(test_image), tol=tolerance, in_decorator=True) + summary['tolerance'] = tolerance if results is None: summary['status'] = 'passed' @@ -530,8 +539,7 @@ def compare_image_to_baseline(self, item, fig, result_dir, summary=None): summary['status'] = 'failed' summary['image_status'] = 'diff' summary['rms'] = results['rms'] - diff_image = (result_dir / 'result-failed-diff.png').absolute() - summary['diff_image'] = diff_image.relative_to(self.results_dir).as_posix() + summary['diff_image'] = Path(results['diff']).relative_to(self.results_dir).as_posix() template = ['Error: Image files did not match.', 'RMS Value: {rms}', 'Expected: \n {expected}',