|
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 |
|
@@ -176,98 +175,171 @@ def check_freetype_version(ver):
|
176 | 175 | return found >= ver[0] and found <= ver[1]
|
177 | 176 |
|
178 | 177 |
|
179 |
| -class ImageComparisonTest(CleanupTest): |
180 |
| - @classmethod |
181 |
| - def setup_class(cls): |
182 |
| - 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) |
| 198 | + try: |
| 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() |
183 | 256 | try:
|
184 |
| - matplotlib.style.use(cls._style) |
| 257 | + matplotlib.style.use(self.style) |
185 | 258 | matplotlib.testing.set_font_settings_for_testing()
|
186 |
| - 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__)) |
187 | 263 | except:
|
188 | 264 | # Restore original settings before raising errors during the update.
|
189 |
| - CleanupTest.teardown_class() |
| 265 | + self.teardown_class() |
190 | 266 | raise
|
191 | 267 |
|
192 |
| - @classmethod |
193 |
| - def teardown_class(cls): |
194 |
| - CleanupTest.teardown_class() |
195 |
| - |
196 |
| - @staticmethod |
197 |
| - def remove_text(figure): |
198 |
| - figure.suptitle("") |
199 |
| - for ax in figure.get_axes(): |
200 |
| - ax.set_title("") |
201 |
| - ax.xaxis.set_major_formatter(ticker.NullFormatter()) |
202 |
| - ax.xaxis.set_minor_formatter(ticker.NullFormatter()) |
203 |
| - ax.yaxis.set_major_formatter(ticker.NullFormatter()) |
204 |
| - ax.yaxis.set_minor_formatter(ticker.NullFormatter()) |
205 |
| - try: |
206 |
| - ax.zaxis.set_major_formatter(ticker.NullFormatter()) |
207 |
| - ax.zaxis.set_minor_formatter(ticker.NullFormatter()) |
208 |
| - except AttributeError: |
209 |
| - 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 |
210 | 332 |
|
211 |
| - def test(self): |
212 |
| - baseline_dir, result_dir = _image_directories(self._func) |
213 |
| - |
214 |
| - for fignum, baseline in zip(plt.get_fignums(), self._baseline_images): |
215 |
| - for extension in self._extensions: |
216 |
| - will_fail = not extension in comparable_formats() |
217 |
| - if will_fail: |
218 |
| - fail_msg = 'Cannot compare %s files on this system' % extension |
219 |
| - else: |
220 |
| - fail_msg = 'No failure expected' |
221 |
| - |
222 |
| - orig_expected_fname = os.path.join(baseline_dir, baseline) + '.' + extension |
223 |
| - if extension == 'eps' and not os.path.exists(orig_expected_fname): |
224 |
| - orig_expected_fname = os.path.join(baseline_dir, baseline) + '.pdf' |
225 |
| - expected_fname = make_test_filename(os.path.join( |
226 |
| - result_dir, os.path.basename(orig_expected_fname)), 'expected') |
227 |
| - actual_fname = os.path.join(result_dir, baseline) + '.' + extension |
228 |
| - if os.path.exists(orig_expected_fname): |
229 |
| - shutil.copyfile(orig_expected_fname, expected_fname) |
230 |
| - else: |
231 |
| - will_fail = True |
232 |
| - fail_msg = ( |
233 |
| - "Do not have baseline image {0} because this " |
234 |
| - "file does not exist: {1}".format( |
235 |
| - expected_fname, |
236 |
| - orig_expected_fname |
237 |
| - ) |
238 |
| - ) |
239 |
| - |
240 |
| - @knownfailureif( |
241 |
| - will_fail, fail_msg, |
242 |
| - known_exception_class=ImageComparisonFailure) |
243 |
| - def do_test(fignum, actual_fname, expected_fname): |
244 |
| - figure = plt.figure(fignum) |
245 |
| - |
246 |
| - if self._remove_text: |
247 |
| - self.remove_text(figure) |
248 |
| - |
249 |
| - figure.savefig(actual_fname, **self._savefig_kwarg) |
250 |
| - |
251 |
| - err = compare_images(expected_fname, actual_fname, |
252 |
| - self._tol, in_decorator=True) |
253 |
| - |
254 |
| - try: |
255 |
| - if not os.path.exists(expected_fname): |
256 |
| - raise ImageComparisonFailure( |
257 |
| - 'image does not exist: %s' % expected_fname) |
258 |
| - |
259 |
| - if err: |
260 |
| - raise ImageComparisonFailure( |
261 |
| - 'images not close: %(actual)s vs. %(expected)s ' |
262 |
| - '(RMS %(rms).3f)'%err) |
263 |
| - except ImageComparisonFailure: |
264 |
| - if not check_freetype_version(self._freetype_version): |
265 |
| - xfail( |
266 |
| - "Mismatched version of freetype. Test requires '%s', you have '%s'" % |
267 |
| - (self._freetype_version, ft2font.__freetype_version__)) |
268 |
| - raise |
269 |
| - |
270 |
| - 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) |
271 | 343 |
|
272 | 344 |
|
273 | 345 | def image_comparison(baseline_images=None, extensions=None, tol=0,
|
@@ -326,35 +398,11 @@ def image_comparison(baseline_images=None, extensions=None, tol=0,
|
326 | 398 | #default no kwargs to savefig
|
327 | 399 | savefig_kwarg = dict()
|
328 | 400 |
|
329 |
| - def compare_images_decorator(func): |
330 |
| - # We want to run the setup function (the actual test function |
331 |
| - # that generates the figure objects) only once for each type |
332 |
| - # of output file. The only way to achieve this with nose |
333 |
| - # appears to be to create a test class with "setup_class" and |
334 |
| - # "teardown_class" methods. Creating a class instance doesn't |
335 |
| - # work, so we use type() to actually create a class and fill |
336 |
| - # it with the appropriate methods. |
337 |
| - name = func.__name__ |
338 |
| - # For nose 1.0, we need to rename the test function to |
339 |
| - # something without the word "test", or it will be run as |
340 |
| - # well, outside of the context of our image comparison test |
341 |
| - # generator. |
342 |
| - func = staticmethod(func) |
343 |
| - func.__get__(1).__name__ = str('_private') |
344 |
| - new_class = type( |
345 |
| - name, |
346 |
| - (ImageComparisonTest,), |
347 |
| - {'_func': func, |
348 |
| - '_baseline_images': baseline_images, |
349 |
| - '_extensions': extensions, |
350 |
| - '_tol': tol, |
351 |
| - '_freetype_version': freetype_version, |
352 |
| - '_remove_text': remove_text, |
353 |
| - '_savefig_kwarg': savefig_kwarg, |
354 |
| - '_style': style}) |
355 |
| - |
356 |
| - return new_class |
357 |
| - 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 | + |
358 | 406 |
|
359 | 407 | def _image_directories(func):
|
360 | 408 | """
|
|
0 commit comments