Skip to content

Commit 634cdf3

Browse files
committed
Prepare for cross-framework test suite
- introduced `xfail(reason)` function - uses: `raise KnownFailureTest(msg)` -> `xfail(reason)` - same name and signature as in pytest - introduced `skip(reason)` function - uses: `raise SkipTest(msg)` -> `skip(reason)` - same name and signature as in pytest - introduced `skipif(condition, reason=None)` decorator - uses: replaces `def func(): if condition: skip()` - same name and signature as in pytest - can be used with functions, classes, and methods - supports string condition (evaluated at runtime) - moved nose related code to `testing.nose` submodule - plugins in `testing.nose.plugins` submodule - decorators implementation in `testing.nose.decorators` (interface is still in `testing.decorators`, implementation will have been chosen at runtime according to used test framework) - `matplotlib.test` function unifications - `tests.py` now uses `matplotlib.test()`
1 parent 4e21b9a commit 634cdf3

17 files changed

+289
-168
lines changed

lib/matplotlib/__init__.py

Lines changed: 6 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,7 +1398,7 @@ def use(arg, warn=True, force=False):
13981398
if 'matplotlib.backends' in sys.modules:
13991399
# Warn only if called with a different name
14001400
if (rcParams['backend'] != name) and warn:
1401-
warnings.warn(_use_error_msg)
1401+
warnings.warn(_use_error_msg, stacklevel=2)
14021402

14031403
# Unless we've been told to force it, just return
14041404
if not force:
@@ -1580,70 +1580,17 @@ def _init_tests():
15801580
)
15811581
)
15821582

1583-
try:
1584-
import nose
1585-
try:
1586-
from unittest import mock
1587-
except:
1588-
import mock
1589-
except ImportError:
1590-
print("matplotlib.test requires nose and mock to run.")
1591-
raise
1592-
1593-
1594-
def _get_extra_test_plugins():
1595-
from .testing.performgc import PerformGC
1596-
from .testing.noseclasses import KnownFailure
1597-
from nose.plugins import attrib
1583+
from .testing.nose import check_deps
1584+
check_deps()
15981585

1599-
return [PerformGC, KnownFailure, attrib.Plugin]
16001586

1601-
1602-
def _get_nose_env():
1603-
env = {'NOSE_COVER_PACKAGE': 'matplotlib',
1604-
'NOSE_COVER_HTML': 1,
1605-
'NOSE_COVER_NO_PRINT': 1}
1606-
return env
1607-
1608-
1609-
def test(verbosity=1, coverage=False):
1587+
def test(verbosity=1, coverage=False, **kwargs):
16101588
"""run the matplotlib test suite"""
16111589
_init_tests()
16121590

1613-
old_backend = rcParams['backend']
1614-
try:
1615-
use('agg')
1616-
import nose
1617-
import nose.plugins.builtin
1618-
from nose.plugins.manager import PluginManager
1619-
from nose.plugins import multiprocess
1620-
1621-
# store the old values before overriding
1622-
plugins = _get_extra_test_plugins()
1623-
plugins.extend([plugin for plugin in nose.plugins.builtin.plugins])
1624-
1625-
manager = PluginManager(plugins=[x() for x in plugins])
1626-
config = nose.config.Config(verbosity=verbosity, plugins=manager)
1627-
1628-
# Nose doesn't automatically instantiate all of the plugins in the
1629-
# child processes, so we have to provide the multiprocess plugin with
1630-
# a list.
1631-
multiprocess._instantiate_plugins = plugins
1632-
1633-
env = _get_nose_env()
1634-
if coverage:
1635-
env['NOSE_WITH_COVERAGE'] = 1
1636-
1637-
success = nose.run(
1638-
defaultTest=default_test_modules,
1639-
config=config,
1640-
env=env,
1641-
)
1642-
finally:
1643-
if old_backend.lower() != 'agg':
1644-
use(old_backend)
1591+
from .testing.nose import test as nose_test
1592+
return nose_test(verbosity, coverage, **kwargs)
16451593

1646-
return success
16471594

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

lib/matplotlib/testing/__init__.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import (absolute_import, division, print_function,
22
unicode_literals)
33

4+
import inspect
45
import warnings
56
from contextlib import contextmanager
67

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

1516

