Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 1456f05

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

File tree

Expand file treeCollapse file tree

130 files changed

+54191
-57574
lines changed
Filter options

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Dismiss banner
Expand file treeCollapse file tree

130 files changed

+54191
-57574
lines changed

‎lib/matplotlib/testing/compare.py

Copy file name to clipboardExpand all lines: lib/matplotlib/testing/compare.py
+111-30Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/testing/decorators.py
+29-6Lines 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

Copy file name to clipboard
+67Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.