Skip to content

Add Py.test testing framework support #6730

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

Merged
merged 10 commits into from
Aug 23, 2016
27 changes: 23 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,13 @@ env:
- NPROC=2
- TEST_ARGS=--no-pep8
- NOSE_ARGS="--processes=$NPROC --process-timeout=300"
- PYTEST_ARGS="-ra --timeout=300 --durations=25 --cov-report= --cov=lib" # -n $NPROC
- PYTHON_ARGS=
- DELETE_FONT_CACHE=
- USE_PYTEST=false
#- PYTHONHASHSEED=0 # Workaround for pytest-xdist flaky colletion order
# # https://github.com/pytest-dev/pytest/issues/920
# # https://github.com/pytest-dev/pytest/issues/1075

matrix:
include:
Expand All @@ -60,6 +65,8 @@ matrix:
env: TEST_ARGS=--pep8
- python: 3.5
env: BUILD_DOCS=true
- python: 3.5
env: USE_PYTEST=true PANDAS=pandas DELETE_FONT_CACHE=1 TEST_ARGS=
- python: "nightly"
env: PRE=--pre
- os: osx
Expand Down Expand Up @@ -107,10 +114,14 @@ install:
# Install dependencies from pypi
pip install $PRE python-dateutil $NUMPY pyparsing!=2.1.6 $PANDAS pep8 cycler coveralls coverage
pip install $PRE pillow sphinx!=1.3.0 $MOCK numpydoc ipython colorspacious

# Install nose from a build which has partial
# support for python36 and suport for coverage output suppressing
pip install git+https://github.com/jenshnielsen/nose.git@matplotlibnose

# pytest-cov>=2.3.1 due to https://github.com/pytest-dev/pytest-cov/issues/124
pip install $PRE pytest 'pytest-cov>=2.3.1' pytest-timeout pytest-xdist pytest-faulthandler

# We manually install humor sans using the package from Ubuntu 14.10. Unfortunatly humor sans is not
# availible in the Ubuntu version used by Travis but we can manually install the deb from a later
# version since is it basically just a .ttf file
Expand Down Expand Up @@ -147,16 +158,21 @@ script:
- |
echo Testing import of tkagg backend
MPLBACKEND="tkagg" python -c 'import matplotlib.pyplot as plt; print(plt.get_backend())'
echo The following args are passed to nose $NOSE_ARGS
if [[ $BUILD_DOCS == false ]]; then
if [[ $DELETE_FONT_CACHE == 1 ]]; then
rm -rf ~/.cache/matplotlib
fi
export MPL_REPO_DIR=$PWD # needed for pep8-conformance test of the examples
if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
python tests.py $NOSE_ARGS $TEST_ARGS
if [[ $USE_PYTEST == false ]]; then
echo The following args are passed to nose $NOSE_ARGS
if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
python tests.py $NOSE_ARGS $TEST_ARGS
else
gdb -return-child-result -batch -ex r -ex bt --args python $PYTHON_ARGS tests.py $NOSE_ARGS $TEST_ARGS
fi
else
gdb -return-child-result -batch -ex r -ex bt --args python $PYTHON_ARGS tests.py $NOSE_ARGS $TEST_ARGS
echo The following args are passed to pytest $PYTEST_ARGS
py.test $PYTEST_ARGS $TEST_ARGS
fi
else
cd doc
Expand All @@ -171,6 +187,9 @@ script:
pip install $PRE requests==2.9.2 linkchecker
linkchecker build/html/index.html
fi
# Currently disabled because of differece in behaviour
# between `pytest-cov` and `nose-coverage`
#- if [[ $USE_PYTEST == true ]]; then coveralls; fi
- rm -rf $HOME/.cache/matplotlib/tex.cache
- rm -rf $HOME/.cache/matplotlib/test_cache

Expand Down
26 changes: 23 additions & 3 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ environment:
CMD_IN_ENV: "cmd /E:ON /V:ON /C obvci_appveyor_python_build_env.cmd"
# Workaround for https://github.com/conda/conda-build/issues/636
PYTHONIOENCODING: "UTF-8"
TEST_ARGS: --no-pep8
PYTEST_ARGS: -ra --timeout=300 --durations=25 #--cov-report= --cov=lib #-n %NUMBER_OF_PROCESSORS%
USE_PYTEST: no
#PYTHONHASHSEED: 0 # Workaround for pytest-xdist flaky colletion order
# # https://github.com/pytest-dev/pytest/issues/920
# # https://github.com/pytest-dev/pytest/issues/1075

