8000 Merge pull request #778 from mdboom/tests-faster · matplotlib/matplotlib@1456f05 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1456f05

Browse files
committed
Merge pull request #778 from mdboom/tests-faster
Made tests faster
2 parents 84524eb + 16f450f commit 1456f05

File tree

130 files changed

+54191
-57574
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

130 files changed

+54191
-57574
lines changed

lib/matplotlib/testing/compare.py

Lines changed: 111 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88

99
import matplotlib
1010
from matplotlib.testing.noseclasses import ImageComparisonFailure
11-
from matplotlib.testing import image_util
11+
from matplotlib.testing import image_util, util
1212
from matplotlib import _png
13+
from matplotlib import _get_configdir
14+
from distutils import version
15+
import hashlib
1316
import math
1417
import operator
1518
import os
@@ -28,6 +31,15 @@
2831
]
2932

3033
#-----------------------------------------------------------------------
34+
35+
def make_test_filename(fname, purpose):
36+
"""
37+
Make a new filename by inserting `purpose` before the file's
38+
extension.
39+
"""
40+
base, ext = os.path.splitext(fname)
41+
return '%s-%s%s' % (base, purpose, ext)
42+
3143
def compare_float( expected, actual, relTol = None, absTol = None ):
3244
"""Fail if the floating point values are not close enough, with
3345
the givem message.
@@ -87,35 +99,68 @@ def compare_float( expected, actual, relTol = None, absTol = None ):
8799
# A dictionary that maps filename extensions to functions that map
88100
# parameters old and new to a list that can be passed to Popen to
89101
# convert files with that extension to png format.
102+
def get_cache_dir():
103+
cache_dir = os.path.join(_get_configdir(), 'test_cache')
104+
if not os.path.exists(cache_dir):
105+
try:
106+
os.makedirs(cache_dir)
107+
except IOError:
108+
return None
109+
if not os.access(cache_dir, os.W_OK):
110+
return None
111+
return cache_dir
112+
113+
def get_file_hash(path, block_size=2**20):
114+
md5 = hashlib.md5()
115+
with open(path, 'rb') as fd:
116+
while True:
117+
data = fd.read(block_size)
118+
if not data:
119+
break
120+
md5.update(data)
121+
return md5.hexdigest()
122+
90123
converter = { }
91124

92125
def make_external_conversion_command(cmd):
93-
def convert(*args):
94-
cmdline = cmd(*args)
95-
oldname, newname = args
126+
def convert(old, new):
127+
cmdline = cmd(old, new)
96128
pipe = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
97129
stdout, stderr = pipe.communicate()
98130
errcode = pipe.wait()
99-
if not os.path.exists(newname) or errcode:
131+
if not os.path.exists(new) or errcode:
100132
msg = "Conversion command failed:\n%s\n" % ' '.join(cmdline)
101133
if stdout:
102134
msg += "Standard output:\n%s\n" % stdout
103135
if stderr:
104136
msg += "Standard error:\n%s\n" % stderr
105137
raise IOError(msg)
138+
106139
return convert
107140

108141
if matplotlib.checkdep_ghostscript() is not None:
109-
# FIXME: make checkdep_ghostscript return the command
110-
if sys.platform == 'win32':
111-
gs = 'gswin32c'
112-
else:
113-
gs = 'gs'
114-
cmd = lambda old, new: \
115-
[gs, '-q', '-sDEVICE=png16m', '-dNOPAUSE', '-dBATCH',
116-
'-sOutputFile=' + new, old]
117-
converter['pdf'] = make_external_conversion_command(cmd)
118-
converter['eps'] = make_external_conversion_command(cmd)
142+
def make_ghostscript_conversion_command():
143+
# FIXME: make checkdep_ghostscript return the command
144+
if sys.platform == 'win32':
145+
gs = 'gswin32c'
146+
else:
147+
gs = 'gs'
148+
cmd = [gs, '-q', '-sDEVICE=png16m', '-sOutputFile=-']
149+
150+
process = util.MiniExpect(cmd)
151+
152+
def do_convert(old, new):
153+
process.expect("GS>")
154+
process.sendline("(%s) run" % old)
155+
with open(new, 'wb') as fd:
156+
process.expect(">>showpage, press <return> to continue<<", fd)
157+
process.sendline('')
158+
159+
return do_convert
160+
161+
converter['pdf'] = make_ghostscript_conversion_command()
162+
converter['eps'] = make_ghostscript_conversion_command()
163+
119164

120165
if matplotlib.checkdep_inkscape() is not None:
121166
cmd = lambda old, new: \
@@ -127,7 +172,7 @@ def comparable_formats():
127172
on this system.'''
128173
return ['png'] + converter.keys()
129174

130-
def convert(filename):
175+
def convert(filename, cache):
131176
'''
132177
Convert the named file into a png file.
133178
Returns the name of the created file.
@@ -138,11 +183,29 @@ def convert(filename):
138183
newname = base + '_' + extension + '.png'
139184
if not os.path.exists(filename):
140185
raise IOError("'%s' does not exist" % filename)
186+
141187
# Only convert the file if the destination doesn't already exist or
142188
# is out of date.
143189
if (not os.path.exists(newname) or
144190
os.stat(newname).st_mtime < os.stat(filename).st_mtime):
191+
if cache:
192+
cache_dir = get_cache_dir()
193+
else:
194+
cache_dir = None
195+
196+
if cache_dir is not None:
197+
hash = get_file_hash(filename)
198+
new_ext = os.path.splitext(newname)[1]
199+
cached_file = os.path.join(cache_dir, hash + new_ext)
200+
if os.path.exists(cached_file):
201+
shutil.copyfile(cached_file, newname)
202+
return newname
203+
145204
converter[extension](filename, newname)
205+
206+
if cache_dir is not None:
207+
shutil.copyfile(newname, cached_file)
208+
146209
return newname
147210

148211
verifiers = { }
@@ -206,8 +269,8 @@ def compare_images( expected, actual, tol, in_decorator=False ):
206269
# Convert the image to png
207270
extension = expected.split('.')[-1]
208271
if extension != 'png':
209-
actual = convert(actual)
210-
expected = convert(expected)
272+
actual = convert(actual, False)
273+
expected = convert(expected, True)
211274

212275
# open the image files and remove the alpha channel (if it exists)
213276
expectedImage = _png.read_png_int( expected )
@@ -216,24 +279,42 @@ def compare_images( expected, actual, tol, in_decorator=False ):
216279
actualImage, expectedImage = crop_to_same(actual, actualImage, expected, expectedImage)
217280

218281
# normalize the images
219-
expectedImage = image_util.autocontrast( expectedImage, 2 )
220-
actualImage = image_util.autocontrast( actualImage, 2 )
282+
# expectedImage = image_util.autocontrast( expectedImage, 2 )
283+
# actualImage = image_util.autocontrast( actualImage, 2 )
221284

222285
# compare the resulting image histogram functions
223-
rms = 0
224-
bins = np.arange(257)
225-
for i in xrange(0, 3):
226-
h1p = expectedImage[:,:,i]
227-
h2p = actualImage[:,:,i]
286+
expected_version = version.LooseVersion("1.6")
287+
found_version = version.LooseVersion(np.__version__)
288+
289+
# On Numpy 1.6, we can use bincount with minlength, which is much faster than
290+
# using histogram
291+
if found_version >= expected_version:
292+
rms = 0
293+
294+
for i in xrange(0, 3):
295+
h1p = expectedImage[:,:,i]
296+
h2p = actualImage[:,:,i]
297+
298+
h1h = np.bincount(h1p.ravel(), minlength=256)
299+
h2h = np.bincount(h2p.ravel(), minlength=256)
300+
301+
rms += np.sum(np.power((h1h-h2h), 2))
302+
else:
303+
rms = 0
304+
ns = np.arange(257)
305+
306+
for i in xrange(0, 3):
307+
h1p = expectedImage[:,:,i]
308+
h2p = actualImage[:,:,i]
309+
310+
h1h = np.histogram(h1p, bins=bins)[0]
311+
h2h = np.histogram(h2p, bins=bins)[0]
228312

229-
h1h = np.histogram(h1p, bins=bins)[0]
230-
h2h = np.histogram(h2p, bins=bins)[0]
313+
rms += np.sum(np.power((h1h-h2h), 2))
231314

232-
rms += np.sum(np.power((h1h-h2h), 2))
233315
rms = np.sqrt(rms / (256 * 3))
234316

235-
diff_image = os.path.join(os.path.dirname(actual),
236-
'failed-diff-'+os.path.basename(actual))
317+
diff_image = make_test_filename(actual, 'failed-diff')
237318

238319
if ( (rms / 10000.0) <= tol ):
239320
if os.path.exists(diff_image):

lib/matplotlib/testing/decorators.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import matplotlib
77
import matplotlib.tests
88
import matplotlib.units
9+
from matplotlib import ticker
910
from matplotlib import pyplot as plt
1011
from matplotlib import ft2font
1112
import numpy as np
12-
from matplotlib.testing.compare import comparable_formats, compare_images
13+
from matplotlib.testing.compare import comparable_formats, compare_images, \
14+
make_test_filename
1315
import warnings
1416

1517
def knownfailureif(fail_condition, msg=None, known_exception_class=None ):
@@ -98,6 +100,16 @@ def setup_class(cls):
98100

99101
cls._func()
100102

103+
@staticmethod
104+
def remove_text(figure):
105+
figure.suptitle("")
106+
for ax in figure.get_axes():
107+
ax.set_title("")
108+
ax.xaxis.set_major_formatter(ticker.NullFormatter())
109+
ax.xaxis.set_minor_formatter(ticker.NullFormatter())
110+
ax.yaxis.set_major_formatter(ticker.NullFormatter())
111+
ax.yaxis.set_minor_formatter(ticker.NullFormatter())
112+
101113
def test(self):
102114
baseline_dir, result_dir = _image_directories(self._func)
103115

@@ -114,7 +126,8 @@ def test(self):
114126
orig_expected_fname = os.path.join(baseline_dir, baseline) + '.' + extension
115127
if extension == 'eps' and not os.path.exists(orig_expected_fname):
116128
orig_expected_fname = os.path.join(baseline_dir, baseline) + '.pdf'
117-
expected_fname = os.path.join(result_dir, 'expected-' + os.path.basename(orig_expected_fname))
129+
expected_fname = make_test_filename(os.path.join(
130+
result_dir, os.path.basename(orig_expected_fname)), 'expected')
118131
actual_fname = os.path.join(result_dir, baseline) + '.' + extension
119132
if os.path.exists(orig_expected_fname):
120133
shutil.copyfile(orig_expected_fname, expected_fname)
@@ -126,9 +139,13 @@ def test(self):
126139
will_fail, fail_msg,
127140
known_exception_class=ImageComparisonFailure)
128141
def do_test():
142+
if self._remove_text:
143+
self.remove_text(figure)
144+
129145
figure.savefig(actual_fname)
130146

131-
err = compare_images(expected_fname, actual_fname, self._tol, in_decorator=True)
147+
err = compare_images(expected_fname, actual_fname,
148+
self._tol, in_decorator=True)
132149

133150
try:
134151
if not os.path.exists(expected_fname):
@@ -148,7 +165,8 @@ def do_test():
148165

149166
yield (do_test,)
150167

151-
def image_comparison(baseline_images=None, extensions=None, tol=1e-3, freetype_version=None):
168+
def image_comparison(baseline_images=None, extensions=None, tol=1e-3,
169+
freetype_version=None, remove_text=False):
152170
"""
153171
call signature::
154172
@@ -176,6 +194,11 @@ def image_comparison(baseline_images=None, extensions=None, tol=1e-3, freetype_v
176194
*freetype_version*: str or tuple
177195
The expected freetype version or range of versions for this
178196
test to pass.
197+
198+
*remove_text*: bool
199+
Remove the title and tick text from the figure before
200+
comparison. This does not remove other, more deliberate,
201+
text, such as legends and annotations.
179202
"""
180203

181204
if baseline_images is None:
@@ -207,7 +230,8 @@ def compare_images_decorator(func):
207230
'_baseline_images': baseline_images,
208231
'_extensions': extensions,
209232
'_tol': tol,
210-
'_freetype_version': freetype_version})
233+
'_freetype_version': freetype_version,
234+
'_remove_text': remove_text})
211235

212236
return new_class
213237
return compare_images_decorator
@@ -239,4 +263,3 @@ def _image_directories(func):
239263
os.makedirs(result_dir)
240264

241265
return baseline_dir, result_dir
242-

lib/matplotlib/testing/util.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import subprocess
2+
3+
4+
class MiniExpect:
5+
"""
6+
This is a very basic version of pexpect, providing only the
7+
functionality necessary for the testing framework, built on top of
8+
`subprocess` rather than directly on lower-level calls.
9+
"""
10+
def __init__(self, args):
11+
"""
12+
Start the subprocess so it may start accepting commands.
13+
14+
*args* is a list of commandline arguments to pass to
15+
`subprocess.Popen`.
16+
"""
17+
self._name = args[0]
18+
self._process = subprocess.Popen(
19+
args,
20+
stdin=subprocess.PIPE,
21+
stdout=subprocess.PIPE,
22+
stderr=subprocess.STDOUT)
23+
24+
def check_alive(self):
25+
"""
26+
Raises a RuntimeError if the process is no longer alive.
27+
"""
28+
returncode = self._process.poll()
29+
if returncode is not None:
30+
raise RuntimeError("%s unexpectedly quit" % self._name)
31+
32+
def sendline(self, line):
33+
"""
34+
Send a line to the process.
35+
"""
36+
self.check_alive()
37+
stdin = self._process.stdin
38+
stdin.write(line)
39+
stdin.write('\n')
40+
stdin.flush()
41+
42+
def expect(self, s, output=None):
43+
"""
44+
Wait for the string *s* to appear in the child process's output.
45+
46+
*output* (optional) is a writable file object where all of the
47+
content preceding *s* will be written.
48+
"""
49+
self.check_alive()
50+
read = self._process.stdout.read
51+
pos = 0
52+
buf = ''
53+
while True:
54+
char = read(1)
55+
if not char:
56+
raise IOError("Unexpected end-of-file")
57+
elif char == s[pos]:
58+
buf += char
59+
pos += 1
60+
if pos == len(s):
61+
return
62+
else:
63+
if output is not None:
64+
output.write(buf)
65+
output.write(char)
66+
buf = ''
67+
pos = 0
Binary file not shown.
Loading

0 commit comments

Comments
 (0)
0