Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prepare for cross-framework test suite #6920

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
65 changes: 6 additions & 59 deletions lib/matplotlib/__init__.py
Expand Up @@ -1401,7 +1401,7 @@ def use(arg, warn=True, force=False):
if 'matplotlib.backends' in sys.modules:
# Warn only if called with a different name
if (rcParams['backend'] != name) and warn:
warnings.warn(_use_error_msg)
warnings.warn(_use_error_msg, stacklevel=2)

# Unless we've been told to force it, just return
if not force:
Expand Down Expand Up @@ -1586,70 +1586,17 @@ def _init_tests():
)
)

try:
import nose
try:
from unittest import mock
except:
import mock
except ImportError:
print("matplotlib.test requires nose and mock to run.")
raise


def _get_extra_test_plugins():
from .testing.performgc import PerformGC
from .testing.noseclasses import KnownFailure
from nose.plugins import attrib
from .testing.nose import check_deps
check_deps()

return [PerformGC, KnownFailure, attrib.Plugin]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mock is used in other tests; not sure this test should be removed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check is not removed, it is just moved to check_deps function


def _get_nose_env():
env = {'NOSE_COVER_PACKAGE': ['matplotlib', 'mpl_toolkits'],
'NOSE_COVER_HTML': 1,
'NOSE_COVER_NO_PRINT': 1}
return env


def test(verbosity=1, coverage=False):
def test(verbosity=1, coverage=False, **kwargs):
"""run the matplotlib test suite"""
_init_tests()

old_backend = rcParams['backend']
try:
use('agg')
import nose
import nose.plugins.builtin
from nose.plugins.manager import PluginManager
from nose.plugins import multiprocess

# store the old values before overriding
plugins = _get_extra_test_plugins()
plugins.extend([plugin for plugin in nose.plugins.builtin.plugins])

manager = PluginManager(plugins=[x() for x in plugins])
config = nose.config.Config(verbosity=verbosity, plugins=manager)

# Nose doesn't automatically instantiate all of the plugins in the
# child processes, so we have to provide the multiprocess plugin with
# a list.
multiprocess._instantiate_plugins = plugins

env = _get_nose_env()
if coverage:
env['NOSE_WITH_COVERAGE'] = 1

success = nose.run(
defaultTest=default_test_modules,
config=config,
env=env,
)
finally:
if old_backend.lower() != 'agg':
use(old_backend)
from .testing.nose import test as nose_test
return nose_test(verbosity, coverage, **kwargs)

return success

test.__test__ = False # nose: this function is not a test

Expand Down
44 changes: 44 additions & 0 deletions lib/matplotlib/testing/__init__.py
@@ -1,6 +1,7 @@
from __future__ import (absolute_import, division, print_function,
unicode_literals)

import inspect
import warnings
from contextlib import contextmanager

Expand All @@ -13,6 +14,49 @@ def _is_list_like(obj):
return not is_string_like(obj) and iterable(obj)


def xfail(msg=""):
"""Explicitly fail an currently-executing test with the given message."""
from .nose import knownfail
knownfail(msg)


def skip(msg=""):
"""Skip an executing test with the given message."""
from nose import SkipTest
raise SkipTest(msg)


# stolen from pytest
def getrawcode(obj, trycall=True):
"""Return code object for given function."""
try:
return obj.__code__
except AttributeError:
obj = getattr(obj, 'im_func', obj)
obj = getattr(obj, 'func_code', obj)
obj = getattr(obj, 'f_code', obj)
obj = getattr(obj, '__code__', obj)
if trycall and not hasattr(obj, 'co_firstlineno'):
if hasattr(obj, '__call__') and not inspect.isclass(obj):
x = getrawcode(obj.__call__, trycall=False)
if hasattr(x, 'co_firstlineno'):
return x
return obj


def copy_metadata(src_func, tgt_func):
"""Replicates metadata of the function. Returns target function."""
tgt_func.__dict__ = src_func.__dict__
tgt_func.__doc__ = src_func.__doc__
tgt_func.__module__ = src_func.__module__
tgt_func.__name__ = src_func.__name__
if hasattr(src_func, '__qualname__'):
tgt_func.__qualname__ = src_func.__qualname__
if not hasattr(tgt_func, 'compat_co_firstlineno'):
tgt_func.compat_co_firstlineno = getrawcode(src_func).co_firstlineno
return tgt_func


