diff --git a/conftest.py b/conftest.py index b3907f980cd3..2a1dbe0a4ece 100644 --- a/conftest.py +++ b/conftest.py @@ -10,7 +10,6 @@ matplotlib.use('agg') from matplotlib import default_test_modules -from matplotlib.testing.decorators import ImageComparisonTest IGNORED_TESTS = { @@ -86,12 +85,6 @@ def pytest_ignore_collect(path, config): def pytest_pycollect_makeitem(collector, name, obj): if inspect.isclass(obj): - if issubclass(obj, ImageComparisonTest): - # Workaround `image_compare` decorator as it returns class - # instead of function and this confuses pytest because it crawls - # original names and sees 'test_*', but not 'Test*' in that case - return pytest.Class(name, parent=collector) - if is_nose_class(obj) and not issubclass(obj, unittest.TestCase): # Workaround unittest-like setup/teardown names in pure classes setup = getattr(obj, 'setUp', None) diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index 0b549b36d088..dcca0eec53fc 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -62,7 +62,7 @@ def getrawcode(obj, trycall=True): def copy_metadata(src_func, tgt_func): """Replicates metadata of the function. Returns target function.""" - tgt_func.__dict__ = src_func.__dict__ + tgt_func.__dict__.update(src_func.__dict__) tgt_func.__doc__ = src_func.__doc__ tgt_func.__module__ = src_func.__module__ tgt_func.__name__ = src_func.__name__ diff --git a/lib/matplotlib/testing/decorators.py b/lib/matplotlib/testing/decorators.py index 43f14198ffd8..447d735fbed2 100644 --- a/lib/matplotlib/testing/decorators.py +++ b/lib/matplotlib/testing/decorators.py @@ -24,10 +24,9 @@ from matplotlib import ticker from matplotlib import pyplot as plt from matplotlib import ft2font -from matplotlib import rcParams from matplotlib.testing.compare import comparable_formats, compare_images, \ make_test_filename -from . import copy_metadata, is_called_from_pytest, skip, xfail +from . import copy_metadata, is_called_from_pytest, xfail from .exceptions import ImageComparisonFailure @@ -59,7 +58,10 @@ def knownfailureif(fail_condition, msg=None, known_exception_class=None): """ if is_called_from_pytest(): import pytest - strict = fail_condition and fail_condition != 'indeterminate' + if fail_condition == 'indeterminate': + fail_condition, strict = True, False + else: + fail_condition, strict = bool(fail_condition), True return pytest.mark.xfail(condition=fail_condition, reason=msg, raises=known_exception_class, strict=strict) else: @@ -173,98 +175,171 @@ def check_freetype_version(ver): return found >= ver[0] and found <= ver[1] -class ImageComparisonTest(CleanupTest): - @classmethod - def setup_class(cls): - CleanupTest.setup_class() +def checked_on_freetype_version(required_freetype_version): + if check_freetype_version(required_freetype_version): + return lambda f: f + + reason = ("Mismatched version of freetype. " + "Test requires '%s', you have '%s'" % + (required_freetype_version, ft2font.__freetype_version__)) + return knownfailureif('indeterminate', msg=reason, + known_exception_class=ImageComparisonFailure) + + +def remove_ticks_and_titles(figure): + figure.suptitle("") + null_formatter = ticker.NullFormatter() + for ax in figure.get_axes(): + ax.set_title("") + ax.xaxis.set_major_formatter(null_formatter) + ax.xaxis.set_minor_formatter(null_formatter) + ax.yaxis.set_major_formatter(null_formatter) + ax.yaxis.set_minor_formatter(null_formatter) try: - matplotlib.style.use(cls._style) + ax.zaxis.set_major_formatter(null_formatter) + ax.zaxis.set_minor_formatter(null_formatter) + except AttributeError: + pass + + +def raise_on_image_difference(expected, actual, tol): + __tracebackhide__ = True + + err = compare_images(expected, actual, tol, in_decorator=True) + + if not os.path.exists(expected): + raise ImageComparisonFailure('image does not exist: %s' % expected) + + if err: + raise ImageComparisonFailure( + 'images not close: %(actual)s vs. %(expected)s ' + '(RMS %(rms).3f)' % err) + + +def xfail_if_format_is_uncomparable(extension): + will_fail = extension not in comparable_formats() + if will_fail: + fail_msg = 'Cannot compare %s files on this system' % extension + else: + fail_msg = 'No failure expected' + + return knownfailureif(will_fail, fail_msg, + known_exception_class=ImageComparisonFailure) + + +def mark_xfail_if_format_is_uncomparable(extension): + will_fail = extension not in comparable_formats() + if will_fail: + fail_msg = 'Cannot compare %s files on this system' % extension + import pytest + return pytest.mark.xfail(extension, reason=fail_msg, strict=False, + raises=ImageComparisonFailure) + else: + return extension + + +class ImageComparisonDecorator(CleanupTest): + def __init__(self, baseline_images, extensions, tol, + freetype_version, remove_text, savefig_kwargs, style): + self.func = self.baseline_dir = self.result_dir = None + self.baseline_images = baseline_images + self.extensions = extensions + self.tol = tol + self.freetype_version = freetype_version + self.remove_text = remove_text + self.savefig_kwargs = savefig_kwargs + self.style = style + + def setup(self): + func = self.func + self.setup_class() + try: + matplotlib.style.use(self.style) matplotlib.testing.set_font_settings_for_testing() - cls._func() + func() + assert len(plt.get_fignums()) == len(self.baseline_images), ( + 'Figures and baseline_images count are not the same' + ' (`%s`)' % getattr(func, '__qualname__', func.__name__)) except: # Restore original settings before raising errors during the update. - CleanupTest.teardown_class() + self.teardown_class() raise - @classmethod - def teardown_class(cls): - CleanupTest.teardown_class() - - @staticmethod - def remove_text(figure): - figure.suptitle("") - for ax in figure.get_axes(): - ax.set_title("") - ax.xaxis.set_major_formatter(ticker.NullFormatter()) - ax.xaxis.set_minor_formatter(ticker.NullFormatter()) - ax.yaxis.set_major_formatter(ticker.NullFormatter()) - ax.yaxis.set_minor_formatter(ticker.NullFormatter()) - try: - ax.zaxis.set_major_formatter(ticker.NullFormatter()) - ax.zaxis.set_minor_formatter(ticker.NullFormatter()) - except AttributeError: - pass + def teardown(self): + self.teardown_class() + + def copy_baseline(self, baseline, extension): + baseline_path = os.path.join(self.baseline_dir, baseline) + orig_expected_fname = baseline_path + '.' + extension + if extension == 'eps' and not os.path.exists(orig_expected_fname): + orig_expected_fname = baseline_path + '.pdf' + expected_fname = make_test_filename(os.path.join( + self.result_dir, os.path.basename(orig_expected_fname)), 'expected') + actual_fname = os.path.join(self.result_dir, baseline) + '.' + extension + if os.path.exists(orig_expected_fname): + shutil.copyfile(orig_expected_fname, expected_fname) + else: + xfail("Do not have baseline image {0} because this " + "file does not exist: {1}".format(expected_fname, + orig_expected_fname)) + return expected_fname, actual_fname + + def compare(self, idx, baseline, extension): + __tracebackhide__ = True + if self.baseline_dir is None: + self.baseline_dir, self.result_dir = _image_directories(self.func) + expected_fname, actual_fname = self.copy_baseline(baseline, extension) + fignum = plt.get_fignums()[idx] + fig = plt.figure(fignum) + if self.remove_text: + remove_ticks_and_titles(fig) + fig.savefig(actual_fname, **self.savefig_kwargs) + raise_on_image_difference(expected_fname, actual_fname, self.tol) + + def nose_runner(self): + func = self.compare + func = checked_on_freetype_version(self.freetype_version)(func) + funcs = {extension: xfail_if_format_is_uncomparable(extension)(func) + for extension in self.extensions} + for idx, baseline in enumerate(self.baseline_images): + for extension in self.extensions: + yield funcs[extension], idx, baseline, extension + + def pytest_runner(self): + from pytest import mark + + extensions = map(mark_xfail_if_format_is_uncomparable, self.extensions) + + @mark.parametrize("extension", extensions) + @mark.parametrize("idx,baseline", enumerate(self.baseline_images)) + @checked_on_freetype_version(self.freetype_version) + def wrapper(idx, baseline, extension): + __tracebackhide__ = True + self.compare(idx, baseline, extension) + + # sadly we cannot use fixture here because of visibility problems + # and for for obvious reason avoid `nose.tools.with_setup` + wrapper.setup, wrapper.teardown = self.setup, self.teardown + + return wrapper + + def __call__(self, func): + self.func = func + if is_called_from_pytest(): + return copy_metadata(func, self.pytest_runner()) + else: + import nose.tools - def test(self): - baseline_dir, result_dir = _image_directories(self._func) - - for fignum, baseline in zip(plt.get_fignums(), self._baseline_images): - for extension in self._extensions: - will_fail = not extension in comparable_formats() - if will_fail: - fail_msg = 'Cannot compare %s files on this system' % extension - else: - fail_msg = 'No failure expected' - - orig_expected_fname = os.path.join(baseline_dir, baseline) + '.' + extension - if extension == 'eps' and not os.path.exists(orig_expected_fname): - orig_expected_fname = os.path.join(baseline_dir, baseline) + '.pdf' - expected_fname = make_test_filename(os.path.join( - result_dir, os.path.basename(orig_expected_fname)), 'expected') - actual_fname = os.path.join(result_dir, baseline) + '.' + extension - if os.path.exists(orig_expected_fname): - shutil.copyfile(orig_expected_fname, expected_fname) - else: - will_fail = True - fail_msg = ( - "Do not have baseline image {0} because this " - "file does not exist: {1}".format( - expected_fname, - orig_expected_fname - ) - ) - - @knownfailureif( - will_fail, fail_msg, - known_exception_class=ImageComparisonFailure) - def do_test(fignum, actual_fname, expected_fname): - figure = plt.figure(fignum) - - if self._remove_text: - self.remove_text(figure) - - figure.savefig(actual_fname, **self._savefig_kwarg) - - err = compare_images(expected_fname, actual_fname, - self._tol, in_decorator=True) - - try: - if not os.path.exists(expected_fname): - raise ImageComparisonFailure( - 'image does not exist: %s' % expected_fname) - - if err: - raise ImageComparisonFailure( - 'images not close: %(actual)s vs. %(expected)s ' - '(RMS %(rms).3f)'%err) - except ImageComparisonFailure: - if not check_freetype_version(self._freetype_version): - xfail( - "Mismatched version of freetype. Test requires '%s', you have '%s'" % - (self._freetype_version, ft2font.__freetype_version__)) - raise - - yield do_test, fignum, actual_fname, expected_fname + @nose.tools.with_setup(self.setup, self.teardown) + def runner_wrapper(): + try: + for case in self.nose_runner(): + yield case + except GeneratorExit: + # nose bug... + self.teardown() + + return copy_metadata(func, runner_wrapper) def image_comparison(baseline_images=None, extensions=None, tol=0, @@ -323,35 +398,11 @@ def image_comparison(baseline_images=None, extensions=None, tol=0, #default no kwargs to savefig savefig_kwarg = dict() - def compare_images_decorator(func): - # We want to run the setup function (the actual test function - # that generates the figure objects) only once for each type - # of output file. The only way to achieve this with nose - # appears to be to create a test class with "setup_class" and - # "teardown_class" methods. Creating a class instance doesn't - # work, so we use type() to actually create a class and fill - # it with the appropriate methods. - name = func.__name__ - # For nose 1.0, we need to rename the test function to - # something without the word "test", or it will be run as - # well, outside of the context of our image comparison test - # generator. - func = staticmethod(func) - func.__get__(1).__name__ = str('_private') - new_class = type( - name, - (ImageComparisonTest,), - {'_func': func, - '_baseline_images': baseline_images, - '_extensions': extensions, - '_tol': tol, - '_freetype_version': freetype_version, - '_remove_text': remove_text, - '_savefig_kwarg': savefig_kwarg, - '_style': style}) - - return new_class - return compare_images_decorator + return ImageComparisonDecorator( + baseline_images=baseline_images, extensions=extensions, tol=tol, + freetype_version=freetype_version, remove_text=remove_text, + savefig_kwargs=savefig_kwarg, style=style) + def _image_directories(func): """ @@ -416,7 +467,6 @@ def find_dotted_module(module_name, path=None): def switch_backend(backend): # Local import to avoid a hard nose dependency and only incur the # import time overhead at actual test-time. - import nose def switch_backend_decorator(func): def backend_switcher(*args, **kwargs): try: diff --git a/lib/matplotlib/tests/baseline_images/test_axes/scatter.png b/lib/matplotlib/tests/baseline_images/test_axes/scatter.png index b23b8c6b8ef5..f5cd0dd5a695 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/scatter.png and b/lib/matplotlib/tests/baseline_images/test_axes/scatter.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/scatter.svg b/lib/matplotlib/tests/baseline_images/test_axes/scatter.svg index 78a47d457f65..a39062630deb 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/scatter.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/scatter.svg @@ -27,7 +27,7 @@ z " style="fill:#ffffff;"/> - - - - - - - - - - +" id="mec7b6a7454" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="m7665191d92" style="stroke:#000000;stroke-width:0.5;"/> - + @@ -185,12 +139,12 @@ z - + - + @@ -228,12 +182,12 @@ Q 31.109375 20.453125 19.1875 8.296875 - + - + @@ -279,12 +233,12 @@ Q 46.96875 40.921875 40.578125 39.3125 - + - + @@ -316,12 +270,12 @@ z - + - + @@ -360,12 +314,12 @@ z - + - + @@ -408,12 +362,12 @@ Q 48.484375 72.75 52.59375 71.296875 - + - + @@ -441,20 +395,20 @@ z +" id="m2ccdaa1d4a" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="m5cded60920" style="stroke:#000000;stroke-width:0.5;"/> - + @@ -477,12 +431,12 @@ z - + - + @@ -518,12 +472,12 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + @@ -538,12 +492,12 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + @@ -558,12 +512,12 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + @@ -578,12 +532,12 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + @@ -598,12 +552,12 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + @@ -618,12 +572,12 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + @@ -638,12 +592,12 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + @@ -659,7 +613,7 @@ Q 19.53125 74.21875 31.78125 74.21875 - + diff --git a/lib/matplotlib/tests/test_agg.py b/lib/matplotlib/tests/test_agg.py index e392e97a9a1e..355c850758c0 100644 --- a/lib/matplotlib/tests/test_agg.py +++ b/lib/matplotlib/tests/test_agg.py @@ -16,6 +16,7 @@ from matplotlib.image import imread from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas from matplotlib.figure import Figure +from matplotlib.testing import skip from matplotlib.testing.decorators import ( cleanup, image_comparison, knownfailureif) from matplotlib import pyplot as plt @@ -251,7 +252,7 @@ def process_image(self, padded_src, dpi): return t2 if V(np.__version__) < V('1.7.0'): - return + skip('Disabled on Numpy < 1.7.0') fig = plt.figure() ax = fig.add_subplot(111) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index a5c176c4d472..45bb09e42fb1 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1268,14 +1268,14 @@ def test_hist2d_transpose(): @image_comparison(baseline_images=['scatter', 'scatter']) def test_scatter_plot(): - ax = plt.axes() + fig, ax = plt.subplots() data = {"x": [3, 4, 2, 6], "y": [2, 5, 2, 3], "c": ['r', 'y', 'b', 'lime'], "s": [24, 15, 19, 29]} ax.scatter(data["x"], data["y"], c=data["c"], s=data["s"]) # Reuse testcase from above for a labeled data test - ax = plt.axes() + fig, ax = plt.subplots() ax.scatter("x", "y", c="c", s="s", data=data) @@ -1375,8 +1375,8 @@ def _as_mpl_axes(self): @image_comparison(baseline_images=['log_scales']) def test_log_scales(): fig = plt.figure() - ax = plt.gca() - plt.plot(np.log(np.linspace(0.1, 100))) + ax = fig.add_subplot(1, 1, 1) + ax.plot(np.log(np.linspace(0.1, 100))) ax.set_yscale('log', basey=5.5) ax.invert_yaxis() ax.set_xscale('log', basex=9.0) diff --git a/lib/matplotlib/tests/test_coding_standards.py b/lib/matplotlib/tests/test_coding_standards.py index 123c226945de..2da1983fe8e3 100644 --- a/lib/matplotlib/tests/test_coding_standards.py +++ b/lib/matplotlib/tests/test_coding_standards.py @@ -100,6 +100,7 @@ def assert_pep8_conformance(module=matplotlib, exclude_files=None, The file should be a line separated list of filenames/directories as can be passed to the "pep8" tool's exclude list. """ + __tracebackhide__ = True if not HAS_PEP8: raise SkipTest('The pep8 tool is required for this test') @@ -156,6 +157,8 @@ def assert_pep8_conformance(module=matplotlib, exclude_files=None, def test_pep8_conformance_installed_files(): + __tracebackhide__ = True + exclude_files = ['_delaunay.py', '_image.py', '_tri.py',