Skip to content

Commit 0083367

Browse files
committed
Factor the converted-image cache out of compare.py
There is a cache of png files keyed by the MD5 hashes of corresponding svg and pdf files, which helps reduce test suite running times for svg and pdf files that stay exactly the same from one run to the next. This patch enables caching of test results, not only expected results, which is only useful if the tests are mostly deterministic (see #7748). It adds reporting of cache misses, which can be helpful in getting tests to stay deterministic, and expiration since the test results are going to change more often than the expected results.
1 parent 1fa4dd7 commit 0083367

File tree

8 files changed

+405
-68
lines changed

8 files changed

+405
-68
lines changed

conftest.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
matplotlib.use('agg')
1111

1212
from matplotlib import default_test_modules
13+
from matplotlib.testing import conversion_cache as ccache
1314

1415

1516
IGNORED_TESTS = {
@@ -62,6 +63,11 @@ def pytest_addoption(parser):
6263

6364
group.addoption('--no-pep8', action='store_true',
6465
help='skip PEP8 compliance tests')
66+
group.addoption("--conversion-cache-max-size", action="store",
67+
help="conversion cache maximum size in bytes")
68+
group.addoption("--conversion-cache-report-misses",
69+
action="store_true",
70+
help="report conversion cache misses")
6571

6672

6773
def pytest_configure(config):
@@ -71,12 +77,30 @@ def pytest_configure(config):
7177
if config.getoption('--no-pep8'):
7278
default_test_modules.remove('matplotlib.tests.test_coding_standards')
7379
IGNORED_TESTS['matplotlib'] += 'test_coding_standards'
80+
max_size = config.getoption('--conversion-cache-max-size')
81+
if max_size is not None:
82+
ccache.conversion_cache = \
83+
ccache.ConversionCache(max_size=int(max_size))
84+
else:
85+
ccache.conversion_cache = ccache.ConversionCache()
7486

7587

7688
def pytest_unconfigure(config):
89+
ccache.conversion_cache.expire()
7790
matplotlib._called_from_pytest = False
7891

7992

93+
def pytest_terminal_summary(terminalreporter):
94+
tr = terminalreporter
95+
data = ccache.conversion_cache.report()
96+
tr.write_sep('-', 'Image conversion cache report')
97+
tr.write_line('Hit rate: %d/%d' % (len(data['hits']), len(data['gets'])))
98+
if tr.config.getoption('--conversion-cache-report-misses'):
99+
tr.write_line('Missed files:')
100+
for filename in sorted(data['gets'].difference(data['hits'])):
101+
tr.write_line(' %s' % filename)
102+
103+
80104
def pytest_ignore_collect(path, config):
81105
if path.ext == '.py':
82106
collect_filter = config.getoption('--collect-filter')

lib/matplotlib/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,6 +1490,7 @@ def _jupyter_nbextension_paths():
14901490
'matplotlib.tests.test_backend_svg',
14911491
'matplotlib.tests.test_basic',
14921492
'matplotlib.tests.test_bbox_tight',
1493+
'matplotlib.tests.test_cache',
14931494
'matplotlib.tests.test_coding_standards',
14941495
'matplotlib.tests.test_collections',
14951496
'matplotlib.tests.test_colorbar',

lib/matplotlib/testing/compare.py

Lines changed: 19 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,14 @@
55
from __future__ import (absolute_import, division, print_function,
66
unicode_literals)
77

8-
import six
9-
10-
import hashlib
118
import os
12-
import shutil
139

1410
import numpy as np
1511

1612
import matplotlib
1713
from matplotlib.compat import subprocess
1814
from matplotlib.testing.exceptions import ImageComparisonFailure
1915
from matplotlib import _png
20-
from matplotlib import _get_cachedir
21-
from matplotlib import cbook
22-
from distutils import version
2316

2417
__all__ = ['compare_float', 'compare_images', 'comparable_formats']
2518

@@ -76,40 +69,6 @@ def compare_float(expected, actual, relTol=None, absTol=None):
7669
return msg or None
7770

7871

79-
def get_cache_dir():
80-
cachedir = _get_cachedir()
81-
if cachedir is None:
82-
raise RuntimeError('Could not find a suitable configuration directory')
83-
cache_dir = os.path.join(cachedir, 'test_cache')
84-
if not os.path.exists(cache_dir):
85-
try:
86-
cbook.mkdirs(cache_dir)
87-
except IOError:
88-
return None
89-
if not os.access(cache_dir, os.W_OK):
90-
return None
91-
return cache_dir
92-
93-
94-
def get_file_hash(path, block_size=2 ** 20):
95-
md5 = hashlib.md5()
96-
with open(path, 'rb') as fd:
97-
while True:
98-
data = fd.read(block_size)
99-
if not data:
100-
break
101-
md5.update(data)
102-
103-
if path.endswith('.pdf'):
104-
from matplotlib import checkdep_ghostscript
105-
md5.update(checkdep_ghostscript()[1].encode('utf-8'))
106-
elif path.endswith('.svg'):
107-
from matplotlib import checkdep_inkscape
108-
md5.update(checkdep_inkscape().encode('utf-8'))
109-
110-
return md5.hexdigest()
111-
112-
11372
def make_external_conversion_command(cmd):
11473
def convert(old, new):
11574
cmdline = cmd(old, new)
@@ -160,16 +119,20 @@ def comparable_formats():
160119
return ['png'] + list(converter)
161120

162121

163-
def convert(filename, cache):
122+
def convert(filename, cache=None):
164123
"""
165124
Convert the named file into a png file. Returns the name of the
166125
created file.
167126
168-
If *cache* is True, the result of the conversion is cached in
169-
`matplotlib._get_cachedir() + '/test_cache/'`. The caching is based
170-
on a hash of the exact contents of the input file. The is no limit
171-
on the size of the cache, so it may need to be manually cleared
172-
periodically.
127+
Parameters
128+
----------
129+
filename : str
130+
cache : ConversionCache, optional
131+
132+
Returns
133+
-------
134+
str
135+
The converted file.
173136
174137
"""
175138
base, extension = filename.rsplit('.', 1)
@@ -184,23 +147,12 @@ def convert(filename, cache):
184147
# is out of date.
185148
if (not os.path.exists(newname) or
186149
os.stat(newname).st_mtime < os.stat(filename).st_mtime):
187-
if cache:
188-
cache_dir = get_cache_dir()
189-
else:
190-
cache_dir = None
191-
192-
if cache_dir is not None:
193-
hash_value = get_file_hash(filename)
194-
new_ext = os.path.splitext(newname)[1]
195-
cached_file = os.path.join(cache_dir, hash_value + new_ext)
196-
if os.path.exists(cached_file):
197-
shutil.copyfile(cached_file, newname)
198-
return newname
199-
150+
in_cache = cache and cache.get(filename, newname)
151+
if in_cache:
152+
return newname
200153
converter[extension](filename, newname)
201-
202-
if cache_dir is not None:
203-
shutil.copyfile(newname, cached_file)
154+
if cache:
155+
cache.put(filename, newname)
204156

205157
return newname
206158

@@ -262,7 +214,7 @@ def calculate_rms(expectedImage, actualImage):
262214
return rms
263215

264216

265-
def compare_images(expected, actual, tol, in_decorator=False):
217+
def compare_images(expected, actual, tol, in_decorator=False, cache=None):
266218
"""
267219
Compare two "image" files checking differences within a tolerance.
268220
@@ -283,6 +235,7 @@ def compare_images(expected, actual, tol, in_decorator=False):
283235
in_decorator : bool
284236
If called from image_comparison decorator, this should be
285237
True. (default=False)
238+
cache : cache.ConversionCache, optional
286239
287240
Example
288241
-------
@@ -308,8 +261,8 @@ def compare_images(expected, actual, tol, in_decorator=False):
308261
raise IOError('Baseline image %r does not exist.' % expected)
309262

310263
if extension != 'png':
311-
actual = convert(actual, False)
312-
expected = convert(expected, True)
264+
actual = convert(actual, cache)
265+
expected = convert(expected, cache)
313266

314267
# open the image files and remove the alpha channel (if it exists)
315268
expectedImage = _png.read_png_int(expected)

0 commit comments

Comments
 (0)