diff --git a/.travis.yml b/.travis.yml index f701302d9..41537a466 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,34 +8,37 @@ install: "pip install -U tox" env: matrix: # coveralls is not listed in tox's envlist, but should run in travis - - TESTENV=coveralls + - TOXENV=coveralls # note: please use "tox --listenvs" to populate the build matrix below - - TESTENV=linting - - TESTENV=py26 - - TESTENV=py27 - - TESTENV=py33 - - TESTENV=py34 - - TESTENV=py35 - - TESTENV=pypy - - TESTENV=py27-pexpect - - TESTENV=py27-xdist - - TESTENV=py27-trial - - TESTENV=py35-pexpect - - TESTENV=py35-xdist - - TESTENV=py35-trial - - TESTENV=py27-nobyte - - TESTENV=doctesting - - TESTENV=freeze - - TESTENV=docs + - TOXENV=linting + - TOXENV=py26 + - TOXENV=py27 + - TOXENV=py33 + - TOXENV=py34 + - TOXENV=py35 + - TOXENV=pypy + - TOXENV=py27-pexpect + - TOXENV=py27-xdist + - TOXENV=py27-trial + - TOXENV=py35-pexpect + - TOXENV=py35-xdist + - TOXENV=py35-trial + - TOXENV=py27-nobyte + - TOXENV=doctesting + - TOXENV=freeze + - TOXENV=docs matrix: include: - - env: TESTENV=py36 + - env: TOXENV=py36 python: '3.6-dev' - - env: TESTENV=py37 + - env: TOXENV=py37 + python: 'nightly' + allow_failures: + - env: TOXENV=py37 python: 'nightly' -script: tox --recreate -e $TESTENV +script: tox --recreate notifications: irc: diff --git a/AUTHORS b/AUTHORS index d17963e73..3d59906b2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -17,6 +17,7 @@ Anthony Sottile Armin Rigo Aron Curzon Aviv Palivoda +Barney Gale Ben Webb Benjamin Peterson Bernard Pratz @@ -43,6 +44,7 @@ Dave Hunt David Díaz-Barquero David Mohr David Vierra +Denis Kirisov Diego Russo Dmitry Dygalo Duncan Betts @@ -117,11 +119,14 @@ Nicolas Delaby Oleg Pidsadnyi Oliver Bestwalter Omar Kohl +Omer Hadari +Patrick Hayes Pieter Mulder Piotr Banaszkiewicz Punyashloka Biswal Quentin Pradet Ralf Schmitt +Ran Benita Raphael Pierzina Raquel Alegre Ravi Chandra @@ -148,5 +153,6 @@ Tyler Goodlet Vasily Kuznetsov Victor Uriarte Vlad Dragos +Vidar T. Fauske Wouter van Ackooy Xuecong Liao diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 44ac7baf3..472c79696 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,11 @@ Changes ``__test__`` attribute to ``False`` in the class body (`#2007`_). Thanks to `@syre`_ for the report and `@lwm`_ for the PR. +* Change junitxml.py to produce reports that comply with Junitxml schema. + If the same test fails with failure in call and then errors in teardown + we split testcase element into two, one containing the error and the other + the failure. (`#2228`_) Thanks to `@kkoukiou`_ for the PR. + * Testcase reports with a ``url`` attribute will now properly write this to junitxml. Thanks `@fushi`_ for the PR (`#1874`_). @@ -73,6 +78,7 @@ Bug Fixes .. _@unsignedint: https://github.com/unsignedint .. _@Kriechi: https://github.com/Kriechi + .. _#1407: https://github.com/pytest-dev/pytest/issues/1407 .. _#1512: https://github.com/pytest-dev/pytest/issues/1512 .. _#1874: https://github.com/pytest-dev/pytest/pull/1874 @@ -83,24 +89,65 @@ Bug Fixes .. _#2166: https://github.com/pytest-dev/pytest/pull/2166 .. _#2147: https://github.com/pytest-dev/pytest/issues/2147 .. _#2208: https://github.com/pytest-dev/pytest/issues/2208 +.. _#2228: https://github.com/pytest-dev/pytest/issues/2228 3.0.7 (unreleased) -======================= +================== -* Change junitxml.py to produce reports that comply with Junitxml schema. - If the same test fails with failure in call and then errors in teardown - we split testcase element into two, one containing the error and the other - the failure. (`#2228`_) Thanks to `@kkoukiou`_ for the PR. - -* - -* + +* Fix issue in assertion rewriting breaking due to modules silently discarding + other modules when importing fails + Notably, importing the `anydbm` module is fixed. (`#2248`_). + Thanks `@pfhayes`_ for the PR. + +* junitxml: Fix problematic case where system-out tag occured twice per testcase + element in the XML report. Thanks `@kkoukiou`_ for the PR. + +* Fix regression, pytest now skips unittest correctly if run with ``--pdb`` + (`#2137`_). Thanks to `@gst`_ for the report and `@mbyt`_ for the PR. + +* Ignore exceptions raised from descriptors (e.g. properties) during Python test collection (`#2234`_). + Thanks to `@bluetech`_. + +* ``--override-ini`` now correctly overrides some fundamental options like ``python_files`` (`#2238`_). + Thanks `@sirex`_ for the report and `@nicoddemus`_ for the PR. + +* Replace ``raise StopIteration`` usages in the code by simple ``returns`` to finish generators, in accordance to `PEP-479`_ (`#2160`_). + Thanks `@tgoodlet`_ for the report and `@nicoddemus`_ for the PR. + +* Fix internal errors when an unprintable ``AssertionError`` is raised inside a test. + Thanks `@omerhadari`_ for the PR. + +* Skipping plugin now also works with test items generated by custom collectors (`#2231`_). + Thanks to `@vidartf`_. + +* Fix trailing whitespace in console output if no .ini file presented (`#2281`_). Thanks `@fbjorn`_ for the PR. + +* Conditionless ``xfail`` markers no longer rely on the underlying test item + being an instance of ``PyobjMixin``, and can therefore apply to tests not + collected by the built-in python test collector. Thanks `@barneygale`_ for the + PR. * +.. _@pfhayes: https://github.com/pfhayes +.. _@bluetech: https://github.com/bluetech +.. _@gst: https://github.com/gst +.. _@sirex: https://github.com/sirex +.. _@vidartf: https://github.com/vidartf .. _@kkoukiou: https://github.com/KKoukiou +.. _@omerhadari: https://github.com/omerhadari +.. _@fbjorn: https://github.com/fbjorn -.. _#2228: https://github.com/pytest-dev/pytest/issues/2228 +.. _#2248: https://github.com/pytest-dev/pytest/issues/2248 +.. _#2137: https://github.com/pytest-dev/pytest/issues/2137 +.. _#2160: https://github.com/pytest-dev/pytest/issues/2160 +.. _#2231: https://github.com/pytest-dev/pytest/issues/2231 +.. _#2234: https://github.com/pytest-dev/pytest/issues/2234 +.. _#2238: https://github.com/pytest-dev/pytest/issues/2238 +.. _#2281: https://github.com/pytest-dev/pytest/issues/2281 + +.. _PEP-479: https://www.python.org/dev/peps/pep-0479/ 3.0.6 (2017-01-22) @@ -135,6 +182,7 @@ Bug Fixes terminal output it relies on is missing. Thanks to `@eli-b`_ for the PR. +.. _@barneygale: https://github.com/barneygale .. _@lesteve: https://github.com/lesteve .. _@malinoff: https://github.com/malinoff .. _@pelme: https://github.com/pelme @@ -2467,7 +2515,7 @@ Bug fixes: teardown function are called earlier. - add an all-powerful metafunc.parametrize function which allows to parametrize test function arguments in multiple steps and therefore - from indepdenent plugins and palces. + from independent plugins and places. - add a @pytest.mark.parametrize helper which allows to easily call a test function with different argument values - Add examples to the "parametrize" example page, including a quick port diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py index 616d5c431..6eceb0c7f 100644 --- a/_pytest/_code/code.py +++ b/_pytest/_code/code.py @@ -352,6 +352,8 @@ class ExceptionInfo(object): help for navigating the traceback. """ _striptext = '' + _assert_start_repr = "AssertionError(u\'assert " if sys.version_info[0] < 3 else "AssertionError(\'assert " + def __init__(self, tup=None, exprinfo=None): import _pytest._code if tup is None: @@ -359,8 +361,8 @@ class ExceptionInfo(object): if exprinfo is None and isinstance(tup[1], AssertionError): exprinfo = getattr(tup[1], 'msg', None) if exprinfo is None: - exprinfo = py._builtin._totext(tup[1]) - if exprinfo and exprinfo.startswith('assert '): + exprinfo = py.io.saferepr(tup[1]) + if exprinfo and exprinfo.startswith(self._assert_start_repr): self._striptext = 'AssertionError: ' self._excinfo = tup #: the exception class diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index abf5b491f..7408c4746 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -215,7 +215,8 @@ class AssertionRewritingHook(object): mod.__loader__ = self py.builtin.exec_(co, mod.__dict__) except: - del sys.modules[name] + if name in sys.modules: + del sys.modules[name] raise return sys.modules[name] diff --git a/_pytest/cacheprovider.py b/_pytest/cacheprovider.py index cd09f4a02..782fdadd9 100755 --- a/_pytest/cacheprovider.py +++ b/_pytest/cacheprovider.py @@ -1,7 +1,7 @@ """ merged implementation of the cache provider -the name cache was not choosen to ensure pluggy automatically +the name cache was not chosen to ensure pluggy automatically ignores the external pytest-cache """ diff --git a/_pytest/config.py b/_pytest/config.py index 81f39cda4..6a607c7e3 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -877,6 +877,7 @@ class Config(object): self.trace = self.pluginmanager.trace.root.get("config") self.hook = self.pluginmanager.hook self._inicache = {} + self._override_ini = () self._opt2dest = {} self._cleanup = [] self._warn = self.pluginmanager._warn @@ -977,6 +978,7 @@ class Config(object): self.invocation_dir = py.path.local() self._parser.addini('addopts', 'extra command line options', 'args') self._parser.addini('minversion', 'minimally required pytest version') + self._override_ini = ns.override_ini or () def _consider_importhook(self, args, entrypoint_name): """Install the PEP 302 import hook if using assertion re-writing. @@ -1159,15 +1161,14 @@ class Config(object): # and -o foo1=bar1 -o foo2=bar2 options # always use the last item if multiple value set for same ini-name, # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2 - if self.getoption("override_ini", None): - for ini_config_list in self.option.override_ini: - for ini_config in ini_config_list: - try: - (key, user_ini_value) = ini_config.split("=", 1) - except ValueError: - raise UsageError("-o/--override-ini expects option=value style.") - if key == name: - value = user_ini_value + for ini_config_list in self._override_ini: + for ini_config in ini_config_list: + try: + (key, user_ini_value) = ini_config.split("=", 1) + except ValueError: + raise UsageError("-o/--override-ini expects option=value style.") + if key == name: + value = user_ini_value return value def getoption(self, name, default=notset, skip=False): diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index f675217f8..c4d21635f 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -14,6 +14,7 @@ from _pytest.compat import ( getfslineno, get_real_func, is_generator, isclass, getimfunc, getlocation, getfuncargnames, + safe_getattr, ) def pytest_sessionstart(session): @@ -124,8 +125,6 @@ def getfixturemarker(obj): exceptions.""" try: return getattr(obj, "_pytestfixturefunction", None) - except KeyboardInterrupt: - raise except Exception: # some objects raise errors like request (from flask import request) # we don't expect them to be fixture functions @@ -1068,7 +1067,9 @@ class FixtureManager(object): self._holderobjseen.add(holderobj) autousenames = [] for name in dir(holderobj): - obj = getattr(holderobj, name, None) + # The attribute can be an arbitrary descriptor, so the attribute + # access below can raise. safe_getatt() ignores such exceptions. + obj = safe_getattr(holderobj, name, None) # fixture functions have a pytest_funcarg__ prefix (pre-2.3 style) # or are "@pytest.fixture" marked marker = getfixturemarker(obj) diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index bb382a597..403f823a0 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -247,7 +247,7 @@ def pytest_unconfigure(config): # ------------------------------------------------------------------------- -# hooks for customising the assert methods +# hooks for customizing the assert methods # ------------------------------------------------------------------------- def pytest_assertrepr_compare(config, op, left, right): @@ -256,7 +256,7 @@ def pytest_assertrepr_compare(config, op, left, right): Return None for no custom explanation, otherwise return a list of strings. The strings will be joined by newlines but any newlines *in* a string will be escaped. Note that all but the first line will - be indented sligthly, the intention is for the first line to be a summary. + be indented slightly, the intention is for the first line to be a summary. """ # ------------------------------------------------------------------------- @@ -264,7 +264,14 @@ def pytest_assertrepr_compare(config, op, left, right): # ------------------------------------------------------------------------- def pytest_report_header(config, startdir): - """ return a string to be displayed as header info for terminal reporting.""" + """ return a string to be displayed as header info for terminal reporting. + + .. note:: + + This function should be implemented only in plugins or ``conftest.py`` + files situated at the tests root directory due to how pytest + :ref:`discovers plugins during startup `. + """ @hookspec(firstresult=True) def pytest_report_teststatus(report): diff --git a/_pytest/junitxml.py b/_pytest/junitxml.py index 4f7792aec..e5f20b6f6 100644 --- a/_pytest/junitxml.py +++ b/_pytest/junitxml.py @@ -121,7 +121,7 @@ class _NodeReporter(object): node = kind(data, message=message) self.append(node) - def _write_captured_output(self, report): + def write_captured_output(self, report): for capname in ('out', 'err'): content = getattr(report, 'capstd' + capname) if content: @@ -130,7 +130,6 @@ class _NodeReporter(object): def append_pass(self, report): self.add_stats('passed') - self._write_captured_output(report) def append_failure(self, report): # msg = str(report.longrepr.reprtraceback.extraline) @@ -149,7 +148,6 @@ class _NodeReporter(object): fail = Junit.failure(message=message) fail.append(bin_xml_escape(report.longrepr)) self.append(fail) - self._write_captured_output(report) def append_collect_error(self, report): # msg = str(report.longrepr.reprtraceback.extraline) @@ -167,7 +165,6 @@ class _NodeReporter(object): msg = "test setup failure" self._add_simple( Junit.error, msg, report.longrepr) - self._write_captured_output(report) def append_skipped(self, report): if hasattr(report, "wasxfail"): @@ -182,7 +179,7 @@ class _NodeReporter(object): Junit.skipped("%s:%s: %s" % (filename, lineno, skipreason), type="pytest.skip", message=skipreason)) - self._write_captured_output(report) + self.write_captured_output(report) def finalize(self): data = self.to_xml().unicode(indent=0) @@ -369,6 +366,8 @@ class LogXML(object): reporter.append_skipped(report) self.update_testcase_duration(report) if report.when == "teardown": + reporter = self._opentestcase(report) + reporter.write_captured_output(report) self.finalize(report) report_wid = getattr(report, "worker_id", None) report_ii = getattr(report, "item_index", None) diff --git a/_pytest/main.py b/_pytest/main.py index 73858e099..f100b7974 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -81,7 +81,7 @@ def pytest_namespace(): def pytest_configure(config): - pytest.config = config # compatibiltiy + pytest.config = config # compatibility def wrap_session(config, doit): diff --git a/_pytest/mark.py b/_pytest/mark.py index ef9a94ad9..582eb1277 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -72,7 +72,7 @@ def pytest_collection_modifyitems(items, config): return # pytest used to allow "-" for negating # but today we just allow "-" at the beginning, use "not" instead - # we probably remove "-" alltogether soon + # we probably remove "-" altogether soon if keywordexpr.startswith("-"): keywordexpr = "not " + keywordexpr[1:] selectuntil = False diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 17cba5700..97fc62312 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -335,7 +335,7 @@ def testdir(request, tmpdir_factory): return Testdir(request, tmpdir_factory) -rex_outcome = re.compile("(\d+) ([\w-]+)") +rex_outcome = re.compile(r"(\d+) ([\w-]+)") class RunResult(object): """The result of running a command. @@ -570,7 +570,7 @@ class Testdir(object): def mkpydir(self, name): """Create a new python package. - This creates a (sub)direcotry with an empty ``__init__.py`` + This creates a (sub)directory with an empty ``__init__.py`` file so that is recognised as a python package. """ @@ -665,7 +665,7 @@ class Testdir(object): def inline_genitems(self, *args): """Run ``pytest.main(['--collectonly'])`` in-process. - Retuns a tuple of the collected items and a + Returns a tuple of the collected items and a :py:class:`HookRecorder` instance. This runs the :py:func:`pytest.main` function to run all of @@ -863,7 +863,7 @@ class Testdir(object): :py:meth:`parseconfigure`. :param withinit: Whether to also write a ``__init__.py`` file - to the temporarly directory to ensure it is a package. + to the temporary directory to ensure it is a package. """ kw = {self.request.function.__name__: Source(source).strip()} diff --git a/_pytest/python.py b/_pytest/python.py index ff4edff5b..471a9563f 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -174,7 +174,7 @@ def pytest_pycollect_makeitem(collector, name, obj): outcome = yield res = outcome.get_result() if res is not None: - raise StopIteration + return # nothing was collected elsewhere, let's do it here if isclass(obj): if collector.istestclass(obj, name): @@ -632,7 +632,7 @@ class Generator(FunctionMixin, PyCollector): def getcallargs(self, obj): if not isinstance(obj, (tuple, list)): obj = (obj,) - # explict naming + # explicit naming if isinstance(obj[0], py.builtin._basestring): name = obj[0] obj = obj[1:] diff --git a/_pytest/skipping.py b/_pytest/skipping.py index a005bc7c2..86176acaf 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -112,14 +112,14 @@ class MarkEvaluator(object): def _getglobals(self): d = {'os': os, 'sys': sys, 'config': self.item.config} - d.update(self.item.obj.__globals__) + if hasattr(self.item, 'obj'): + d.update(self.item.obj.__globals__) return d def _istrue(self): if hasattr(self, 'result'): return self.result if self.holder: - d = self._getglobals() if self.holder.args or 'condition' in self.holder.kwargs: self.result = False # "holder" might be a MarkInfo or a MarkDecorator; only @@ -133,6 +133,7 @@ class MarkEvaluator(object): for expr in args: self.expr = expr if isinstance(expr, py.builtin._basestring): + d = self._getglobals() result = cached_eval(self.item.config, expr, d) else: if "reason" not in kwargs: diff --git a/_pytest/terminal.py b/_pytest/terminal.py index f509283cb..b07945426 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -295,8 +295,8 @@ class TerminalReporter(object): def pytest_report_header(self, config): inifile = "" if config.inifile: - inifile = config.rootdir.bestrelpath(config.inifile) - lines = ["rootdir: %s, inifile: %s" %(config.rootdir, inifile)] + inifile = " " + config.rootdir.bestrelpath(config.inifile) + lines = ["rootdir: %s, inifile:%s" % (config.rootdir, inifile)] plugininfo = config.pluginmanager.list_plugin_distinfo() if plugininfo: diff --git a/_pytest/tmpdir.py b/_pytest/tmpdir.py index 565c35cf9..67f999e5a 100644 --- a/_pytest/tmpdir.py +++ b/_pytest/tmpdir.py @@ -116,7 +116,7 @@ def tmpdir(request, tmpdir_factory): path object. """ name = request.node.name - name = re.sub("[\W]", "_", name) + name = re.sub(r"[\W]", "_", name) MAXVAL = 30 if len(name) > MAXVAL: name = name[:MAXVAL] diff --git a/_pytest/unittest.py b/_pytest/unittest.py index 73224010b..276b9ba16 100644 --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -5,7 +5,7 @@ import sys import traceback import pytest -# for transfering markers +# for transferring markers import _pytest._code from _pytest.python import transfer_markers from _pytest.skipping import MarkEvaluator @@ -65,7 +65,6 @@ class UnitTestCase(pytest.Class): yield TestCaseFunction('runTest', parent=self) - class TestCaseFunction(pytest.Function): _excinfo = None @@ -152,14 +151,33 @@ class TestCaseFunction(pytest.Function): def stopTest(self, testcase): pass + def _handle_skip(self): + # implements the skipping machinery (see #2137) + # analog to pythons Lib/unittest/case.py:run + testMethod = getattr(self._testcase, self._testcase._testMethodName) + if (getattr(self._testcase.__class__, "__unittest_skip__", False) or + getattr(testMethod, "__unittest_skip__", False)): + # If the class or method was skipped. + skip_why = (getattr(self._testcase.__class__, '__unittest_skip_why__', '') or + getattr(testMethod, '__unittest_skip_why__', '')) + try: # PY3, unittest2 on PY2 + self._testcase._addSkip(self, self._testcase, skip_why) + except TypeError: # PY2 + if sys.version_info[0] != 2: + raise + self._testcase._addSkip(self, skip_why) + return True + return False + def runtest(self): if self.config.pluginmanager.get_plugin("pdbinvoke") is None: self._testcase(result=self) else: # disables tearDown and cleanups for post mortem debugging (see #1890) + if self._handle_skip(): + return self._testcase.debug() - def _prunetraceback(self, excinfo): pytest.Function._prunetraceback(self, excinfo) traceback = excinfo.traceback.filter( diff --git a/doc/en/assert.rst b/doc/en/assert.rst index 5ec07a78e..ef8636142 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -287,5 +287,5 @@ For further information, Benjamin Peterson wrote up `Behind the scenes of pytest ``--nomagic``. .. versionchanged:: 3.0 - Removes the ``--no-assert`` and``--nomagic`` options. + Removes the ``--no-assert`` and ``--nomagic`` options. Removes the ``--assert=reinterp`` option. diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 6f88aad73..bd2c35c67 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -243,7 +243,9 @@ Fixture finalization / executing teardown code pytest supports execution of fixture specific finalization code when the fixture goes out of scope. By using a ``yield`` statement instead of ``return``, all -the code after the *yield* statement serves as the teardown code.:: +the code after the *yield* statement serves as the teardown code: + +.. code-block:: python # content of conftest.py @@ -275,22 +277,23 @@ occur around each single test. In either case the test module itself does not need to change or know about these details of fixture setup. -Note that we can also seamlessly use the ``yield`` syntax with ``with`` statements:: +Note that we can also seamlessly use the ``yield`` syntax with ``with`` statements: + +.. code-block:: python # content of test_yield2.py + import smtplib import pytest - @pytest.fixture - def passwd(): - with open("/etc/passwd") as f: - yield f.readlines() + @pytest.fixture(scope="module") + def smtp(request): + with smtplib.SMTP("smtp.gmail.com") as smtp: + yield smtp # provide the fixture value - def test_has_lines(passwd): - assert len(passwd) >= 1 -The file ``f`` will be closed after the test finished execution -because the Python ``file`` object supports finalization when +The ``smtp`` connection will be closed after the test finished execution +because the ``smtp`` object automatically closes when the ``with`` statement ends. diff --git a/doc/en/talks.rst b/doc/en/talks.rst index c35fba0b0..85faf6455 100644 --- a/doc/en/talks.rst +++ b/doc/en/talks.rst @@ -4,7 +4,9 @@ Talks and Tutorials .. sidebar:: Next Open Trainings - `pytest workshop `_, 8th December 2016, Bern, Switzerland + `Professional Testing with Python + `_, + 26-28 April 2017, Leipzig, Germany. .. _`funcargs`: funcargs.html diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 770f81e46..f6ed6e4e3 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -236,20 +236,31 @@ import ``helper.py`` normally. The contents of Requiring/Loading plugins in a test module or conftest file ----------------------------------------------------------- -You can require plugins in a test module or a conftest file like this:: +You can require plugins in a test module or a ``conftest.py`` file like this: - pytest_plugins = "name1", "name2", +.. code-block:: python + + pytest_plugins = ["name1", "name2"] When the test module or conftest plugin is loaded the specified plugins -will be loaded as well. You can also use dotted path like this:: +will be loaded as well. Any module can be blessed as a plugin, including internal +application modules: + +.. code-block:: python pytest_plugins = "myapp.testsupport.myplugin" -which will import the specified module as a ``pytest`` plugin. +``pytest_plugins`` variables are processed recursively, so note that in the example above +if ``myapp.testsupport.myplugin`` also declares ``pytest_plugins``, the contents +of the variable will also be loaded as plugins, and so on. -Plugins imported like this will automatically be marked to require -assertion rewriting using the :func:`pytest.register_assert_rewrite` -mechanism. However for this to have any effect the module must not be +This mechanism makes it easy to share fixtures within applications or even +external applications without the need to create external plugins using +the ``setuptools``'s entry point technique. + +Plugins imported by ``pytest_plugins`` will also automatically be marked +for assertion rewriting (see :func:`pytest.register_assert_rewrite`). +However for this to have any effect the module must not be imported already; if it was already imported at the time the ``pytest_plugins`` statement is processed, a warning will result and assertions inside the plugin will not be re-written. To fix this you diff --git a/testing/python/collect.py b/testing/python/collect.py index 5511b3d75..e4069983a 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -166,6 +166,16 @@ class TestClass(object): "because it has a __new__ constructor*" ) + def test_issue2234_property(self, testdir): + testdir.makepyfile(""" + class TestCase(object): + @property + def prop(self): + raise NotImplementedError() + """) + result = testdir.runpytest() + assert result.ret == EXIT_NOTESTSCOLLECTED + class TestGenerator(object): def test_generative_functions(self, testdir): diff --git a/testing/test_assertion.py b/testing/test_assertion.py index fc2819f69..e5c18b43a 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -365,7 +365,7 @@ class TestAssert_reprcompare(object): expl = '\n'.join(callequal(left, right, verbose=True)) assert expl.endswith(textwrap.dedent(expected).strip()) - def test_list_different_lenghts(self): + def test_list_different_lengths(self): expl = callequal([0, 1], [0, 1, 2]) assert len(expl) > 1 expl = callequal([0, 1, 2], [0, 1]) @@ -996,6 +996,25 @@ def test_assert_with_unicode(monkeypatch, testdir): result = testdir.runpytest() result.stdout.fnmatch_lines(['*AssertionError*']) +def test_raise_unprintable_assertion_error(testdir): + testdir.makepyfile(r""" + def test_raise_assertion_error(): + raise AssertionError('\xff') + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([r"> raise AssertionError('\xff')", 'E AssertionError: *']) + +def test_raise_assertion_error_raisin_repr(testdir): + testdir.makepyfile(u""" + class RaisingRepr(object): + def __repr__(self): + raise Exception() + def test_raising_repr(): + raise AssertionError(RaisingRepr()) + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(['E AssertionError: ']) + def test_issue_1944(testdir): testdir.makepyfile(""" def f(): diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 0ec528e82..047a2ac6e 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -271,7 +271,7 @@ class TestAssertionRewrite(object): getmsg(f, must_pass=True) - def test_short_circut_evaluation(self): + def test_short_circuit_evaluation(self): def f(): assert True or explode # noqa diff --git a/testing/test_capture.py b/testing/test_capture.py index 364281fe6..28326fa73 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -604,7 +604,7 @@ def test_capture_binary_output(testdir): def test_error_during_readouterr(testdir): - """Make sure we suspend capturing if errors occurr during readouterr""" + """Make sure we suspend capturing if errors occur during readouterr""" testdir.makepyfile(pytest_xyz=""" from _pytest.capture import FDCapture def bad_snap(self): diff --git a/testing/test_config.py b/testing/test_config.py index 7876063bd..171e9486e 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -519,7 +519,7 @@ def test_consider_args_after_options_for_rootdir_and_inifile(testdir, args): args[i] = d2 with root.as_cwd(): result = testdir.runpytest(*args) - result.stdout.fnmatch_lines(['*rootdir: *myroot, inifile: ']) + result.stdout.fnmatch_lines(['*rootdir: *myroot, inifile:']) @pytest.mark.skipif("sys.platform == 'win32'") @@ -778,6 +778,21 @@ class TestOverrideIniArgs(object): result = testdir.runpytest("--override-ini", 'xdist_strict True', "-s") result.stderr.fnmatch_lines(["*ERROR* *expects option=value*"]) + @pytest.mark.parametrize('with_ini', [True, False]) + def test_override_ini_handled_asap(self, testdir, with_ini): + """-o should be handled as soon as possible and always override what's in ini files (#2238)""" + if with_ini: + testdir.makeini(""" + [pytest] + python_files=test_*.py + """) + testdir.makepyfile(unittest_ini_handle=""" + def test(): + pass + """) + result = testdir.runpytest("--override-ini", 'python_files=unittest_*.py') + result.stdout.fnmatch_lines(["*1 passed in*"]) + def test_with_arg_outside_cwd_without_inifile(self, tmpdir, monkeypatch): monkeypatch.chdir(str(tmpdir)) a = tmpdir.mkdir("a") diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index b4e4c5b14..a0f51fc71 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -580,6 +580,25 @@ class TestPython(object): systemout = pnode.find_first_by_tag("system-err") assert "hello-stderr" in systemout.toxml() + def test_avoid_double_stdout(self, testdir): + testdir.makepyfile(""" + import sys + import pytest + + @pytest.fixture + def arg(request): + yield + sys.stdout.write('hello-stdout teardown') + raise ValueError() + def test_function(arg): + sys.stdout.write('hello-stdout call') + """) + result, dom = runandparse(testdir) + node = dom.find_first_by_tag("testsuite") + pnode = node.find_first_by_tag("testcase") + systemout = pnode.find_first_by_tag("system-out") + assert "hello-stdout call" in systemout.toxml() + assert "hello-stdout teardown" in systemout.toxml() def test_mangle_test_address(): from _pytest.junitxml import mangle_test_address diff --git a/testing/test_pdb.py b/testing/test_pdb.py index cf6d6f7fb..161b4f5f7 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -126,6 +126,21 @@ class TestPDB(object): assert 'debug.me' in rest self.flush(child) + def test_pdb_unittest_skip(self, testdir): + """Test for issue #2137""" + p1 = testdir.makepyfile(""" + import unittest + @unittest.skipIf(True, 'Skipping also with pdb active') + class MyTestCase(unittest.TestCase): + def test_one(self): + assert 0 + """) + child = testdir.spawn_pytest("-rs --pdb %s" % p1) + child.expect('Skipping also with pdb active') + child.expect('1 skipped in') + child.sendeof() + self.flush(child) + def test_pdb_interaction_capture(self, testdir): p1 = testdir.makepyfile(""" def test_1(): diff --git a/testing/test_session.py b/testing/test_session.py index b6e7d2b6c..66a0f5978 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -197,7 +197,7 @@ class TestNewSession(SessionTests): colfail = [x for x in finished if x.failed] assert len(colfail) == 1 - def test_minus_x_overriden_by_maxfail(self, testdir): + def test_minus_x_overridden_by_maxfail(self, testdir): testdir.makepyfile(__init__="") testdir.makepyfile(test_one="xxxx", test_two="yyyy", test_third="zzz") reprec = testdir.inline_run("-x", "--maxfail=2", testdir.tmpdir) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 6a8d147ad..f621a010f 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -969,3 +969,26 @@ def test_module_level_skip_error(testdir): result.stdout.fnmatch_lines( "*Using pytest.skip outside of a test is not allowed*" ) + + +def test_mark_xfail_item(testdir): + # Ensure pytest.mark.xfail works with non-Python Item + testdir.makeconftest(""" + import pytest + + class MyItem(pytest.Item): + nodeid = 'foo' + def setup(self): + marker = pytest.mark.xfail(True, reason="Expected failure") + self.add_marker(marker) + def runtest(self): + assert False + + def pytest_collect_file(path, parent): + return MyItem("foo", parent) + """) + result = testdir.inline_run() + passed, skipped, failed = result.listoutcomes() + assert not failed + xfailed = [r for r in skipped if hasattr(r, 'wasxfail')] + assert xfailed diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 77fb3b154..d72f6a12b 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -906,3 +906,12 @@ def test_summary_stats(exp_line, exp_color, stats_arg): print("Actually got: \"%s\"; with color \"%s\"" % (line, color)) assert line == exp_line assert color == exp_color + + +def test_no_trailing_whitespace_after_inifile_word(testdir): + result = testdir.runpytest('') + assert 'inifile:\n' in result.stdout.str() + + testdir.makeini('[pytest]') + result = testdir.runpytest('') + assert 'inifile: tox.ini\n' in result.stdout.str() diff --git a/tox.ini b/tox.ini index e57fabd4b..1b9fb9f5a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] minversion=2.0 distshare={homedir}/.tox/distshare -# make sure to update enviroment list on appveyor.yml +# make sure to update environment list on appveyor.yml envlist= linting py26