|
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