matrix:
# for testing purpose: numpy 1.8 on py2.7, for the rest use 1.10/latest
Expand All @@ -38,6 +44,13 @@ environment:
PYTHON_VERSION: "3.5"
TEST_ALL: "no"
CONDA_INSTALL_LOCN: "C:\\Miniconda35-x64"
- TARGET_ARCH: "x64"
CONDA_PY: "35"
CONDA_NPY: "110"
PYTHON_VERSION: "3.5"
TEST_ALL: "no"
CONDA_INSTALL_LOCN: "C:\\Miniconda35-x64"
USE_PYTEST: yes
- TARGET_ARCH: "x86"
CONDA_PY: "27"
CONDA_NPY: "18"
Expand All @@ -58,7 +71,7 @@ platform:
build: false

init:
- cmd: "ECHO %PYTHON_VERSION% %CONDA_INSTALL_LOCN%"
- cmd: "ECHO %PYTHON_VERSION% PYTEST=%USE_PYTEST% %CONDA_INSTALL_LOCN%"

install:
- cmd: set PATH=%CONDA_INSTALL_LOCN%;%CONDA_INSTALL_LOCN%\scripts;%PATH%;
Expand All @@ -82,10 +95,15 @@ install:
# same things as the requirements in ci/conda_recipe/meta.yaml
# if conda-forge gets a new pyqt, it might be nice to install it as well to have more backends
# https://github.com/conda-forge/conda-forge.github.io/issues/157#issuecomment-223536381
- cmd: conda create -q -n test-environment python=%PYTHON_VERSION% pip setuptools numpy python-dateutil freetype=2.6 msinttypes "tk=8.5" pyparsing pytz tornado "libpng>=1.6.21,<1.7" "zlib=1.2" "cycler>=0.10" nose mock
- conda create -q -n test-environment python=%PYTHON_VERSION%
pip setuptools numpy python-dateutil freetype=2.6 msinttypes "tk=8.5"
pyparsing pytz tornado "libpng>=1.6.21,<1.7" "zlib=1.2" "cycler>=0.10"
nose mock sphinx
- activate test-environment
- cmd: echo %PYTHON_VERSION% %TARGET_ARCH%
- cmd: IF %PYTHON_VERSION% == 2.7 conda install -q functools32
# pytest-cov>=2.3.1 due to https://github.com/pytest-dev/pytest-cov/issues/124
- if x%USE_PYTEST% == xyes conda install -q pytest "pytest-cov>=2.3.1" pytest-timeout #pytest-xdist

# Let the install prefer the static builds of the libs
- set LIBRARY_LIB=%CONDA_PREFIX%\Library\lib
Expand Down Expand Up @@ -124,7 +142,9 @@ test_script:
# Test import of tkagg backend
- python -c "import matplotlib as m; m.use('tkagg'); import matplotlib.pyplot as plt; print(plt.get_backend())"
# tests
- python tests.py
- if x%USE_PYTEST% == xyes echo The following args are passed to pytest %PYTEST_ARGS%
- if x%USE_PYTEST% == xyes py.test %PYTEST_ARGS% %TEST_ARGS%
- if x%USE_PYTEST% == xno python tests.py %TEST_ARGS%
# Generate a html for visual tests
- python visual_tests.py

Expand Down
112 changes: 112 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from __future__ import (absolute_import, division, print_function,
unicode_literals)

import inspect
import os
import pytest
import unittest

import matplotlib
matplotlib.use('agg')

from matplotlib import default_test_modules
from matplotlib.testing.decorators import ImageComparisonTest


IGNORED_TESTS = {
'matplotlib': [
'test_usetex',
],
}


def blacklist_check(path):
"""Check if test is blacklisted and should be ignored"""
head, tests_dir = os.path.split(path.dirname)
if tests_dir != 'tests':
return True
head, top_module = os.path.split(head)
return path.purebasename in IGNORED_TESTS.get(top_module, [])


def whitelist_check(path):
"""Check if test is not whitelisted and should be ignored"""
left = path.dirname
last_left = None
module_path = path.purebasename
while len(left) and left != last_left:
last_left = left
left, tail = os.path.split(left)
module_path = '.'.join([tail, module_path])
if module_path in default_test_modules:
return False
return True


COLLECT_FILTERS = {
'none': lambda _: False,
'blacklist': blacklist_check,
'whitelist': whitelist_check,
}


def is_nose_class(cls):
"""Check if supplied class looks like Nose testcase"""
return any(name in ['setUp', 'tearDown']
for name, _ in inspect.getmembers(cls))


def pytest_addoption(parser):
group = parser.getgroup("matplotlib", "matplotlib custom options")

group.addoption('--collect-filter', action='store',
choices=COLLECT_FILTERS, default='blacklist',
help='filter tests during collection phase')

group.addoption('--no-pep8', action='store_true',
help='skip PEP8 compliance tests')


def pytest_configure(config):
matplotlib._called_from_pytest = True
matplotlib._init_tests()

if config.getoption('--no-pep8'):
default_test_modules.remove('matplotlib.tests.test_coding_standards')
IGNORED_TESTS['matplotlib'] += 'test_coding_standards'