# stolen from pandas
@contextmanager
def assert_produces_warning(expected_warning=Warning, filter_level="always",
Expand Down
60 changes: 20 additions & 40 deletions lib/matplotlib/testing/decorators.py
Expand Up @@ -4,7 +4,6 @@
import six

import functools
import gc
import inspect
import os
import sys
Expand All @@ -16,8 +15,6 @@
# allows other functions here to be used by pytest-based testing suites without
# requiring nose to be installed.

import numpy as np

import matplotlib as mpl
import matplotlib.style
import matplotlib.units
Expand All @@ -27,13 +24,23 @@
from matplotlib import pyplot as plt
from matplotlib import ft2font
from matplotlib import rcParams
from matplotlib.testing.noseclasses import KnownFailureTest, \
KnownFailureDidNotFailTest, ImageComparisonFailure
from matplotlib.testing.compare import comparable_formats, compare_images, \
make_test_filename
from . import copy_metadata, skip, xfail
from .exceptions import ImageComparisonFailure


def skipif(condition, *args, **kwargs):
"""Skip the given test function if eval(condition) results in a True
value.

Optionally specify a reason for better reporting.
"""
from .nose.decorators import skipif
return skipif(condition, *args, **kwargs)


def knownfailureif(fail_condition, msg=None, known_exception_class=None ):
def knownfailureif(fail_condition, msg=None, known_exception_class=None):
"""

Assume a will fail if *fail_condition* is True. *fail_condition*
Expand All @@ -45,32 +52,8 @@ def knownfailureif(fail_condition, msg=None, known_exception_class=None ):
if the exception is an instance of this class. (Default = None)

"""
# based on numpy.testing.dec.knownfailureif
if msg is None:
msg = 'Test known to fail'
def known_fail_decorator(f):
# Local import to avoid a hard nose dependency and only incur the
# import time overhead at actual test-time.
import nose
def failer(*args, **kwargs):
try:
# Always run the test (to generate images).
result = f(*args, **kwargs)
except Exception as err:
if fail_condition:
if known_exception_class is not None:
if not isinstance(err,known_exception_class):
# This is not the expected exception
raise
# (Keep the next ultra-long comment so in shows in console.)
raise KnownFailureTest(msg) # An error here when running nose means that you don't have the matplotlib.testing.noseclasses:KnownFailure plugin in use.
else:
raise
if fail_condition and fail_condition != 'indeterminate':
raise KnownFailureDidNotFailTest(msg)
return result
return nose.tools.make_decorator(f)(failer)
return known_fail_decorator
from .nose.decorators import knownfailureif
return knownfailureif(fail_condition, msg, known_exception_class)


def _do_cleanup(original_units_registry, original_settings):
Expand Down Expand Up @@ -214,7 +197,7 @@ def remove_text(figure):
def test(self):
baseline_dir, result_dir = _image_directories(self._func)
if self._style != 'classic':
raise KnownFailureTest('temporarily disabled until 2.0 tag')
xfail('temporarily disabled until 2.0 tag')
for fignum, baseline in zip(plt.get_fignums(), self._baseline_images):
for extension in self._extensions:
will_fail = not extension in comparable_formats()
Expand Down Expand Up @@ -266,13 +249,14 @@ def do_test():
'(RMS %(rms).3f)'%err)
except ImageComparisonFailure:
if not check_freetype_version(self._freetype_version):
raise KnownFailureTest(
xfail(
"Mismatched version of freetype. Test requires '%s', you have '%s'" %
(self._freetype_version, ft2font.__freetype_version__))
raise

yield (do_test,)


def image_comparison(baseline_images=None, extensions=None, tol=0,
freetype_version=None, remove_text=False,
savefig_kwarg=None, style='classic'):
Expand Down Expand Up @@ -420,9 +404,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:
Expand All @@ -434,7 +415,7 @@ def backend_switcher(*args, **kwargs):
plt.switch_backend(prev_backend)
return result

return nose.tools.make_decorator(func)(backend_switcher)
return copy_metadata(func, backend_switcher)
return switch_backend_decorator


Expand All @@ -453,7 +434,6 @@ def skip_if_command_unavailable(cmd):
try:
check_output(cmd)
except:
from nose import SkipTest
raise SkipTest('missing command: %s' % cmd[0])
skip('missing command: %s' % cmd[0])

return lambda f: f
12 changes: 0 additions & 12 deletions lib/matplotlib/testing/exceptions.py
@@ -1,15 +1,3 @@
class KnownFailureTest(Exception):
"""
Raise this exception to mark a test as a known failing test.
"""


class KnownFailureDidNotFailTest(Exception):
"""
Raise this exception to mark a test should have failed but did not.
"""


class ImageComparisonFailure(AssertionError):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather not move this for compatibility reasons.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I am pretty sure other projects actually uses our KnownFailure stuff.

On Wed, Aug 17, 2016 at 1:15 PM, Thomas A Caswell notifications@github.com
wrote:

In lib/matplotlib/testing/exceptions.py
#6920 (comment):

@@ -1,15 +1,3 @@
-class KnownFailureTest(Exception):

I would rather not move this for compatibility reasons.


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/matplotlib/matplotlib/pull/6920/files/f20efb99d67e581f18f8fd58db07d6279528fcbd#r75165500,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AARy-A6W8iBehFuv_ltVBw91aSSmfrI6ks5qg0GmgaJpZM4Jemgm
.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot agree with this for two reasons:

  • PRs that relay on raise KnownFailureTest should not pass after this PR ever, and this change will force them to switch on xfail
  • Other projects must not relay on matplotlib testing features, it is their problem if they are

I can understand backward compatibility on matplotlib core features, but with removing nose support all this stuff will gone. I have already tried my best to make dual nose-pytest support, but leaving nose-specific stuff on the current places will blow something in any time in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to note, matplotlib and many other projects depend on some of numpy's
testing features. By your logic, that shouldn't be done and every project
should implement their own numerical comparison tools?

I am not exactly sure how to proceed here.

On Wed, Aug 17, 2016 at 1:31 PM, Nikita Kniazev notifications@github.com
wrote:

In lib/matplotlib/testing/exceptions.py
#6920 (comment):

@@ -1,15 +1,3 @@
-class KnownFailureTest(Exception):

I cannot agree with this for two reasons:

  • PRs that relay on raise KnownFailureTest should not pass after this
    PR ever, and this change will force them to switch on xfail
  • Other projects must not relay on matplotlib testing features, it is
    their problem if they are

I can understand backward compatibility on matplotlib core features, but
with removing nose support all this stuff will gone. I have already tried
my best to make dual nose-pytest support, but leaving nose-specific stuff
on the current places will blow something in any time in the future.


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/matplotlib/matplotlib/pull/6920/files/f20efb99d67e581f18f8fd58db07d6279528fcbd#r75168432,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AARy-HYvL9iMog7FVqaAIeBGMR-v5ev-ks5qg0WIgaJpZM4Jemgm
.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By my logic - anything that is not a part of the public API can be changed or even removed at any time, so everyone who did something like using KnownFailureTest makes it on its own risk.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Numpy's tools are useful for testing using numpy arrays, so I don't find referencing them to be a compelling argument. The exceptions removed here are not unique to matplotlib functionality. It was a bad idea to for people to use them from us in the first place, and are trivially recreated for anyone down stream who happen to use them.

"""
Raise this exception to mark a test as a comparison between two images.
Expand Down
70 changes: 70 additions & 0 deletions lib/matplotlib/testing/nose/__init__.py
@@ -0,0 +1,70 @@
from __future__ import (absolute_import, division, print_function,
unicode_literals)


def get_extra_test_plugins():
from .plugins.performgc import PerformGC
from .plugins.knownfailure import KnownFailure
from nose.plugins import attrib

return [PerformGC, KnownFailure, attrib.Plugin]


def get_env():
env = {'NOSE_COVER_PACKAGE': ['matplotlib', 'mpl_toolkits'],
'NOSE_COVER_HTML': 1,
'NOSE_COVER_NO_PRINT': 1}
return env


def check_deps():
try:
import nose
try:
from unittest import mock
except ImportError:
import mock
except ImportError:
print("matplotlib.test requires nose and mock to run.")
raise


def test(verbosity=None, coverage=False, switch_backend_warn=True, **kwargs):
from ... import default_test_modules, get_backend, use

old_backend = get_backend()
try:
use('agg')
import nose
from nose.plugins import multiprocess

# Nose doesn't automatically instantiate all of the plugins in the
# child processes, so we have to provide the multiprocess plugin with
# a list.
extra_plugins = get_extra_test_plugins()
multiprocess._instantiate_plugins = extra_plugins

env = get_env()
if coverage:
env['NOSE_WITH_COVERAGE'] = 1

if verbosity is not None:
env['NOSE_VERBOSE'] = verbosity

success = nose.run(
addplugins=[plugin() for plugin in extra_plugins],
env=env,
defaultTest=default_test_modules,
**kwargs
)
finally:
if old_backend.lower() != 'agg':
use(old_backend, warn=switch_backend_warn)

return success


def knownfail(msg):
from .exceptions import KnownFailureTest
# Keep the next ultra-long comment so it shows in console.
raise KnownFailureTest(msg) # An error here when running nose means that you don't have the matplotlib.testing.nose.plugins:KnownFailure plugin in use. # noqa