diff --git a/.travis.yml b/.travis.yml index e8e380cc8..aebc0c106 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,24 +7,26 @@ install: "pip install -U tox" # # command to run tests env: matrix: + - TESTENV=coveralls + - TESTENV=doctesting - TESTENV=flakes - TESTENV=py26 - TESTENV=py27 + - TESTENV=py27-cxfreeze + - TESTENV=py27-nobyte + - TESTENV=py27-pexpect + - TESTENV=py27-subprocess + - TESTENV=py27-trial + - TESTENV=py27-xdist + - TESTENV=py33 - TESTENV=py33 - TESTENV=py34 + - TESTENV=py34-pexpect + - TESTENV=py34-trial + - TESTENV=py34-trial + - TESTENV=py34-xdist - TESTENV=py35 - TESTENV=pypy - - TESTENV=py27-pexpect - - TESTENV=py34-pexpect - - TESTENV=py27-nobyte - - TESTENV=py27-xdist - - TESTENV=py34-xdist - - TESTENV=py27-trial - - TESTENV=py34-trial - - TESTENV=py27-subprocess - - TESTENV=doctesting - - TESTENV=py27-cxfreeze - - TESTENV=coveralls matrix: allow_failures: # py35 is currently broken on travis, see #744 diff --git a/AUTHORS b/AUTHORS index e11dcb85b..f5cb30bb9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -47,6 +47,7 @@ Marc Schlaich Mark Abramowitz Martijn Faassen Nicolas Delaby +Pieter Mulder Piotr Banaszkiewicz Punyashloka Biswal Ralf Schmitt diff --git a/CHANGELOG b/CHANGELOG index 46571fdfc..753c72ef8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,10 +1,6 @@ 2.8.0.dev (compared to 2.7.X) ----------------------------- -- fix issue744: fix for ast.Call changes in Python 3.5+. Thanks - Guido van Rossum, Matthias Bussonnier, Stefan Zimmermann and - Thomas Kluyver. - - fix issue768: docstrings found in python modules were not setting up session fixtures. Thanks Jason R. Coombs for reporting and Bruno Oliveira for the PR. @@ -27,7 +23,7 @@ - Summary bar now is colored yellow for warning situations such as: all tests either were skipped or xpass/xfailed, - or no tests were run at all (this is a partial fix for issue500). + or no tests were run at all (this is a partial fix for issue500). Thanks Eric Siegerman. - New `testdirs` ini option: list of directories to search for tests @@ -105,6 +101,34 @@ - add ``file`` and ``line`` attributes to JUnit-XML output. +2.7.3 (compared to 2.7.2) +----------------------------- + +- fix issue744: fix for ast.Call changes in Python 3.5+. Thanks + Guido van Rossum, Matthias Bussonnier, Stefan Zimmermann and + Thomas Kluyver. + +- fix issue842: applying markers in classes no longer propagate this markers + to superclasses which also have markers. + Thanks xmo-odoo for the report and Bruno Oliveira for the PR. + +- preserve warning functions after call to pytest.deprecated_call. Thanks + Pieter Mulder for PR. + +- fix issue833: --fixtures now shows all fixtures of collected test files, instead of just the + fixtures declared on the first one. + Thanks Florian Bruhin for reporting and Bruno Oliveira for the PR. + +- fix issue863: skipped tests now report the correct reason when a skip/xfail + condition is met when using multiple markers. + Thanks Raphael Pierzina for reporting and Bruno Oliveira for the PR. + +- optimized tmpdir fixture initialization, which should make test sessions + faster (specially when using pytest-xdist). The only visible effect + is that now pytest uses a subdirectory in the $TEMP directory for all + directories created by this fixture (defaults to $TEMP/pytest-$USER). + Thanks Bruno Oliveira for the PR. + 2.7.2 (compared to 2.7.1) ----------------------------- @@ -128,7 +152,7 @@ Thanks Thomas De Schampheleire for reporting and Bruno Oliveira for the PR. - fix issue718: failed to create representation of sets containing unsortable - elements in python 2. Thanks Edison Gustavo Muenz + elements in python 2. Thanks Edison Gustavo Muenz. - fix issue756, fix issue752 (and similar issues): depend on py-1.4.29 which has a refined algorithm for traceback generation. diff --git a/_pytest/mark.py b/_pytest/mark.py index 817dc72fe..9f2e05f67 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -1,4 +1,5 @@ """ generic mechanism for marking and selecting python functions. """ +import inspect import py @@ -253,15 +254,17 @@ class MarkDecorator: otherwise add *args/**kwargs in-place to mark information. """ if args and not kwargs: func = args[0] - if len(args) == 1 and (istestfunc(func) or - hasattr(func, '__bases__')): - if hasattr(func, '__bases__'): + is_class = inspect.isclass(func) + if len(args) == 1 and (istestfunc(func) or is_class): + if is_class: if hasattr(func, 'pytestmark'): - l = func.pytestmark - if not isinstance(l, list): - func.pytestmark = [l, self] - else: - l.append(self) + mark_list = func.pytestmark + if not isinstance(mark_list, list): + mark_list = [mark_list] + # always work on a copy to avoid updating pytestmark + # from a superclass by accident + mark_list = mark_list + [self] + func.pytestmark = mark_list else: func.pytestmark = [self] else: diff --git a/_pytest/python.py b/_pytest/python.py index 9c9cfe9df..2a9fa4421 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1,5 +1,6 @@ """ Python test discovery, setup and run of test functions. """ import fnmatch +import functools import py import inspect import sys @@ -22,15 +23,23 @@ callable = py.builtin.callable # used to work around a python2 exception info leak exc_clear = getattr(sys, 'exc_clear', lambda: None) - def filter_traceback(entry): return entry.path != cutdir1 and not entry.path.relto(cutdir2) -def getfslineno(obj): - # xxx let decorators etc specify a sane ordering +def get_real_func(obj): + """gets the real function object of the (possibly) wrapped object by + functools.wraps or functools.partial. + """ while hasattr(obj, "__wrapped__"): obj = obj.__wrapped__ + if isinstance(obj, functools.partial): + obj = obj.func + return obj + +def getfslineno(obj): + # xxx let decorators etc specify a sane ordering + obj = get_real_func(obj) if hasattr(obj, 'place_as'): obj = obj.place_as fslineno = py.code.getfslineno(obj) @@ -606,7 +615,7 @@ class FunctionMixin(PyobjMixin): def _prunetraceback(self, excinfo): if hasattr(self, '_obj') and not self.config.option.fulltrace: - code = py.code.Code(self.obj) + code = py.code.Code(get_real_func(self.obj)) path, firstlineno = code.path, code.firstlineno traceback = excinfo.traceback ntraceback = traceback.cut(path=path, firstlineno=firstlineno) @@ -976,21 +985,13 @@ def showfixtures(config): def _showfixtures_main(config, session): session.perform_collect() curdir = py.path.local() - if session.items: - nodeid = session.items[0].nodeid - else: - part = session._initialparts[0] - nodeid = "::".join(map(str, [curdir.bestrelpath(part[0])] + part[1:])) - nodeid.replace(session.fspath.sep, "/") - tw = py.io.TerminalWriter() verbose = config.getvalue("verbose") fm = session._fixturemanager available = [] - for argname in fm._arg2fixturedefs: - fixturedefs = fm.getfixturedefs(argname, nodeid) + for argname, fixturedefs in fm._arg2fixturedefs.items(): assert fixturedefs is not None if not fixturedefs: continue @@ -1582,7 +1583,7 @@ class FixtureLookupError(LookupError): for function in stack: fspath, lineno = getfslineno(function) try: - lines, _ = inspect.getsourcelines(function) + lines, _ = inspect.getsourcelines(get_real_func(function)) except IOError: error_msg = "file %s, line %s: source code not available" addline(error_msg % (fspath, lineno+1)) @@ -1970,7 +1971,15 @@ def getfuncargnames(function, startindex=None): if realfunction != function: startindex += num_mock_patch_args(function) function = realfunction - argnames = inspect.getargs(py.code.getrawcode(function))[0] + if isinstance(function, functools.partial): + argnames = inspect.getargs(py.code.getrawcode(function.func))[0] + partial = function + argnames = argnames[len(partial.args):] + if partial.keywords: + for kw in partial.keywords: + argnames.remove(kw) + else: + argnames = inspect.getargs(py.code.getrawcode(function))[0] defaults = getattr(function, 'func_defaults', getattr(function, '__defaults__', None)) or () numdefaults = len(defaults) diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 482b78b0e..875cb510e 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -45,8 +45,8 @@ def deprecated_call(func, *args, **kwargs): try: ret = func(*args, **kwargs) finally: - warnings.warn_explicit = warn_explicit - warnings.warn = warn + warnings.warn_explicit = oldwarn_explicit + warnings.warn = oldwarn if not l: __tracebackhide__ = True raise AssertionError("%r did not produce DeprecationWarning" %(func,)) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index d22d758de..36e54d7d8 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -98,24 +98,36 @@ class MarkEvaluator: return d def _istrue(self): + if hasattr(self, 'result'): + return self.result if self.holder: d = self._getglobals() if self.holder.args: self.result = False - for expr in self.holder.args: - self.expr = expr - if isinstance(expr, py.builtin._basestring): - result = cached_eval(self.item.config, expr, d) - else: - if self.get("reason") is None: - # XXX better be checked at collection time - pytest.fail("you need to specify reason=STRING " - "when using booleans as conditions.") - result = bool(expr) - if result: - self.result = True + # "holder" might be a MarkInfo or a MarkDecorator; only + # MarkInfo keeps track of all parameters it received in an + # _arglist attribute + if hasattr(self.holder, '_arglist'): + arglist = self.holder._arglist + else: + arglist = [(self.holder.args, self.holder.kwargs)] + for args, kwargs in arglist: + for expr in args: self.expr = expr - break + if isinstance(expr, py.builtin._basestring): + result = cached_eval(self.item.config, expr, d) + else: + if "reason" not in kwargs: + # XXX better be checked at collection time + msg = "you need to specify reason=STRING " \ + "when using booleans as conditions." + pytest.fail(msg) + result = bool(expr) + if result: + self.result = True + self.reason = kwargs.get('reason', None) + self.expr = expr + return self.result else: self.result = True return getattr(self, 'result', False) @@ -124,7 +136,7 @@ class MarkEvaluator: return self.holder.kwargs.get(attr, default) def getexplanation(self): - expl = self.get('reason', None) + expl = getattr(self, 'reason', None) or self.get('reason', None) if not expl: if not hasattr(self, 'expr'): return "" diff --git a/_pytest/tmpdir.py b/_pytest/tmpdir.py index 6acac97b7..9f9c4da6b 100644 --- a/_pytest/tmpdir.py +++ b/_pytest/tmpdir.py @@ -52,7 +52,14 @@ class TempdirFactory: basetemp.remove() basetemp.mkdir() else: - basetemp = py.path.local.make_numbered_dir(prefix='pytest-') + # use a sub-directory in the temproot to speed-up + # make_numbered_dir() call + import getpass + temproot = py.path.local.get_temproot() + rootdir = temproot.join('pytest-%s' % getpass.getuser()) + rootdir.ensure(dir=1) + basetemp = py.path.local.make_numbered_dir(prefix='pytest-', + rootdir=rootdir) self._basetemp = t = basetemp.realpath() self.trace("new basetemp", t) return t diff --git a/doc/en/faq.txt b/doc/en/faq.txt index 2f7bd17d2..88ae460e5 100644 --- a/doc/en/faq.txt +++ b/doc/en/faq.txt @@ -106,15 +106,16 @@ Is using pytest fixtures versus xUnit setup a style question? For simple applications and for people experienced with nose_ or unittest-style test setup using `xUnit style setup`_ probably feels natural. For larger test suites, parametrized testing -or setup of complex test resources using funcargs_ may feel more natural. -Moreover, funcargs are ideal for writing advanced test support -code (like e.g. the monkeypatch_, the tmpdir_ or capture_ funcargs) +or setup of complex test resources using fixtures_ may feel more natural. +Moreover, fixtures are ideal for writing advanced test support +code (like e.g. the monkeypatch_, the tmpdir_ or capture_ fixtures) because the support code can register setup/teardown functions in a managed class/module/function scope. .. _monkeypatch: monkeypatch.html .. _tmpdir: tmpdir.html .. _capture: capture.html +.. _fixtures: fixture.html .. _`why pytest_pyfuncarg__ methods?`: diff --git a/doc/en/links.inc b/doc/en/links.inc index 76a2bfd32..3d7863751 100644 --- a/doc/en/links.inc +++ b/doc/en/links.inc @@ -6,7 +6,7 @@ .. _`pytest_nose`: plugin/nose.html .. _`reStructured Text`: http://docutils.sourceforge.net .. _`Python debugger`: http://docs.python.org/lib/module-pdb.html -.. _nose: http://somethingaboutorange.com/mrl/projects/nose/ +.. _nose: https://nose.readthedocs.org/en/latest/ .. _pytest: http://pypi.python.org/pypi/pytest .. _mercurial: http://mercurial.selenic.com/wiki/ .. _`setuptools`: http://pypi.python.org/pypi/setuptools diff --git a/testing/python/collect.py b/testing/python/collect.py index 67dc99c8b..dc073b103 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -865,3 +865,47 @@ def test_unorderable_types(testdir): result = testdir.runpytest() assert "TypeError" not in result.stdout.str() assert result.ret == 0 + + +def test_collect_functools_partial(testdir): + """ + Test that collection of functools.partial object works, and arguments + to the wrapped functions are dealt correctly (see #811). + """ + testdir.makepyfile(""" + import functools + import pytest + + @pytest.fixture + def fix1(): + return 'fix1' + + @pytest.fixture + def fix2(): + return 'fix2' + + def check1(i, fix1): + assert i == 2 + assert fix1 == 'fix1' + + def check2(fix1, i): + assert i == 2 + assert fix1 == 'fix1' + + def check3(fix1, i, fix2): + assert i == 2 + assert fix1 == 'fix1' + assert fix2 == 'fix2' + + test_ok_1 = functools.partial(check1, i=2) + test_ok_2 = functools.partial(check1, i=2, fix1='fix1') + test_ok_3 = functools.partial(check1, 2) + test_ok_4 = functools.partial(check2, i=2) + test_ok_5 = functools.partial(check3, i=2) + test_ok_6 = functools.partial(check3, i=2, fix1='fix1') + + test_fail_1 = functools.partial(check2, 2) + test_fail_2 = functools.partial(check3, 2) + """) + result = testdir.inline_run() + result.assertoutcome(passed=6, failed=2) diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 2678a07e7..efa0eb6af 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -2482,6 +2482,44 @@ class TestShowFixtures: """) + def test_show_fixtures_different_files(self, testdir): + """ + #833: --fixtures only shows fixtures from first file + """ + testdir.makepyfile(test_a=''' + import pytest + + @pytest.fixture + def fix_a(): + """Fixture A""" + pass + + def test_a(fix_a): + pass + ''') + testdir.makepyfile(test_b=''' + import pytest + + @pytest.fixture + def fix_b(): + """Fixture B""" + pass + + def test_b(fix_b): + pass + ''') + result = testdir.runpytest("--fixtures") + result.stdout.fnmatch_lines(""" + * fixtures defined from test_a * + fix_a + Fixture A + + * fixtures defined from test_b * + fix_b + Fixture B + """) + + class TestContextManagerFixtureFuncs: def test_simple(self, testdir): testdir.makepyfile(""" diff --git a/testing/test_mark.py b/testing/test_mark.py index eb2e10f3d..ec63bedf7 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -369,6 +369,45 @@ class TestFunctional: print (item, item.keywords) assert 'a' in item.keywords + def test_mark_decorator_subclass_does_not_propagate_to_base(self, testdir): + p = testdir.makepyfile(""" + import pytest + + @pytest.mark.a + class Base: pass + + @pytest.mark.b + class Test1(Base): + def test_foo(self): pass + + class Test2(Base): + def test_bar(self): pass + """) + items, rec = testdir.inline_genitems(p) + self.assert_markers(items, test_foo=('a', 'b'), test_bar=('a',)) + + def test_mark_decorator_baseclasses_merged(self, testdir): + p = testdir.makepyfile(""" + import pytest + + @pytest.mark.a + class Base: pass + + @pytest.mark.b + class Base2(Base): pass + + @pytest.mark.c + class Test1(Base2): + def test_foo(self): pass + + class Test2(Base2): + @pytest.mark.d + def test_bar(self): pass + """) + items, rec = testdir.inline_genitems(p) + self.assert_markers(items, test_foo=('a', 'b', 'c'), + test_bar=('a', 'b', 'd')) + def test_mark_with_wrong_marker(self, testdir): reprec = testdir.inline_runsource(""" import pytest @@ -477,6 +516,22 @@ class TestFunctional: reprec = testdir.inline_run("-m", "mark1") reprec.assertoutcome(passed=1) + def assert_markers(self, items, **expected): + """assert that given items have expected marker names applied to them. + expected should be a dict of (item name -> seq of expected marker names) + + .. note:: this could be moved to ``testdir`` if proven to be useful + to other modules. + """ + from _pytest.mark import MarkInfo + items = dict((x.name, x) for x in items) + for name, expected_markers in expected.items(): + markers = items[name].keywords._markers + marker_names = set([name for (name, v) in markers.items() + if isinstance(v, MarkInfo)]) + assert marker_names == set(expected_markers) + + class TestKeywordSelection: def test_select_simple(self, testdir): file_test = testdir.makepyfile(""" diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 5a2bf92fa..21909ae2a 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -63,12 +63,16 @@ def test_deprecated_call_ret(): assert ret == 42 def test_deprecated_call_preserves(): - r = py.std.warnings.onceregistry.copy() - f = py.std.warnings.filters[:] + onceregistry = py.std.warnings.onceregistry.copy() + filters = py.std.warnings.filters[:] + warn = py.std.warnings.warn + warn_explicit = py.std.warnings.warn_explicit test_deprecated_call_raises() test_deprecated_call() - assert r == py.std.warnings.onceregistry - assert f == py.std.warnings.filters + assert onceregistry == py.std.warnings.onceregistry + assert filters == py.std.warnings.filters + assert warn is py.std.warnings.warn + assert warn_explicit is py.std.warnings.warn_explicit def test_deprecated_explicit_call_raises(): pytest.raises(AssertionError, diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 00f6833e6..6e827cb46 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -409,6 +409,26 @@ class TestSkipif: ]) assert result.ret == 0 + @pytest.mark.parametrize('marker, msg1, msg2', [ + ('skipif', 'SKIP', 'skipped'), + ('xfail', 'XPASS', 'xpassed'), + ]) + def test_skipif_reporting_multiple(self, testdir, marker, msg1, msg2): + testdir.makepyfile(test_foo=""" + import pytest + @pytest.mark.{marker}(False, reason='first_condition') + @pytest.mark.{marker}(True, reason='second_condition') + def test_foobar(): + assert 1 + """.format(marker=marker)) + result = testdir.runpytest('-s', '-rsxX') + result.stdout.fnmatch_lines([ + "*{msg1}*test_foo.py*second_condition*".format(msg1=msg1), + "*1 {msg2}*".format(msg2=msg2), + ]) + assert result.ret == 0 + + def test_skip_not_report_default(testdir): p = testdir.makepyfile(test_one=""" import pytest diff --git a/tox.ini b/tox.ini index 730f4ccd4..1a7079da8 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ envlist= [testenv] commands= py.test --lsof -rfsxX {posargs:testing} +passenv = USER USERNAME deps= nose mock @@ -18,6 +19,7 @@ deps= nose mock<1.1 # last supported version for py26 +<<<<<<< HEAD [testenv:py27-subprocess] changedir=. basepython=python2.7 @@ -27,6 +29,8 @@ deps=pytest-xdist commands= py.test -n3 -rfsxX --runpytest=subprocess {posargs:testing} +======= +>>>>>>> pytest-2.7 [testenv:genscript] commands= py.test --genscript=pytest1