diff --git a/AUTHORS b/AUTHORS index 1bcea995b..c6fb08c2f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -5,6 +5,7 @@ Contributors include:: Abdeali JK Abhijeet Kasurde +Alexei Kozlenok Anatoly Bubenkoff Andreas Zeidler Andy Freeland @@ -12,12 +13,14 @@ Anthon van der Neut Armin Rigo Aron Curzon Aviv Palivoda +Ben Webb Benjamin Peterson Bob Ippolito Brian Dorsey Brian Okken Brianna Laugher Bruno Oliveira +Cal Leeming Carl Friedrich Bolz Charles Cloud Chris Lamb @@ -28,20 +31,24 @@ Daniel Grana Daniel Hahler Daniel Nuri Dave Hunt +David Díaz-Barquero David Mohr David Vierra Edison Gustavo Muenz Eduardo Schettino -Endre Galaczi Elizaveta Shashkova +Endre Galaczi +Eric Hunsberger Eric Hunsberger Eric Siegerman Erik M. Bray +Feng Ma Florian Bruhin Floris Bruynooghe Gabriel Reis Georgy Dyuldin Graham Horler +Greg Price Grig Gheorghiu Guido Wesdorp Harald Armin Massa @@ -65,6 +72,7 @@ Mark Abramowitz Markus Unterwaditzer Martijn Faassen Martin Prusse +Martin K. Scherer Matt Bachmann Matt Williams Michael Aquilina @@ -84,18 +92,16 @@ Raphael Pierzina Roman Bolshakov Ronny Pfannschmidt Ross Lawley +Russel Winder Ryan Wooden Samuele Pedroni Stephan Obermann Tareq Alayan +Simon Gomizelj +Stefano Taschini +Stefan Farmbauer +Thomas Grainger Tom Viner Trevor Bekolay Wouter van Ackooy -David Díaz-Barquero -Eric Hunsberger -Simon Gomizelj -Russel Winder -Ben Webb -Alexei Kozlenok -Cal Leeming -Feng Ma +Bernard Pratz diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b5b6ec81d..41166dce3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -82,15 +82,40 @@ was only available for test modules. Thanks `@flub`_, `@sober7`_ and `@nicoddemus`_ for the PR (`#1619`_). -* +* Text documents without any doctests no longer appear as "skipped". + Thanks `@graingert`_ for reporting and providing a full PR (`#1580`_). -* +* Fix internal error issue when ``method`` argument is missing for + ``teardown_method()``. Fixes (`#1605`_). + +* Fix exception visualization in case the current working directory (CWD) gets + deleted during testing. Fixes (`#1235`). Thanks `@bukzor`_ for reporting. PR by + `@marscher`. Thanks `@nicoddemus`_ for his help. + +* Ensure that a module within a namespace package can be found when it + is specified on the command line together with the ``--pyargs`` + option. Thanks to `@taschini`_ for the PR (`#1597`_). + +* Raise helpful failure message, when requesting parametrized fixture at runtime, + e.g. with ``request.getfuncargvalue``. BACKWARD INCOMPAT: Previously these params + were simply never defined. So a fixture decorated like ``@pytest.fixture(params=[0, 1, 2])`` + only ran once. Now a failure is raised. Fixes (`#460`_). Thanks to + `@nikratio`_ for bug report, `@RedBeardCode`_ and `@tomviner`_ for PR. + +.. _#1580: https://github.com/pytest-dev/pytest/pull/1580 +.. _#1605: https://github.com/pytest-dev/pytest/issues/1605 +.. _#1597: https://github.com/pytest-dev/pytest/pull/1597 +.. _#460: https://github.com/pytest-dev/pytest/pull/460 + +.. _@graingert: https://github.com/graingert +.. _@taschini: https://github.com/taschini +.. _@nikratio: https://github.com/nikratio +.. _@RedBeardCode: https://github.com/RedBeardCode * Fix `#1421`_: Exit tests if a collection error occurs and add ``--continue-on-collection-errors`` option to restore previous behaviour. Thanks `@olegpidsadnyi`_ and `@omarkohl`_ for the complete PR (`#1628`_). -* * @@ -294,7 +319,7 @@ Thanks `@biern`_ for the PR. * Fix `traceback style docs`_ to describe all of the available options - (auto/long/short/line/native/no), with `auto` being the default since v2.6. + (auto/long/short/line/native/no), with ``auto`` being the default since v2.6. Thanks `@hackebrot`_ for the PR. * Fix (`#1422`_): junit record_xml_property doesn't allow multiple records diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5a00c191b..bedc82c65 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -48,7 +48,7 @@ to fix the bug yet. Fix bugs -------- -Look through the GitHub issues for bugs. Here is sample filter you can use: +Look through the GitHub issues for bugs. Here is a filter you can use: https://github.com/pytest-dev/pytest/labels/bug :ref:`Talk ` to developers to find out how you can fix specific bugs. @@ -60,8 +60,7 @@ Don't forget to check the issue trackers of your favourite plugins, too! Implement features ------------------ -Look through the GitHub issues for enhancements. Here is sample filter you -can use: +Look through the GitHub issues for enhancements. Here is a filter you can use: https://github.com/pytest-dev/pytest/labels/enhancement :ref:`Talk ` to developers to find out how you can implement specific @@ -70,16 +69,15 @@ features. Write documentation ------------------- -pytest could always use more documentation. What exactly is needed? +Pytest could always use more documentation. What exactly is needed? * More complementary documentation. Have you perhaps found something unclear? * Documentation translations. We currently have only English. * Docstrings. There can never be too many of them. * Blog posts, articles and such -- they're all very appreciated. -You can also edit documentation files directly in the Github web interface -without needing to make a fork and local copy. This can be convenient for -small fixes. +You can also edit documentation files directly in the GitHub web interface, +without using a local copy. This can be convenient for small fixes. .. _submitplugin: @@ -95,13 +93,14 @@ in repositories living under the ``pytest-dev`` organisations: - `pytest-dev on Bitbucket `_ All pytest-dev Contributors team members have write access to all contained -repositories. pytest core and plugins are generally developed +repositories. Pytest core and plugins are generally developed using `pull requests`_ to respective repositories. The objectives of the ``pytest-dev`` organisation are: * Having a central location for popular pytest plugins -* Sharing some of the maintenance responsibility (in case a maintainer no longer whishes to maintain a plugin) +* Sharing some of the maintenance responsibility (in case a maintainer no + longer wishes to maintain a plugin) You can submit your plugin by subscribing to the `pytest-dev mail list `_ and writing a @@ -127,27 +126,18 @@ transferred to the ``pytest-dev`` organisation. Here's a rundown of how a repository transfer usually proceeds (using a repository named ``joedoe/pytest-xyz`` as example): -* One of the ``pytest-dev`` administrators creates: - - - ``pytest-xyz-admin`` team, with full administration rights to - ``pytest-dev/pytest-xyz``. - - ``pytest-xyz-developers`` team, with write access to - ``pytest-dev/pytest-xyz``. - -* ``joedoe`` is invited to the ``pytest-xyz-admin`` team; - -* After accepting the invitation, ``joedoe`` transfers the repository from its - original location to ``pytest-dev/pytest-xyz`` (A nice feature is that GitHub handles URL redirection from - the old to the new location automatically). - -* ``joedoe`` is free to add any other collaborators to the - ``pytest-xyz-admin`` or ``pytest-xyz-developers`` team as desired. +* ``joedoe`` transfers repository ownership to ``pytest-dev`` administrator ``calvin``. +* ``calvin`` creates ``pytest-xyz-admin`` and ``pytest-xyz-developers`` teams, inviting ``joedoe`` to both as **maintainer**. +* ``calvin`` transfers repository to ``pytest-dev`` and configures team access: + + - ``pytest-xyz-admin`` **admin** access; + - ``pytest-xyz-developers`` **write** access; The ``pytest-dev/Contributors`` team has write access to all projects, and every project administrator is in it. We recommend that each plugin has at least three people who have the right to release to PyPI. -Repository owners can be assured that no ``pytest-dev`` administrator will ever make +Repository owners can rest assured that no ``pytest-dev`` administrator will ever make releases of your repository or take ownership in any way, except in rare cases where someone becomes unresponsive after months of contact attempts. As stated, the objective is to share maintenance and avoid "plugin-abandon". @@ -159,15 +149,11 @@ As stated, the objective is to share maintenance and avoid "plugin-abandon". Preparing Pull Requests on GitHub --------------------------------- -There's an excellent tutorial on how Pull Requests work in the -`GitHub Help Center `_ - - .. note:: What is a "pull request"? It informs project's core developers about the changes you want to review and merge. Pull requests are stored on `GitHub servers `_. - Once you send pull request, we can discuss it's potential modifications and + Once you send a pull request, we can discuss its potential modifications and even add more commits to it later on. There's an excellent tutorial on how Pull Requests work in the @@ -216,19 +202,19 @@ but here is a simple overview: This command will run tests via the "tox" tool against Python 2.7 and 3.5 and also perform "lint" coding-style checks. ``runtox.py`` is a thin wrapper around ``tox`` which installs from a development package - index where newer (not yet released to pypi) versions of dependencies + index where newer (not yet released to PyPI) versions of dependencies (especially ``py``) might be present. #. You can now edit your local working copy. You can now make the changes you want and run the tests again as necessary. - To run tests on py27 and pass options to pytest (e.g. enter pdb on failure) - to pytest you can do:: + To run tests on Python 2.7 and pass options to pytest (e.g. enter pdb on + failure) to pytest you can do:: $ python3 runtox.py -e py27 -- --pdb - or to only run tests in a particular test module on py35:: + Or to only run tests in a particular test module on Python 3.5:: $ python3 runtox.py -e py35 -- testing/test_config.py @@ -237,9 +223,9 @@ but here is a simple overview: $ git commit -a -m "" $ git push -u - Make sure you add a CHANGELOG message, and add yourself to AUTHORS. If you - are unsure about either of these steps, submit your pull request and we'll - help you fix it up. + Make sure you add a message to ``CHANGELOG.rst`` and add yourself to + ``AUTHORS``. If you are unsure about either of these steps, submit your + pull request and we'll help you fix it up. #. Finally, submit a pull request through the GitHub website using this data:: @@ -248,6 +234,6 @@ but here is a simple overview: base-fork: pytest-dev/pytest base: master # if it's a bugfix - base: feature # if it's a feature + base: features # if it's a feature diff --git a/_pytest/__init__.py b/_pytest/__init__.py index fcbd2cde7..79942e374 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,3 +1,2 @@ # - __version__ = '2.10.0.dev1' diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py index 78bd7368e..0f1ffb918 100644 --- a/_pytest/_code/code.py +++ b/_pytest/_code/code.py @@ -3,7 +3,6 @@ from inspect import CO_VARARGS, CO_VARKEYWORDS import re import py - builtin_repr = repr reprlib = py.builtin._tryimport('repr', 'reprlib') @@ -36,12 +35,16 @@ class Code(object): def path(self): """ return a path object pointing to source code (note that it might not point to an actually existing file). """ - p = py.path.local(self.raw.co_filename) - # maybe don't try this checking - if not p.check(): + try: + p = py.path.local(self.raw.co_filename) + # maybe don't try this checking + if not p.check(): + raise OSError("py.path check failed.") + except OSError: # XXX maybe try harder like the weird logic # in the standard lib [linecache.updatecache] does? p = self.raw.co_filename + return p @property diff --git a/_pytest/doctest.py b/_pytest/doctest.py index 4050d1ba7..4411158ab 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -146,23 +146,19 @@ def get_optionflags(parent): return flag_acc -class DoctestTextfile(DoctestItem, pytest.Module): +class DoctestTextfile(pytest.Module): + obj = None - def runtest(self): + def collect(self): import doctest - fixture_request = _setup_fixtures(self) # inspired by doctest.testfile; ideally we would use it directly, # but it doesn't support passing a custom checker text = self.fspath.read() filename = str(self.fspath) name = self.fspath.basename - globs = dict(getfixture=fixture_request.getfuncargvalue) - if '__name__' not in globs: - globs['__name__'] = '__main__' + globs = {'__name__': '__main__'} - for name, value in fixture_request.getfuncargvalue('doctest_namespace').items(): - globs[name] = value optionflags = get_optionflags(self) runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, @@ -170,8 +166,8 @@ class DoctestTextfile(DoctestItem, pytest.Module): parser = doctest.DocTestParser() test = parser.get_doctest(text, globs, name, filename, 0) - _check_all_skipped(test) - runner.run(test) + if test.examples: + yield DoctestItem(test.name, self, runner, test) def _check_all_skipped(test): diff --git a/_pytest/main.py b/_pytest/main.py index ed57c4f27..845d5dd00 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -1,7 +1,5 @@ """ core implementation of testing process: init, session, runtest loop. """ -import imp import os -import re import sys import _pytest @@ -25,8 +23,6 @@ EXIT_INTERNALERROR = 3 EXIT_USAGEERROR = 4 EXIT_NOTESTSCOLLECTED = 5 -name_re = re.compile("^[a-zA-Z_]\w*$") - def pytest_addoption(parser): parser.addini("norecursedirs", "directory patterns to avoid for recursion", type="args", default=['.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg']) @@ -144,17 +140,8 @@ def pytest_runtestloop(session): if session.config.option.collectonly: return True - def getnextitem(i): - # this is a function to avoid python2 - # keeping sys.exc_info set when calling into a test - # python2 keeps sys.exc_info till the frame is left - try: - return session.items[i+1] - except IndexError: - return None - for i, item in enumerate(session.items): - nextitem = getnextitem(i) + nextitem = session.items[i+1] if i+1 < len(session.items) else None item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) if session.shouldstop: raise session.Interrupted(session.shouldstop) @@ -400,7 +387,10 @@ class Node(object): if self.config.option.fulltrace: style="long" else: + tb = _pytest._code.Traceback([excinfo.traceback[-1]]) self._prunetraceback(excinfo) + if len(excinfo.traceback) == 0: + excinfo.traceback = tb tbfilter = False # prunetraceback already does it if style == "auto": style = "long" @@ -411,7 +401,13 @@ class Node(object): else: style = "long" - return excinfo.getrepr(funcargs=True, + try: + os.getcwd() + abspath = False + except OSError: + abspath = True + + return excinfo.getrepr(funcargs=True, abspath=abspath, showlocals=self.config.option.showlocals, style=style, tbfilter=tbfilter) @@ -657,36 +653,32 @@ class Session(FSCollector): return True def _tryconvertpyarg(self, x): - mod = None - path = [os.path.abspath('.')] + sys.path - for name in x.split('.'): - # ignore anything that's not a proper name here - # else something like --pyargs will mess up '.' - # since imp.find_module will actually sometimes work for it - # but it's supposed to be considered a filesystem path - # not a package - if name_re.match(name) is None: - return x - try: - fd, mod, type_ = imp.find_module(name, path) - except ImportError: - return x - else: - if fd is not None: - fd.close() + """Convert a dotted module name to path. - if type_[2] != imp.PKG_DIRECTORY: - path = [os.path.dirname(mod)] - else: - path = [mod] - return mod + """ + import pkgutil + try: + loader = pkgutil.find_loader(x) + except ImportError: + return x + if loader is None: + return x + # This method is sometimes invoked when AssertionRewritingHook, which + # does not define a get_filename method, is already in place: + try: + path = loader.get_filename() + except AttributeError: + # Retrieve path from AssertionRewritingHook: + path = loader.modules[x][0].co_filename + if loader.is_package(x): + path = os.path.dirname(path) + return path def _parsearg(self, arg): """ return (fspath, names) tuple after checking the file exists. """ - arg = str(arg) - if self.config.option.pyargs: - arg = self._tryconvertpyarg(arg) parts = str(arg).split("::") + if self.config.option.pyargs: + parts[0] = self._tryconvertpyarg(parts[0]) relpath = parts[0].replace("/", os.sep) path = self.config.invocation_dir.join(relpath, abs=True) if not path.check(): diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 83b87e741..fa63219d8 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -123,15 +123,18 @@ def getexecutable(name, cache={}): except KeyError: executable = py.path.local.sysfind(name) if executable: + import subprocess + popen = subprocess.Popen([str(executable), "--version"], + universal_newlines=True, stderr=subprocess.PIPE) + out, err = popen.communicate() if name == "jython": - import subprocess - popen = subprocess.Popen([str(executable), "--version"], - universal_newlines=True, stderr=subprocess.PIPE) - out, err = popen.communicate() if not err or "2.5" not in err: executable = None if "2.5.2" in err: executable = None # http://bugs.jython.org/issue1790 + elif popen.returncode != 0: + # Handle pyenv's 127. + executable = None cache[name] = executable return executable diff --git a/_pytest/python.py b/_pytest/python.py index a285666db..1f65eee59 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -2031,6 +2031,25 @@ class FixtureRequest(FuncargnamesCompatAttr): except (AttributeError, ValueError): param = NOTSET param_index = 0 + if fixturedef.params is not None: + frame = inspect.stack()[3] + frameinfo = inspect.getframeinfo(frame[0]) + source_path = frameinfo.filename + source_lineno = frameinfo.lineno + source_path = py.path.local(source_path) + if source_path.relto(funcitem.config.rootdir): + source_path = source_path.relto(funcitem.config.rootdir) + msg = ( + "The requested fixture has no parameter defined for the " + "current test.\n\nRequested fixture '{0}' defined in:\n{1}" + "\n\nRequested here:\n{2}:{3}".format( + fixturedef.argname, + getlocation(fixturedef.func, funcitem.config.rootdir), + source_path, + source_lineno, + ) + ) + pytest.fail(msg) else: # indices might not be set if old-style metafunc.addcall() was used param_index = funcitem.callspec.indices.get(argname, 0) @@ -2369,7 +2388,7 @@ class FixtureManager: else: if marker.name: name = marker.name - assert not name.startswith(self._argprefix) + assert not name.startswith(self._argprefix), name fixturedef = FixtureDef(self, nodeid, name, obj, marker.scope, marker.params, unittest=unittest, ids=marker.ids) diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 85bb3415b..1afee3813 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -7,7 +7,7 @@ Release announcements sprint2016 - release-2.9.1 + release-2.9.2 release-2.9.1 release-2.9.0 release-2.8.7 diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 7bad0315a..f03ae092c 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -295,13 +295,13 @@ which will add the string to the test header accordingly:: You can also return a list of strings which will be considered as several lines of information. You can of course also make the amount of reporting -information on e.g. the value of ``config.option.verbose`` so that +information on e.g. the value of ``config.getoption('verbose')`` so that you present more information appropriately:: # content of conftest.py def pytest_report_header(config): - if config.option.verbose > 0: + if config.getoption('verbose') > 0: return ["info1: did you know that ...", "did you?"] which will add info only when run with "--v":: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 4813ebdac..c5401cda6 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +import os import sys import _pytest._code @@ -513,12 +515,11 @@ class TestInvocationVariants: path = testdir.mkpydir("tpkg") path.join("test_hello.py").write('raise ImportError') - result = testdir.runpytest("--pyargs", "tpkg.test_hello") + result = testdir.runpytest_subprocess("--pyargs", "tpkg.test_hello") assert result.ret != 0 - # FIXME: It would be more natural to match NOT - # "ERROR*file*or*package*not*found*". + result.stdout.fnmatch_lines([ - "*collected 0 items*" + "collected*0*items*/*1*errors" ]) def test_cmdline_python_package(self, testdir, monkeypatch): @@ -540,7 +541,7 @@ class TestInvocationVariants: def join_pythonpath(what): cur = py.std.os.environ.get('PYTHONPATH') if cur: - return str(what) + ':' + cur + return str(what) + os.pathsep + cur return what empty_package = testdir.mkpydir("empty_package") monkeypatch.setenv('PYTHONPATH', join_pythonpath(empty_package)) @@ -551,11 +552,72 @@ class TestInvocationVariants: ]) monkeypatch.setenv('PYTHONPATH', join_pythonpath(testdir)) - path.join('test_hello.py').remove() - result = testdir.runpytest("--pyargs", "tpkg.test_hello") + result = testdir.runpytest("--pyargs", "tpkg.test_missing") assert result.ret != 0 result.stderr.fnmatch_lines([ - "*not*found*test_hello*", + "*not*found*test_missing*", + ]) + + def test_cmdline_python_namespace_package(self, testdir, monkeypatch): + """ + test --pyargs option with namespace packages (#1567) + """ + monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', raising=False) + + search_path = [] + for dirname in "hello", "world": + d = testdir.mkdir(dirname) + search_path.append(d) + ns = d.mkdir("ns_pkg") + ns.join("__init__.py").write( + "__import__('pkg_resources').declare_namespace(__name__)") + lib = ns.mkdir(dirname) + lib.ensure("__init__.py") + lib.join("test_{0}.py".format(dirname)). \ + write("def test_{0}(): pass\n" + "def test_other():pass".format(dirname)) + + # The structure of the test directory is now: + # . + # ├── hello + # │   └── ns_pkg + # │   ├── __init__.py + # │   └── hello + # │   ├── __init__.py + # │   └── test_hello.py + # └── world + # └── ns_pkg + # ├── __init__.py + # └── world + # ├── __init__.py + # └── test_world.py + + def join_pythonpath(*dirs): + cur = py.std.os.environ.get('PYTHONPATH') + if cur: + dirs += (cur,) + return os.pathsep.join(str(p) for p in dirs) + monkeypatch.setenv('PYTHONPATH', join_pythonpath(*search_path)) + for p in search_path: + monkeypatch.syspath_prepend(p) + + # mixed module and filenames: + result = testdir.runpytest("--pyargs", "-v", "ns_pkg.hello", "world/ns_pkg") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*test_hello.py::test_hello*PASSED", + "*test_hello.py::test_other*PASSED", + "*test_world.py::test_world*PASSED", + "*test_world.py::test_other*PASSED", + "*4 passed*" + ]) + + # specify tests within a module + result = testdir.runpytest("--pyargs", "-v", "ns_pkg.world.test_world::test_other") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*test_world.py::test_other*PASSED", + "*1 passed*" ]) def test_cmdline_python_package_not_exists(self, testdir): @@ -700,4 +762,3 @@ class TestDurationWithFixture: * setup *test_1* * call *test_1* """) - diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index d519dc01e..59756645a 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1066,3 +1066,15 @@ def test_repr_traceback_with_unicode(style, encoding): formatter = FormattedExcinfo(style=style) repr_traceback = formatter.repr_traceback(e_info) assert repr_traceback is not None + + +def test_cwd_deleted(testdir): + testdir.makepyfile(""" + def test(tmpdir): + tmpdir.chdir() + tmpdir.remove() + assert False + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(['* 1 failed in *']) + assert 'INTERNALERROR' not in result.stdout.str() + result.stderr.str() \ No newline at end of file diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 8b8497db0..737f3e5cb 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -2704,3 +2704,108 @@ class TestContextManagerFixtureFuncs: """.format(flavor=flavor)) result = testdir.runpytest("-s") result.stdout.fnmatch_lines("*mew*") +class TestParameterizedSubRequest: + def test_call_from_fixture(self, testdir): + testfile = testdir.makepyfile(""" + import pytest + + @pytest.fixture(params=[0, 1, 2]) + def fix_with_param(request): + return request.param + + @pytest.fixture + def get_named_fixture(request): + return request.getfuncargvalue('fix_with_param') + + def test_foo(request, get_named_fixture): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(""" + E*Failed: The requested fixture has no parameter defined for the current test. + E* + E*Requested fixture 'fix_with_param' defined in: + E*{0}:4 + E*Requested here: + E*{1}:9 + *1 error* + """.format(testfile.basename, testfile.basename)) + + def test_call_from_test(self, testdir): + testfile = testdir.makepyfile(""" + import pytest + + @pytest.fixture(params=[0, 1, 2]) + def fix_with_param(request): + return request.param + + def test_foo(request): + request.getfuncargvalue('fix_with_param') + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(""" + E*Failed: The requested fixture has no parameter defined for the current test. + E* + E*Requested fixture 'fix_with_param' defined in: + E*{0}:4 + E*Requested here: + E*{1}:8 + *1 failed* + """.format(testfile.basename, testfile.basename)) + + def test_external_fixture(self, testdir): + conffile = testdir.makeconftest(""" + import pytest + + @pytest.fixture(params=[0, 1, 2]) + def fix_with_param(request): + return request.param + """) + + testfile = testdir.makepyfile(""" + def test_foo(request): + request.getfuncargvalue('fix_with_param') + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(""" + E*Failed: The requested fixture has no parameter defined for the current test. + E* + E*Requested fixture 'fix_with_param' defined in: + E*{0}:4 + E*Requested here: + E*{1}:2 + *1 failed* + """.format(conffile.basename, testfile.basename)) + + def test_non_relative_path(self, testdir): + tests_dir = testdir.mkdir('tests') + fixdir = testdir.mkdir('fixtures') + fixfile = fixdir.join("fix.py") + fixfile.write(_pytest._code.Source(""" + import pytest + + @pytest.fixture(params=[0, 1, 2]) + def fix_with_param(request): + return request.param + """)) + + testfile = tests_dir.join("test_foos.py") + testfile.write(_pytest._code.Source(""" + from fix import fix_with_param + + def test_foo(request): + request.getfuncargvalue('fix_with_param') + """)) + + tests_dir.chdir() + testdir.syspathinsert(fixdir) + result = testdir.runpytest() + result.stdout.fnmatch_lines(""" + E*Failed: The requested fixture has no parameter defined for the current test. + E* + E*Requested fixture 'fix_with_param' defined in: + E*{0}:5 + E*Requested here: + E*{1}:5 + *1 failed* + """.format(fixfile.strpath, testfile.basename)) diff --git a/testing/test_collection.py b/testing/test_collection.py index f42ec8f25..aaf68f6df 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -152,7 +152,9 @@ class TestCollectPluginHookRelay: wascalled = [] class Plugin: def pytest_collect_file(self, path, parent): - wascalled.append(path) + if not path.basename.startswith("."): + # Ignore hidden files, e.g. .testmondata. + wascalled.append(path) testdir.makefile(".abc", "xyz") pytest.main([testdir.tmpdir], plugins=[Plugin()]) assert len(wascalled) == 1 diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 4bda1bf1f..d0bcb1425 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -14,13 +14,16 @@ class TestDoctests: >>> i-1 4 """) + for x in (testdir.tmpdir, checkfile): #print "checking that %s returns custom items" % (x,) items, reprec = testdir.inline_genitems(x) assert len(items) == 1 - assert isinstance(items[0], DoctestTextfile) + assert isinstance(items[0], DoctestItem) + assert isinstance(items[0].parent, DoctestTextfile) + # Empty file has no items. items, reprec = testdir.inline_genitems(w) - assert len(items) == 1 + assert len(items) == 0 def test_collect_module_empty(self, testdir): path = testdir.makepyfile(whatever="#") @@ -608,6 +611,11 @@ class TestDoctestSkips: reprec = testdir.inline_run("--doctest-modules") reprec.assertoutcome(skipped=1) + def test_vacuous_all_skipped(self, testdir, makedoctest): + makedoctest('') + reprec = testdir.inline_run("--doctest-modules") + reprec.assertoutcome(passed=0, skipped=0) + class TestDoctestAutoUseFixtures: diff --git a/testing/test_runner.py b/testing/test_runner.py index 4421c5d0d..377801132 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -228,6 +228,39 @@ class BaseFunctionalTests: assert reps[5].nodeid.endswith("test_func") assert reps[5].failed + def test_exact_teardown_issue1206(self, testdir): + rec = testdir.inline_runsource(""" + import pytest + + class TestClass: + def teardown_method(self): + pass + + def test_method(self): + assert True + """) + reps = rec.getreports("pytest_runtest_logreport") + print (reps) + assert len(reps) == 3 + # + assert reps[0].nodeid.endswith("test_method") + assert reps[0].passed + assert reps[0].when == 'setup' + # + assert reps[1].nodeid.endswith("test_method") + assert reps[1].passed + assert reps[1].when == 'call' + # + assert reps[2].nodeid.endswith("test_method") + assert reps[2].failed + assert reps[2].when == "teardown" + assert reps[2].longrepr.reprcrash.message in ( + # python3 error + 'TypeError: teardown_method() takes 1 positional argument but 2 were given', + # python2 error + 'TypeError: teardown_method() takes exactly 1 argument (2 given)' + ) + def test_failure_in_setup_function_ignores_custom_repr(self, testdir): testdir.makepyfile(conftest=""" import pytest diff --git a/testing/test_terminal.py b/testing/test_terminal.py index d5cc10aee..909450f55 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -8,16 +8,11 @@ import _pytest._pluggy as pluggy import _pytest._code import py import pytest -from _pytest import runner from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.terminal import TerminalReporter, repr_pythonversion, getreportopt from _pytest.terminal import build_summary_stats_line, _plugin_nameversions -def basic_run_report(item): - runner.call_and_report(item, "setup", log=False) - return runner.call_and_report(item, "call", log=False) - DistInfo = collections.namedtuple('DistInfo', ['project_name', 'version'])