24
24
from matplotlib import ticker
25
25
from matplotlib import pyplot as plt
26
26
from matplotlib import ft2font
27
- from matplotlib import rcParams
28
27
from matplotlib .testing .compare import comparable_formats , compare_images , \
29
28
make_test_filename
30
- from . import copy_metadata , is_called_from_pytest , skip , xfail
29
+ from . import copy_metadata , is_called_from_pytest , xfail
31
30
from .exceptions import ImageComparisonFailure
32
31
33
32
@@ -59,7 +58,10 @@ def knownfailureif(fail_condition, msg=None, known_exception_class=None):
59
58
"""
60
59
if is_called_from_pytest ():
61
60
import pytest
62
- strict = fail_condition and fail_condition != 'indeterminate'
61
+ if fail_condition == 'indeterminate' :
62
+ fail_condition , strict = True , False
63
+ else :
64
+ fail_condition , strict = bool (fail_condition ), True
63
65
return pytest .mark .xfail (condition = fail_condition , reason = msg ,
64
66
raises = known_exception_class , strict = strict )
65
67
else :
@@ -173,98 +175,171 @@ def check_freetype_version(ver):
173
175
return found >= ver [0 ] and found <= ver [1 ]
174
176
175
177
176
- class ImageComparisonTest (CleanupTest ):
177
- @classmethod
178
- def setup_class (cls ):
179
- CleanupTest .setup_class ()
178
+ def checked_on_freetype_version (required_freetype_version ):
179
+ if check_freetype_version (required_freetype_version ):
180
+ return lambda f : f
181
+
182
+ reason = ("Mismatched version of freetype. "
183
+ "Test requires '%s', you have '%s'" %
184
+ (required_freetype_version , ft2font .__freetype_version__ ))
185
+ return knownfailureif ('indeterminate' , msg = reason ,
186
+ known_exception_class = ImageComparisonFailure )
187
+
188
+
189
+ def remove_ticks_and_titles (figure ):
190
+ figure .suptitle ("" )
191
+ null_formatter = ticker .NullFormatter ()
192
+ for ax in figure .get_axes ():
193
+ ax .set_title ("" )
194
+ ax .xaxis .set_major_formatter (null_formatter )
195
+ ax .xaxis .set_minor_formatter (null_formatter )
196
+ ax .yaxis .set_major_formatter (null_formatter )
197
+ ax .yaxis .set_minor_formatter (null_formatter )
180
198
try :
181
- matplotlib .style .use (cls ._style )
199
+ ax .zaxis .set_major_formatter (null_formatter )
200
+ ax .zaxis .set_minor_formatter (null_formatter )
201
+ except AttributeError :
202
+ pass
203
+
204
+
205
+ def raise_on_image_difference (expected , actual , tol ):
206
+ __tracebackhide__ = True
207
+
208
+ err = compare_images (expected , actual , tol , in_decorator = True )
209
+
210
+ if not os .path .exists (expected ):
211
+ raise ImageComparisonFailure ('image does not exist: %s' % expected )
212
+
213
+ if err :
214
+ raise ImageComparisonFailure (
215
+ 'images not close: %(actual)s vs. %(expected)s '
216
+ '(RMS %(rms).3f)' % err )
217
+
218
+
219
+ def xfail_if_format_is_uncomparable (extension ):
220
+ will_fail = extension not in comparable_formats ()
221
+ if will_fail :
222
+ fail_msg = 'Cannot compare %s files on this system' % extension
223
+ else :
224
+ fail_msg = 'No failure expected'
225
+
226
+ return knownfailureif (will_fail , fail_msg ,
227
+ known_exception_class = ImageComparisonFailure )
228
+
229
+
230
+ def mark_xfail_if_format_is_uncomparable (extension ):
231
+ will_fail = extension not in comparable_formats ()
232
+ if will_fail :
233
+ fail_msg = 'Cannot compare %s files on this system' % extension
234
+ import pytest
235
+ return pytest .mark .xfail (extension , reason = fail_msg , strict = False ,
236
+ raises = ImageComparisonFailure )
237
+ else :
238
+ return extension
239
+
240
+
241
+ class ImageComparisonDecorator (CleanupTest ):
242
+ def __init__ (self , baseline_images , extensions , tol ,
243
+ freetype_version , remove_text , savefig_kwargs , style ):
244
+ self .func = self .baseline_dir = self .result_dir = None
245
+ self .baseline_images = baseline_images
246
+ self .extensions = extensions
247
+ self .tol = tol
248
+ self .freetype_version = freetype_version
249
+ self .remove_text = remove_text
250
+ self .savefig_kwargs = savefig_kwargs
251
+ self .style = style
252
+
253
+ def setup (self ):
254
+ func = self .func
255
+ self .setup_class ()
256
+ try :
257
+ matplotlib .style .use (self .style )
182
258
matplotlib .testing .set_font_settings_for_testing ()
183
- cls ._func ()
259
+ func ()
260
+ assert len (plt .get_fignums ()) == len (self .baseline_images ), (
261
+ 'Figures and baseline_images count are not the same'
262
+ ' (`%s`)' % getattr (func , '__qualname__' , func .__name__ ))
184
263
except :
185
264
# Restore original settings before raising errors during the update.
186
- CleanupTest .teardown_class ()
265
+ self .teardown_class ()
187
266
raise
188
267
189
- @classmethod
190
- def teardown_class (cls ):
191
- CleanupTest .teardown_class ()
192
-
193
- @staticmethod
194
- def remove_text (figure ):
195
- figure .suptitle ("" )
196
- for ax in figure .get_axes ():
197
- ax .set_title ("" )
198
- ax .xaxis .set_major_formatter (ticker .NullFormatter ())
199
- ax .xaxis .set_minor_formatter (ticker .NullFormatter ())
200
- ax .yaxis .set_major_formatter (ticker .NullFormatter ())
201
- ax .yaxis .set_minor_formatter (ticker .NullFormatter ())
202
- try :
203
- ax .zaxis .set_major_formatter (ticker .NullFormatter ())
204
- ax .zaxis .set_minor_formatter (ticker .NullFormatter ())
205
- except AttributeError :
206
- pass
268
+ def teardown (self ):
269
+ self .teardown_class ()
270
+
271
+ def copy_baseline (self , baseline , extension ):
272
+ baseline_path = os .path .join (self .baseline_dir , baseline )
273
+ orig_expected_fname = baseline_path + '.' + extension
274
+ if extension == 'eps' and not os .path .exists (orig_expected_fname ):
275
+ orig_expected_fname = baseline_path + '.pdf'
276
+ expected_fname = make_test_filename (os .path .join (
277
+ self .result_dir , os .path .basename (orig_expected_fname )), 'expected' )
278
+ actual_fname = os .path .join (self .result_dir , baseline ) + '.' + extension
279
+ if os .path .exists (orig_expected_fname ):
280
+ shutil .copyfile (orig_expected_fname , expected_fname )
281
+ else :
282
+ xfail ("Do not have baseline image {0} because this "
283
+ "file does not exist: {1}" .format (expected_fname ,
284
+ orig_expected_fname ))
285
+ return expected_fname , actual_fname
286
+
287
+ def compare (self , idx , baseline , extension ):
288
+ __tracebackhide__ = True
289
+ if self .baseline_dir is None :
290
+ self .baseline_dir , self .result_dir = _image_directories (self .func )
291
+ expected_fname , actual_fname = self .copy_baseline (baseline , extension )
292
+ fignum = plt .get_fignums ()[idx ]
293
+ fig = plt .figure (fignum )
294
+ if self .remove_text :
295
+ remove_ticks_and_titles (fig )
296
+ fig .savefig (actual_fname , ** self .savefig_kwargs )
297
+ raise_on_image_difference (expected_fname , actual_fname , self .tol )
298
+
299
+ def nose_runner (self ):
300
+ func = self .compare
301
+ func = checked_on_freetype_version (self .freetype_version )(func )
302
+ funcs = {extension : xfail_if_format_is_uncomparable (extension )(func )
303
+ for extension in self .extensions }
304
+ for idx , baseline in enumerate (self .baseline_images ):
305
+ for extension in self .extensions :
306
+ yield funcs [extension ], idx , baseline , extension
307
+
308
+ def pytest_runner (self ):
309
+ from pytest import mark
310
+
311
+ extensions = map (mark_xfail_if_format_is_uncomparable , self .extensions )
312
+
313
+ @mark .parametrize ("extension" , extensions )
314
+ @mark .parametrize ("idx,baseline" , enumerate (self .baseline_images ))
315
+ @checked_on_freetype_version (self .freetype_version )
316
+ def wrapper (idx , baseline , extension ):
317
+ __tracebackhide__ = True
318
+ self .compare (idx , baseline , extension )
319
+
320
+ # sadly we cannot use fixture here because of visibility problems
321
+ # and for for obvious reason avoid `nose.tools.with_setup`
322
+ wrapper .setup , wrapper .teardown = self .setup , self .teardown
323
+
324
+ return wrapper
325
+
326
+ def __call__ (self , func ):
327
+ self .func = func
328
+ if is_called_from_pytest ():
329
+ return copy_metadata (func , self .pytest_runner ())
330
+ else :
331
+ import nose .tools
207
332
208
- def test (self ):
209
- baseline_dir , result_dir = _image_directories (self ._func )
210
-
211
- for fignum , baseline in zip (plt .get_fignums (), self ._baseline_images ):
212
- for extension in self ._extensions :
213
- will_fail = not extension in comparable_formats ()
214
- if will_fail :
215
- fail_msg = 'Cannot compare %s files on this system' % extension
216
- else :
217
- fail_msg = 'No failure expected'
218
-
219
- orig_expected_fname = os .path .join (baseline_dir , baseline ) + '.' + extension
220
- if extension == 'eps' and not os .path .exists (orig_expected_fname ):
221
- orig_expected_fname = os .path .join (baseline_dir , baseline ) + '.pdf'
222
- expected_fname = make_test_filename (os .path .join (
223
- result_dir , os .path .basename (orig_expected_fname )), 'expected' )
224
- actual_fname = os .path .join (result_dir , baseline ) + '.' + extension
225
- if os .path .exists (orig_expected_fname ):
226
- shutil .copyfile (orig_expected_fname , expected_fname )
227
- else :
228
- will_fail = True
229
- fail_msg = (
230
- "Do not have baseline image {0} because this "
231
- "file does not exist: {1}" .format (
232
- expected_fname ,
233
- orig_expected_fname
234
- )
235
- )
236
-
237
- @knownfailureif (
238
- will_fail , fail_msg ,
239
- known_exception_class = ImageComparisonFailure )
240
- def do_test (fignum , actual_fname , expected_fname ):
241
- figure = plt .figure (fignum )
242
-
243
- if self ._remove_text :
244
- self .remove_text (figure )
245
-
246
- figure .savefig (actual_fname , ** self ._savefig_kwarg )
247
-
248
- err = compare_images (expected_fname , actual_fname ,
249
- self ._tol , in_decorator = True )
250
-
251
- try :
252
- if not os .path .exists (expected_fname ):
253
- raise ImageComparisonFailure (
254
- 'image does not exist: %s' % expected_fname )
255
-
256
- if err :
257
- raise ImageComparisonFailure (
258
- 'images not close: %(actual)s vs. %(expected)s '
259
- '(RMS %(rms).3f)' % err )
260
- except ImageComparisonFailure :
261
- if not check_freetype_version (self ._freetype_version ):
262
- xfail (
263
- "Mismatched version of freetype. Test requires '%s', you have '%s'" %
264
- (self ._freetype_version , ft2font .__freetype_version__ ))
265
- raise
266
-
267
- yield do_test , fignum , actual_fname , expected_fname
333
+ @nose .tools .with_setup (self .setup , self .teardown )
334
+ def runner_wrapper ():
335
+ try :
336
+ for case in self .nose_runner ():
337
+ yield case
338
+ except GeneratorExit :
339
+ # nose bug...
340
+ self .teardown ()
341
+
342
+ return copy_metadata (func , runner_wrapper )
268
343
269
344
270
345
def image_comparison (baseline_images = None , extensions = None , tol = 0 ,
@@ -323,35 +398,11 @@ def image_comparison(baseline_images=None, extensions=None, tol=0,
323
398
#default no kwargs to savefig
324
399
savefig_kwarg = dict ()
325
400
326
- def compare_images_decorator (func ):
327
- # We want to run the setup function (the actual test function
328
- # that generates the figure objects) only once for each type
329
- # of output file. The only way to achieve this with nose
330
- # appears to be to create a test class with "setup_class" and
331
- # "teardown_class" methods. Creating a class instance doesn't
332
- # work, so we use type() to actually create a class and fill
333
- # it with the appropriate methods.
334
- name = func .__name__
335
- # For nose 1.0, we need to rename the test function to
336
- # something without the word "test", or it will be run as
337
- # well, outside of the context of our image comparison test
338
- # generator.
339
- func = staticmethod (func )
340
- func .__get__ (1 ).__name__ = str ('_private' )
341
- new_class = type (
342
- name ,
343
- (ImageComparisonTest ,),
344
- {'_func' : func ,
345
- '_baseline_images' : baseline_images ,
346
- '_extensions' : extensions ,
347
- '_tol' : tol ,
348
- '_freetype_version' : freetype_version ,
349
- '_remove_text' : remove_text ,
350
- '_savefig_kwarg' : savefig_kwarg ,
351
- '_style' : style })
352
-
353
- return new_class
354
- return compare_images_decorator
401
+ return ImageComparisonDecorator (
402
+ baseline_images = baseline_images , extensions = extensions , tol = tol ,
403
+ freetype_version = freetype_version , remove_text = remove_text ,
404
+ savefig_kwargs = savefig_kwarg , style = style )
405
+
355
406
356
407
def _image_directories (func ):
357
408
"""
@@ -416,7 +467,6 @@ def find_dotted_module(module_name, path=None):
416
467
def switch_backend (backend ):
417
468
# Local import to avoid a hard nose dependency and only incur the
418
469
# import time overhead at actual test-time.
419
- import nose
420
470
def switch_backend_decorator (func ):
421
471
def backend_switcher (* args , ** kwargs ):
422
472
try :
0 commit comments