17+
def xfail(msg=""):
18+
"""Explicitly fail an currently-executing test with the given message."""
19+
from .nose import knownfail
20+
knownfail(msg)
21+
22+
23+
def skip(msg=""):
24+
"""Skip an executing test with the given message."""
25+
from nose import SkipTest
26+
raise SkipTest(msg)
27+
28+
29+
# stolen from pytest
30+
def getrawcode(obj, trycall=True):
31+
"""Return code object for given function."""
32+
try:
33+
return obj.__code__
34+
except AttributeError:
35+
obj = getattr(obj, 'im_func', obj)
36+
obj = getattr(obj, 'func_code', obj)
37+
obj = getattr(obj, 'f_code', obj)
38+
obj = getattr(obj, '__code__', obj)
39+
if trycall and not hasattr(obj, 'co_firstlineno'):
40+
if hasattr(obj, '__call__') and not inspect.isclass(obj):
41+
x = getrawcode(obj.__call__, trycall=False)
42+
if hasattr(x, 'co_firstlineno'):
43+
return x
44+
return obj
45+
46+
47+
def copy_metadata(src_func, tgt_func):
48+
"""Replicates metadata of the function. Returns target function."""
49+
tgt_func.__dict__ = src_func.__dict__
50+
tgt_func.__doc__ = src_func.__doc__
51+
tgt_func.__module__ = src_func.__module__
52+
tgt_func.__name__ = src_func.__name__
53+
if hasattr(src_func, '__qualname__'):
54+
tgt_func.__qualname__ = src_func.__qualname__
55+
if not hasattr(tgt_func, 'compat_co_firstlineno'):
56+
tgt_func.compat_co_firstlineno = getrawcode(src_func).co_firstlineno
57+
return tgt_func
58+
59+
1660
# stolen from pandas
1761
@contextmanager
1862
def assert_produces_warning(expected_warning=Warning, filter_level="always",

lib/matplotlib/testing/decorators.py

Lines changed: 20 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,13 @@
44
import six
55

66
import functools
7-
import gc
87
import inspect
98
import os
109
import sys
1110
import shutil
1211
import warnings
1312
import unittest
1413

15-
import nose
16-
import numpy as np
17-
1814
import matplotlib as mpl
1915
import matplotlib.style
2016
import matplotlib.units
@@ -24,13 +20,23 @@
2420
from matplotlib import pyplot as plt
2521
from matplotlib import ft2font
2622
from matplotlib import rcParams
27-
from matplotlib.testing.noseclasses import KnownFailureTest, \
28-
KnownFailureDidNotFailTest, ImageComparisonFailure
2923
from matplotlib.testing.compare import comparable_formats, compare_images, \
3024
make_test_filename
25+
from . import copy_metadata, skip, xfail
26+
from .exceptions import ImageComparisonFailure
27+
28+
29+
def skipif(condition, reason=None):
30+
"""Skip the given test function if eval(condition) results in a True
31+
value.
32+
33+
Optionally specify a reason for better reporting.
34+
"""
35+
from .nose.decorators import skipif
36+
return skipif(condition, reason)
3137

3238

33-
def knownfailureif(fail_condition, msg=None, known_exception_class=None ):
39+
def knownfailureif(fail_condition, msg=None, known_exception_class=None):
3440
"""
3541
3642
Assume a will fail if *fail_condition* is True. *fail_condition*
@@ -42,32 +48,8 @@ def knownfailureif(fail_condition, msg=None, known_exception_class=None ):
4248
if the exception is an instance of this class. (Default = None)
4349
4450
"""
45-
# based on numpy.testing.dec.knownfailureif
46-
if msg is None:
47-
msg = 'Test known to fail'
48-
def known_fail_decorator(f):
49-
# Local import to avoid a hard nose dependency and only incur the
50-
# import time overhead at actual test-time.
51-
import nose
52-
def failer(*args, **kwargs):
53-
try:
54-
# Always run the test (to generate images).
55-
result = f(*args, **kwargs)
56-
except Exception as err:
57-
if fail_condition:
58-
if known_exception_class is not None:
59-
if not isinstance(err,known_exception_class):
60-
# This is not the expected exception
61-
raise
62-
# (Keep the next ultra-long comment so in shows in console.)
63-
raise KnownFailureTest(msg) # An error here when running nose means that you don't have the matplotlib.testing.noseclasses:KnownFailure plugin in use.
64-
else:
65-
raise
66-
if fail_condition and fail_condition != 'indeterminate':
67-
raise KnownFailureDidNotFailTest(msg)
68-
return result
69-
return nose.tools.make_decorator(f)(failer)
70-
return known_fail_decorator
51+
from .nose.decorators import knownfailureif
52+
return knownfailureif(fail_condition, msg, known_exception_class)
7153

7254

7355
def _do_cleanup(original_units_registry, original_settings):
@@ -211,7 +193,7 @@ def remove_text(figure):
211193
def test(self):
212194
baseline_dir, result_dir = _image_directories(self._func)
213195
if self._style != 'classic':
214-
raise KnownFailureTest('temporarily disabled until 2.0 tag')
196+
xfail('temporarily disabled until 2.0 tag')
215197
for fignum, baseline in zip(plt.get_fignums(), self._baseline_images):
216198
for extension in self._extensions:
217199
will_fail = not extension in comparable_formats()
@@ -263,13 +245,14 @@ def do_test():
263245
'(RMS %(rms).3f)'%err)
264246
except ImageComparisonFailure:
265247
if not check_freetype_version(self._freetype_version):
266-
raise KnownFailureTest(
248+
xfail(
267249
"Mismatched version of freetype. Test requires '%s', you have '%s'" %
268250
(self._freetype_version, ft2font.__freetype_version__))
269251
raise
270252

271253
yield (do_test,)
272254

255+
273256
def image_comparison(baseline_images=None, extensions=None, tol=0,
274257
freetype_version=None, remove_text=False,
275258
savefig_kwarg=None, style='classic'):
@@ -428,7 +411,7 @@ def backend_switcher(*args, **kwargs):
428411
plt.switch_backend(prev_backend)
429412
return result
430413

431-
return nose.tools.make_decorator(func)(backend_switcher)
414+
return copy_metadata(func, backend_switcher)
432415
return switch_backend_decorator
433416

434417

@@ -447,7 +430,6 @@ def skip_if_command_unavailable(cmd):
447430
try:
448431
check_output(cmd)
449432
except:
450-
from nose import SkipTest
451-
raise SkipTest('missing command: %s' % cmd[0])
433+
skip('missing command: %s' % cmd[0])
452434

453435
return lambda f: f

lib/matplotlib/testing/exceptions.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
1-
class KnownFailureTest(Exception):
2-
"""
3-
Raise this exception to mark a test as a known failing test.
4-
"""
5-
6-
7-
class KnownFailureDidNotFailTest(Exception):
8-
"""
9-
Raise this exception to mark a test should have failed but did not.
10-
"""
11-
12-
131
class ImageComparisonFailure(AssertionError):
142
"""
153
Raise this exception to mark a test as a comparison between two images.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from __future__ import (absolute_import, division, print_function,
2+
unicode_literals)
3+
4+
5+
def get_extra_test_plugins():
6+
from .plugins.performgc import PerformGC
7+
from .plugins.knownfailure import KnownFailure
8+
from nose.plugins import attrib
9+
10+
return [PerformGC, KnownFailure, attrib.Plugin]
11+
12+
13+
def get_env():
14+
env = {'NOSE_COVER_PACKAGE': 'matplotlib',
15+
'NOSE_COVER_HTML': 1,
16+
'NOSE_COVER_NO_PRINT': 1}
17+
return env
18+
19+
20+
def check_deps():
21+
try:
22+
import nose
23+
try:
24+
from unittest import mock
25+
except ImportError:
26+
import mock
27+
except ImportError:
28+
print("matplotlib.test requires nose and mock to run.")
29+
raise
30+
31+
32+
def test(verbosity=None, coverage=False, switch_backend_warn=True, **kwargs):
33+
from ... import default_test_modules, get_backend, use
34+
35+
old_backend = get_backend()
36+
try:
37+
use('agg')
38+
import nose
39+
from nose.plugins import multiprocess
40+
41+
# Nose doesn't automatically instantiate all of the plugins in the
42+
# child processes, so we have to provide the multiprocess plugin with
43+
# a list.
44+
extra_plugins = get_extra_test_plugins()
45+
multiprocess._instantiate_plugins = extra_plugins
46+
47+
env = get_env()
48+
if coverage:
49+
env['NOSE_WITH_COVERAGE'] = 1
50+
51+
if verbosity is not None:
52+
env['NOSE_VERBOSE'] = verbosity
53+
54+
success = nose.run(
55+
addplugins=[plugin() for plugin in extra_plugins],
56+
env=env,
57+
defaultTest=default_test_modules,
58+
**kwargs
59+
)
60+
finally:
61+
if old_backend.lower() != 'agg':
62+
use(old_backend, warn=switch_backend_warn)
63+
64+
return success
65+
66+
67+
def knownfail(msg):
68+
from .exceptions import KnownFailureTest
69+
# Keep the next ultra-long comment so it shows in console.
70+
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

0 commit comments

Comments
 (0)