10000 Figure equality-based tests. · matplotlib/matplotlib@91354a1 · GitHub
[go: up one dir, main page]

Skip to content

Commit 91354a1

Browse files
committed
Figure equality-based tests.
Implement a `check_figures_equal` decorator, which allows tests where both the reference and the test image are generated. The idea is to allow tests of the form "this feature should be equivalent to that (usually more complex) way of achieving the same thing" without further bloating the test image directory. The saved images are properly created in the `result_images` folder, but cannot be "accepted" or "rejected" using the triage_tests UI (as there is indeed no reference image to be saved in the repo). Includes an example use case.
1 parent f881e9b commit 91354a1

File tree

3 files changed

+63
-23
lines changed

3 files changed

+63
-23
lines changed

lib/matplotlib/testing/decorators.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,44 @@ def image_comparison(baseline_images, extensions=None, tol=0,
407407
savefig_kwargs=savefig_kwarg, style=style)
408408

409409

410+
def check_figures_equal(*, extensions=("png", "pdf", "svg"), tol=0):
411+
"""
412+
Decorator for test cases that generate and compare two figures.
413+
414+
The decorated function must take two arguments, *fig_test* and *fig_ref*,
415+
and draw the test and reference images on them. After the function
416+
returns, the figures are saved and compared.
417+
418+
Arguments
419+
---------
420+
extensions : list, default: ["png", "pdf", "svg"]
421+
The extensions to test.
422+
tol : float
423+
The RMS threshold above which the test is considered failed.
424+
"""
425+
426+
def decorator(func):
427+
import pytest
428+
429+
_, result_dir = map(Path, _image_directories(func))
430+
431+
@pytest.mark.parametrize("ext", extensions)
432+
def wrapper(ext):
433+
fig_test = plt.figure("test")
434+
fig_ref = plt.figure("reference")
435+
func(fig_test, fig_ref)
436+
test_image_path = result_dir / (func.__name__ + "." + ext)
437+
ref_image_path = result_dir / (func.__name__ + "-expected." + ext)
438+
fig_test.savefig(test_image_path)
439+
fig_ref.savefig(ref_image_path)
440+
_raise_on_image_difference(
441+
str(ref_image_path), str(test_image_path), tol=tol)
442+
443+
return wrapper
444+
445+
return decorator
446+
447+
410448
def _image_directories(func):
411449
"""
412450
Compute the baseline and result image directories for testing *func*.

lib/matplotlib/tests/test_axes.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import warnings
1515

1616
import matplotlib
17-
from matplotlib.testing.decorators import image_comparison
17+
from matplotlib.testing.decorators import image_comparison, check_figures_equal
1818
import matplotlib.pyplot as plt
1919
import matplotlib.markers as mmarkers
2020
import matplotlib.patches as mpatches
@@ -5702,18 +5702,10 @@ def test_plot_columns_cycle_deprecation():
57025702
plt.plot(np.zeros((2, 2)), np.zeros((2, 3)))
57035703

57045704

5705-
def test_markerfacecolor_none_alpha():
5706-
fig1, ax1 = plt.subplots()
5707-
ax1.plot(0, "o", mfc="none", alpha=.5)
5708-
buf1 = io.BytesIO()
5709-
fig1.savefig(buf1)
5710-
5711-
fig2, ax2 = plt.subplots()
5712-
ax2.plot(0, "o", mfc="w", alpha=.5)
5713-
buf2 = io.BytesIO()
5714-
fig2.savefig(buf2)
5715-
5716-
assert buf1.getvalue() == buf2.getvalue()
5705+
@check_figures_equal()
5706+
def test_markerfacecolor_none_alpha(fig_test, fig_ref):
5707+
fig_test.subplots().plot(0, "o", mfc="none", alpha=.5)
5708+
fig_ref.subplots().plot(0, "o", mfc="w", alpha=.5)
57175709

57185710

57195711
def test_tick_padding_tightbbox():

tools/triage_tests.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -192,14 +192,22 @@ def set_large_image(self, index):
192192
self.thumbnails[self.current_thumbnail].setFrameShape(1)
193193

194194
def accept_test(self):
195-
self.entries[self.current_entry].accept()
195+
entry = self.entries[self.current_entry]
196+
if entry.status == 'autogen':
197+
print('Cannot accept autogenerated test cases.')
198+
return
199+
entry.accept()
196200
self.filelist.currentItem().setText(
197201
self.entries[self.current_entry].display)
198202
# Auto-move to the next entry
199203
self.set_entry(min((self.current_entry + 1), len(self.entries) - 1))
200204

201205
def reject_test(self):
202-
self.entries[self.current_entry].reject()
206+
entry = self.entries[self.current_entry]
207+
if entry.status == 'autogen':
208+
print('Cannot reject autogenerated test cases.')
209+
return
210+
entry.reject()
203211
self.filelist.currentItem().setText(
204212
self.entries[self.current_entry].display)
205213
# Auto-move to the next entry
@@ -261,11 +269,14 @@ def __init__(self, path, root, source):
261269
]
262270
self.thumbnails = [os.path.join(self.dir, x) for x in self.thumbnails]
263271

264-
self.status = 'unknown'
265-
266-
if self.same(os.path.join(self.dir, self.generated),
272+
if not Path(self.destdir, self.generated).exists():
273+
# This case arises from a check_figures_equal test.
274+
self.status = 'autogen'
275+
elif self.same(os.path.join(self.dir, self.generated),
267276
os.path.join(self.destdir, self.generated)):
268277
self.status = 'accept'
278+
else:
279+
self.status = 'unknown'
269280

270281
def same(self, a, b):
271282
"""
@@ -299,14 +310,14 @@ def display(self):
299310
"""
300311
status_map = {'unknown': '\N{BALLOT BOX}',
301312
'accept': '\N{BALLOT BOX WITH CHECK}',
302-
'reject': '\N{BALLOT BOX WITH X}'}
313+
'reject': '\N{BALLOT BOX WITH X}',
314+
'autogen': '\N{BALLOT X}'}
303315
box = status_map[self.status]
304316
return '{} {} [{}]'.format(box, self.name, self.extension)
305317

306318
def accept(self):
307319
"""
308-
Accept this test by copying the generated result to the
309-
source tree.
320+
Accept this test by copying the generated result to the source tree.
310321
"""
311322
a = os.path.join(self.dir, self.generated)
312323
b = os.path.join(self.destdir, self.generated)
@@ -315,8 +326,7 @@ def accept(self):
315326

316327
def reject(self):
317328
"""
318-
Reject this test by copying the expected result to the
319-
source tree.
329+
Reject this test by copying the expected result to the source tree.
320330
"""
321331
a = os.path.join(self.dir, self.expected)
322332
b = os.path.join(self.destdir, self.generated)

0 commit comments

Comments
 (0)
0