def pytest_unconfigure(config):
matplotlib._called_from_pytest = False


def pytest_ignore_collect(path, config):
if path.ext == '.py':
collect_filter = config.getoption('--collect-filter')
return COLLECT_FILTERS[collect_filter](path)


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)
if setup is not None:
obj.setup_method = lambda self, _: obj.setUp(self)
tearDown = getattr(obj, 'tearDown', None)
if tearDown is not None:
obj.teardown_method = lambda self, _: obj.tearDown(self)
setUpClass = getattr(obj, 'setUpClass', None)
if setUpClass is not None:
obj.setup_class = obj.setUpClass
tearDownClass = getattr(obj, 'tearDownClass', None)
if tearDownClass is not None:
obj.teardown_class = obj.tearDownClass

return pytest.Class(name, parent=collector)
24 changes: 20 additions & 4 deletions lib/matplotlib/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import warnings
from contextlib import contextmanager

import matplotlib
from matplotlib.cbook import is_string_like, iterable
from matplotlib import rcParams, rcdefaults, use

Expand All @@ -14,16 +15,31 @@ def _is_list_like(obj):
return not is_string_like(obj) and iterable(obj)


def is_called_from_pytest():
"""Returns whether the call was done from pytest"""
return getattr(matplotlib, '_called_from_pytest', False)


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


def skip(msg=""):
"""Skip an executing test with the given message."""
from nose import SkipTest
raise SkipTest(msg)
__tracebackhide__ = True
if is_called_from_pytest():
import pytest
pytest.skip(msg)
else:
from nose import SkipTest
raise SkipTest(msg)


# stolen from pytest
Expand Down
26 changes: 18 additions & 8 deletions lib/matplotlib/testing/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from matplotlib import rcParams
from matplotlib.testing.compare import comparable_formats, compare_images, \
make_test_filename
from . import copy_metadata, skip, xfail
from . import copy_metadata, is_called_from_pytest, skip, xfail
from .exceptions import ImageComparisonFailure


Expand All @@ -37,8 +37,12 @@ def skipif(condition, *args, **kwargs):

Optionally specify a reason for better reporting.
"""
from .nose.decorators import skipif
return skipif(condition, *args, **kwargs)
if is_called_from_pytest():
import pytest
return pytest.mark.skipif(condition, *args, **kwargs)
else:
from .nose.decorators import skipif
return skipif(condition, *args, **kwargs)


def knownfailureif(fail_condition, msg=None, known_exception_class=None):
Expand All @@ -53,8 +57,14 @@ def knownfailureif(fail_condition, msg=None, known_exception_class=None):
if the exception is an instance of this class. (Default = None)

"""
from .nose.decorators import knownfailureif
return knownfailureif(fail_condition, msg, known_exception_class)
if is_called_from_pytest():
import pytest
strict = fail_condition and fail_condition != 'indeterminate'
return pytest.mark.xfail(condition=fail_condition, reason=msg,
raises=known_exception_class, strict=strict)
else:
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 @@ -198,7 +208,7 @@ def remove_text(figure):
def test(self):
baseline_dir, result_dir = _image_directories(self._func)
if self._style != 'classic':
xfail('temporarily disabled until 2.0 tag')
skip('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 @@ -228,7 +238,7 @@ def test(self):
@knownfailureif(
will_fail, fail_msg,
known_exception_class=ImageComparisonFailure)
def do_test():
def do_test(fignum, actual_fname, expected_fname):
figure = plt.figure(fignum)

if self._remove_text:
Expand All @@ -255,7 +265,7 @@ def do_test():
(self._freetype_version, ft2font.__freetype_version__))
raise

yield (do_test,)
yield do_test, fignum, actual_fname, expected_fname


def image_comparison(baseline_images=None, extensions=None, tol=0,
Expand Down
7 changes: 4 additions & 3 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
import warnings

import matplotlib
from matplotlib.testing.decorators import image_comparison, cleanup, skipif
from matplotlib.testing.decorators import image_comparison, cleanup
from matplotlib.testing import skip
import matplotlib.pyplot as plt
import matplotlib.markers as mmarkers
import matplotlib.patches as mpatches
Expand Down Expand Up @@ -86,11 +87,11 @@ def test_formatter_ticker():
ax.autoscale_view()


@skipif(LooseVersion(np.__version__) >= LooseVersion('1.11.0'),
reason="Fall out from a fixed numpy bug")
@image_comparison(baseline_images=["formatter_large_small"])
def test_formatter_large_small():
# github issue #617, pull #619
if LooseVersion(np.__version__) >= LooseVersion('1.11.0'):
skip("Fall out from a fixed numpy bug")
fig, ax = plt.subplots(1)
x = [0.500000001, 0.500000002]
y = [1e64, 1.1e64]
Expand Down
Loading