diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..242d3da0d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG merge=union diff --git a/.gitignore b/.gitignore index 2b7c267b0..cd6a7fc9e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,11 @@ include/ *.orig *~ +.eggs/ + +# this file is managed by setuptools_scm +_pytest/__init__.py + doc/*/_build build/ dist/ diff --git a/.travis.yml b/.travis.yml index 89ed8d187..e83220105 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,7 @@ env: - TESTENV=py35 - TESTENV=pypy -script: tox --recreate -i ALL=https://devpi.net/hpk/dev/ -e $TESTENV +script: tox --recreate -e $TESTENV notifications: irc: diff --git a/AUTHORS b/AUTHORS index cf52e03a0..8d700878c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,6 +3,7 @@ merlinux GmbH, Germany, office at merlinux eu Contributors include:: +Abhijeet Kasurde Anatoly Bubenkoff Andreas Zeidler Andy Freeland @@ -14,6 +15,7 @@ Bob Ippolito Brian Dorsey Brian Okken Brianna Laugher +Bruno Oliveira Carl Friedrich Bolz Charles Cloud Chris Lamb @@ -24,11 +26,12 @@ Daniel Grana Daniel Nuri Dave Hunt David Mohr +Edison Gustavo Muenz Eduardo Schettino Elizaveta Shashkova +Eric Hunsberger Eric Siegerman Florian Bruhin -Edison Gustavo Muenz Floris Bruynooghe Graham Horler Grig Gheorghiu @@ -46,6 +49,7 @@ Maciek Fijalkowski Maho Marc Schlaich Mark Abramowitz +Markus Unterwaditzer Martijn Faassen Nicolas Delaby Pieter Mulder @@ -58,3 +62,4 @@ Samuele Pedroni Tom Viner Trevor Bekolay Wouter van Ackooy +David Díaz-Barquero diff --git a/CHANGELOG b/CHANGELOG index ab27f7ee9..2c60e8499 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,40 @@ 2.8.0.dev (compared to 2.7.X) ----------------------------- +- Fix #562: @nose.tools.istest now fully respected. + +- Fix issue736: Fix a bug where fixture params would be discarded when combined + with parametrization markers. + Thanks to Markus Unterwaditzer for the PR. + +- fix issue710: introduce ALLOW_UNICODE doctest option: when enabled, the + ``u`` prefix is stripped from unicode strings in expected doctest output. This + allows doctests which use unicode to run in Python 2 and 3 unchanged. + Thanks Jason R. Coombs for the report and Bruno Oliveira for the PR. + +- parametrize now also generates meaningful test IDs for enum, regex and class + objects (as opposed to class instances). + Thanks to Florian Bruhin for the PR. + +- Add 'warns' to assert that warnings are thrown (like 'raises'). + Thanks to Eric Hunsberger for the PR. + +- Fix issue683: Do not apply an already applied mark. Thanks ojake for the PR. + +- Deal with capturing failures better so fewer exceptions get lost to + /dev/null. Thanks David Szotten for the PR. + +- fix issue730: deprecate and warn about the --genscript option. + Thanks Ronny Pfannschmidt for the report and Christian Pommranz for the PR. + +- fix issue751: multiple parametrize with ids bug if it parametrizes class with + two or more test methods. Thanks Sergey Chipiga for reporting and Jan + Bednarik for PR. + +- fix issue82: avoid loading conftest files from setup.cfg/pytest.ini/tox.ini + files and upwards by default (--confcutdir can still be set to override this). + Thanks Bruno Oliveira for the PR. + - 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. @@ -11,7 +45,7 @@ deprecated. Thanks Bruno Oliveira for the PR. -- fix issue 808: pytest's internal assertion rewrite hook now implements the +- fix issue808: pytest's internal assertion rewrite hook now implements the optional PEP302 get_data API so tests can access data files next to them. Thanks xmo-odoo for request and example and Bruno Oliveira for the PR. @@ -24,6 +58,15 @@ - 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). +- fix issue812: pytest now exits with status code 5 in situations where no + tests were run at all, such as the directory given in the command line does + not contain any tests or as result of a command line option filters + all out all tests (-k for example). + Thanks Eric Siegerman (issue812) and Bruno Oliveira for the PR. + +- 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 (related to issue500). Thanks Eric Siegerman. - New `testpaths` ini option: list of directories to search for tests @@ -103,6 +146,11 @@ - fix issue714: add ability to apply indirect=True parameter on particular argnames. +- fix issue890: changed extension of all documentation files from ``txt`` to + ``rst``. Thanks to Abhijeet for the PR. + +- issue951: add new record_xml_property fixture, that supports logging + additional information on xml output. Thanks David Diaz for the PR. 2.7.3 (compared to 2.7.2) ----------------------------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 587b309a6..4a418e62e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -21,7 +21,7 @@ in repositories living under: - `the pytest-dev bitbucket team `_ -All pytest-dev team members have write access to all contained +All pytest-dev Contributors team members have write access to all contained repositories. pytest core and plugins are generally developed using `pull requests`_ to respective repositories. @@ -46,9 +46,9 @@ the following: If no contributor strongly objects and two agree, the repo will be transferred to the ``pytest-dev`` organisation and you'll become a -member of the ``pytest-dev`` team, with commit rights to all projects. -We recommend that each plugin has at least three people who have the -right to release to pypi. +member of the ``pytest-dev Contributors`` team, with commit rights +to all projects. We recommend that each plugin has at least three +people who have the right to release to pypi. .. _reportbugs: @@ -66,6 +66,10 @@ If you are reporting a bug, please include: installed libraries and pytest version. * Detailed steps to reproduce the bug. +If you can write a demonstration test that currently fails but should pass (xfail), +that is a very useful commit to make as well, even if you can't find how +to fix the bug yet. + .. _submitfeedback: Submit feedback for developers @@ -93,6 +97,8 @@ https://github.com/pytest-dev/pytest/labels/bug :ref:`Talk ` to developers to find out how you can fix specific bugs. +Don't forget to check the issue trackers of your favourite plugins, too! + .. _writeplugins: Implement features @@ -111,10 +117,14 @@ Write documentation pytest could always use more documentation. What exactly is needed? * More complementary documentation. Have you perhaps found something unclear? -* Documentation translations. We currently have English and Japanese versions. +* Documentation translations. We currently have only English. * Docstrings. There's never too much 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. + .. _`pull requests`: .. _pull-requests: @@ -181,9 +191,13 @@ 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. + #. Finally, submit a pull request through the GitHub website: - .. image:: img/pullrequest.png + .. image:: doc/en/img/pullrequest.png :width: 700px :align: center diff --git a/HOWTORELEASE.rst b/HOWTORELEASE.rst index 28f8f8546..b886f0fd6 100644 --- a/HOWTORELEASE.rst +++ b/HOWTORELEASE.rst @@ -1,58 +1,87 @@ - -How to release pytest (draft) +How to release pytest -------------------------------------------- -1. bump version numbers in _pytest/__init__.py (setup.py reads it) +Note: this assumes you have already registered on pypi. -2. check and finalize CHANGELOG +1. Bump version numbers in _pytest/__init__.py (setup.py reads it) -3. write doc/en/announce/release-VERSION.txt and include +2. Check and finalize CHANGELOG + +3. Write doc/en/announce/release-VERSION.txt and include it in doc/en/announce/index.txt -4. use devpi for uploading a release tarball to a staging area: - - ``devpi use https://devpi.net/USER/dev`` - - ``devpi upload --formats sdist,bdist_wheel`` +4. Use devpi for uploading a release tarball to a staging area: -5. run from multiple machines: - - ``devpi use https://devpi.net/USER/dev`` - - ``devpi test pytest==VERSION`` + ``devpi use https://devpi.net/USER/dev`` + ``devpi upload --formats sdist,bdist_wheel`` + +5. Run from multiple machines: + + ``devpi use https://devpi.net/USER/dev`` + ``devpi test pytest==VERSION`` + +6. Check that tests pass for relevant combinations with + + ``devpi list pytest`` -6. check that tests pass for relevant combinations with - ``devpi list pytest`` or look at failures with "devpi list -f pytest". There will be some failed environments like e.g. the py33-trial or py27-pexpect tox environments on Win32 platforms which is ok (tox does not support skipping on per-platform basis yet). -7. Regenerate the docs examples using tox:: - # Create and activate a virtualenv with regendoc installed - # (currently needs revision 4a9ec1035734) - tox -e regen +7. Regenerate the docs examples using tox, and check for regressions:: -8. Build the docs, you need a virtualenv with, py and sphinx + tox -e regen + git diff + + +8. Build the docs, you need a virtualenv with py and sphinx installed:: - cd docs/en + + cd doc/en make html -9. Tag the release:: - hg tag VERSION + Commit any changes before tagging the release. + +9. Tag the release:: + + git tag VERSION + git push + +10. Upload the docs using doc/en/Makefile:: + + cd doc/en + make install # or "installall" if you have LaTeX installed for PDF -10. Upload the docs using docs/en/Makefile:: - cd docs/en - make install # or "installall" if you have LaTeX installed This requires ssh-login permission on pytest.org because it uses rsync. Note that the "install" target of doc/en/Makefile defines where the rsync goes to, typically to the "latest" section of pytest.org. -11. publish to pypi "devpi push pytest-VERSION pypi:NAME" where NAME - is the name of pypi.python.org as configured in your - ~/.pypirc file -- it's the same you would use with - "setup.py upload -r NAME" + If you are making a minor release (e.g. 5.4), you also need to manually + create a symlink for "latest":: -12. send release announcement to mailing lists: + ssh pytest-dev@pytest.org + ln -s 5.4 latest - pytest-dev - testing-in-python - python-announce-list@python.org + Browse to pytest.org to verify. + +11. Publish to pypi:: + + devpi push pytest-VERSION pypi:NAME + + where NAME is the name of pypi.python.org as configured in your + ~/.pypirc file `for devpi `_. + + +12. Send release announcement to mailing lists: + + - pytest-dev + - testing-in-python + - python-announce-list@python.org + + +13. **after the release** Bump the version number in ``_pytest/__init__.py``, + to the next Minor release version (i.e. if you released ``pytest-2.8.0``, + set it to ``pytest-2.9.0.dev1``). diff --git a/Makefile b/Makefile index b92a88977..0b0fd61fe 100644 --- a/Makefile +++ b/Makefile @@ -21,11 +21,11 @@ clean: # generate documentation docs: develop - find doc/en -name '*.txt' -not -path 'doc/en/_build/*' | xargs .env/bin/regendoc ${REGENDOC_ARGS} + find doc/en -name '*.rst' -not -path 'doc/en/_build/*' | xargs .env/bin/regendoc ${REGENDOC_ARGS} cd doc/en; make html # upload documentation upload-docs: develop - find doc/en -name '*.txt' -not -path 'doc/en/_build/*' | xargs .env/bin/regendoc ${REGENDOC_ARGS} --update + find doc/en -name '*.rst' -not -path 'doc/en/_build/*' | xargs .env/bin/regendoc ${REGENDOC_ARGS} --update #cd doc/en; make install diff --git a/_pytest/__init__.py b/_pytest/__init__.py deleted file mode 100644 index 5b3715e54..000000000 --- a/_pytest/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -__version__ = '2.8.0.dev4' diff --git a/_pytest/capture.py b/_pytest/capture.py index 855b99bde..58f9cb525 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -86,8 +86,10 @@ class CaptureManager: self.deactivate_funcargs() cap = getattr(self, "_capturing", None) if cap is not None: - outerr = cap.readouterr() - cap.suspend_capturing(in_=in_) + try: + outerr = cap.readouterr() + finally: + cap.suspend_capturing(in_=in_) return outerr def activate_funcargs(self, pyfuncitem): diff --git a/_pytest/config.py b/_pytest/config.py index 492dbbc43..e2afaa976 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -211,6 +211,10 @@ class PytestPluginManager(PluginManager): # support deprecated naming because plugins (xdist e.g.) use it return self.get_plugin(name) + def hasplugin(self, name): + """Return True if the plugin with the given name is registered.""" + return bool(self.get_plugin(name)) + def pytest_configure(self, config): # XXX now that the pluginmanager exposes hookimpl(tryfirst...) # we should remove tryfirst/trylast as markers @@ -897,6 +901,9 @@ class Config(object): self.warn("I2", "could not load setuptools entry import: %s" % (e,)) self.pluginmanager.consider_env() self.known_args_namespace = ns = self._parser.parse_known_args(args) + if self.known_args_namespace.confcutdir is None and self.inifile: + confcutdir = py.path.local(self.inifile).dirname + self.known_args_namespace.confcutdir = confcutdir try: self.hook.pytest_load_initial_conftests(early_config=self, args=args, parser=self._parser) diff --git a/_pytest/doctest.py b/_pytest/doctest.py index c6c60b9c2..fe71c8284 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -63,7 +63,7 @@ class DoctestItem(pytest.Item): lineno = test.lineno + example.lineno + 1 message = excinfo.type.__name__ reprlocation = ReprFileLocation(filename, lineno, message) - checker = doctest.OutputChecker() + checker = _get_unicode_checker() REPORT_UDIFF = doctest.REPORT_UDIFF filelines = py.path.local(filename).readlines(cr=0) lines = [] @@ -100,7 +100,8 @@ def _get_flag_lookup(): NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE, ELLIPSIS=doctest.ELLIPSIS, IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL, - COMPARISON_FLAGS=doctest.COMPARISON_FLAGS) + COMPARISON_FLAGS=doctest.COMPARISON_FLAGS, + ALLOW_UNICODE=_get_allow_unicode_flag()) def get_optionflags(parent): optionflags_str = parent.config.getini("doctest_optionflags") @@ -110,15 +111,30 @@ def get_optionflags(parent): flag_acc |= flag_lookup_table[flag] return flag_acc + class DoctestTextfile(DoctestItem, pytest.File): + def runtest(self): import doctest fixture_request = _setup_fixtures(self) - failed, tot = doctest.testfile( - str(self.fspath), module_relative=False, - optionflags=get_optionflags(self), - extraglobs=dict(getfixture=fixture_request.getfuncargvalue), - raise_on_error=True, verbose=0) + + # 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__' + + optionflags = get_optionflags(self) + runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, + checker=_get_unicode_checker()) + + parser = doctest.DocTestParser() + test = parser.get_doctest(text, globs, name, filename, 0) + runner.run(test) + class DoctestModule(pytest.File): def collect(self): @@ -139,7 +155,8 @@ class DoctestModule(pytest.File): # uses internal doctest module parsing mechanism finder = doctest.DocTestFinder() optionflags = get_optionflags(self) - runner = doctest.DebugRunner(verbose=0, optionflags=optionflags) + runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, + checker=_get_unicode_checker()) for test in finder.find(module, module.__name__, extraglobs=doctest_globals): if test.examples: # skip empty doctests @@ -160,3 +177,59 @@ def _setup_fixtures(doctest_item): fixture_request = FixtureRequest(doctest_item) fixture_request._fillfixtures() return fixture_request + + +def _get_unicode_checker(): + """ + Returns a doctest.OutputChecker subclass that takes in account the + ALLOW_UNICODE option to ignore u'' prefixes in strings. Useful + when the same doctest should run in Python 2 and Python 3. + + An inner class is used to avoid importing "doctest" at the module + level. + """ + if hasattr(_get_unicode_checker, 'UnicodeOutputChecker'): + return _get_unicode_checker.UnicodeOutputChecker() + + import doctest + import re + + class UnicodeOutputChecker(doctest.OutputChecker): + """ + Copied from doctest_nose_plugin.py from the nltk project: + https://github.com/nltk/nltk + """ + + _literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) + + def check_output(self, want, got, optionflags): + res = doctest.OutputChecker.check_output(self, want, got, + optionflags) + if res: + return True + + if not (optionflags & _get_allow_unicode_flag()): + return False + + else: # pragma: no cover + # the code below will end up executed only in Python 2 in + # our tests, and our coverage check runs in Python 3 only + def remove_u_prefixes(txt): + return re.sub(self._literal_re, r'\1\2', txt) + + want = remove_u_prefixes(want) + got = remove_u_prefixes(got) + res = doctest.OutputChecker.check_output(self, want, got, + optionflags) + return res + + _get_unicode_checker.UnicodeOutputChecker = UnicodeOutputChecker + return _get_unicode_checker.UnicodeOutputChecker() + + +def _get_allow_unicode_flag(): + """ + Registers and returns the ALLOW_UNICODE flag. + """ + import doctest + return doctest.register_optionflag('ALLOW_UNICODE') diff --git a/_pytest/genscript.py b/_pytest/genscript.py index d6f452370..0572bfcd6 100755 --- a/_pytest/genscript.py +++ b/_pytest/genscript.py @@ -1,4 +1,4 @@ -""" generate a single-file self-contained version of pytest """ +""" (deprecated) generate a single-file self-contained version of pytest """ import os import sys import pkgutil @@ -31,7 +31,12 @@ def pkg_to_mapping(name): else: # package for pyfile in toplevel.visit('*.py'): pkg = pkgname(name, toplevel, pyfile) - name2src[pkg] = pyfile.read() + if pkg == '_pytest.__init__': + # remove the coding comment line to avoid python bug + lines = pyfile.read().splitlines(True) + name2src[pkg] = ''.join(lines[1:]) + else: + name2src[pkg] = pyfile.read() # with wheels py source code might be not be installed # and the resulting genscript is useless, just bail out. assert name2src, "no source code found for %r at %r" %(name, toplevel) @@ -72,6 +77,8 @@ def pytest_cmdline_main(config): genscript = config.getvalue("genscript") if genscript: tw = _pytest.config.create_terminal_writer(config) + tw.line("WARNING: usage of genscript is deprecated.", + red=True) deps = ['py', 'pluggy', '_pytest', 'pytest'] if sys.version_info < (2,7): deps.append("argparse") diff --git a/_pytest/junitxml.py b/_pytest/junitxml.py index c12fa084a..8b75b139a 100644 --- a/_pytest/junitxml.py +++ b/_pytest/junitxml.py @@ -9,6 +9,7 @@ import os import re import sys import time +import pytest # Python 2.X and 3.X compatibility if sys.version_info[0] < 3: @@ -53,6 +54,20 @@ def bin_xml_escape(arg): return unicode('#x%04X') % i return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg))) +@pytest.fixture +def record_xml_property(request): + """Fixture that adds extra xml properties to the tag for the calling test. + The fixture is callable with (name, value), with value being automatically + xml-encoded. + """ + def inner(name, value): + if hasattr(request.config, "_xml"): + request.config._xml.add_custom_property(name, value) + msg = 'record_xml_property is an experimental feature' + request.config.warn(code='C3', message=msg, + fslocation=request.node.location[:2]) + return inner + def pytest_addoption(parser): group = parser.getgroup("terminal reporting") group.addoption('--junitxml', '--junit-xml', action="store", @@ -75,7 +90,6 @@ def pytest_unconfigure(config): del config._xml config.pluginmanager.unregister(xml) - def mangle_testnames(names): names = [x.replace(".py", "") for x in names if x != '()'] names[0] = names[0].replace("/", '.') @@ -89,6 +103,10 @@ class LogXML(object): self.tests = [] self.passed = self.skipped = 0 self.failed = self.errors = 0 + self.custom_properties = {} + + def add_custom_property(self, name, value): + self.custom_properties[str(name)] = bin_xml_escape(str(value)) def _opentestcase(self, report): names = mangle_testnames(report.nodeid.split("::")) @@ -118,6 +136,10 @@ class LogXML(object): def append(self, obj): self.tests[-1].append(obj) + def append_custom_properties(self): + self.tests[-1].attr.__dict__.update(self.custom_properties) + self.custom_properties.clear() + def append_pass(self, report): self.passed += 1 self._write_captured_output(report) @@ -179,6 +201,7 @@ class LogXML(object): if report.when == "setup": self._opentestcase(report) self.tests[-1].attr.time += getattr(report, 'duration', 0) + self.append_custom_properties() if report.passed: if report.when == "call": # ignore setup/teardown self.append_pass(report) diff --git a/_pytest/main.py b/_pytest/main.py index fc9d64cf6..4f3d2625f 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -19,6 +19,7 @@ EXIT_TESTSFAILED = 1 EXIT_INTERRUPTED = 2 EXIT_INTERNALERROR = 3 EXIT_USAGEERROR = 4 +EXIT_NOTESTSCOLLECTED = 5 name_re = re.compile("^[a-zA-Z_]\w*$") @@ -100,8 +101,10 @@ def wrap_session(config, doit): if excinfo.errisinstance(SystemExit): sys.stderr.write("mainloop: caught Spurious SystemExit!\n") else: - if session._testsfailed: + if session.testsfailed: session.exitstatus = EXIT_TESTSFAILED + elif session.testscollected == 0: + session.exitstatus = EXIT_NOTESTSCOLLECTED finally: excinfo = None # Explicitly break reference cycle. session.startdir.chdir() @@ -509,7 +512,8 @@ class Session(FSCollector): FSCollector.__init__(self, config.rootdir, parent=None, config=config, session=self) self._fs2hookproxy = {} - self._testsfailed = 0 + self.testsfailed = 0 + self.testscollected = 0 self.shouldstop = False self.trace = config.trace.root.get("collection") self._norecursepatterns = config.getini("norecursedirs") @@ -527,11 +531,11 @@ class Session(FSCollector): @pytest.hookimpl(tryfirst=True) def pytest_runtest_logreport(self, report): if report.failed and not hasattr(report, 'wasxfail'): - self._testsfailed += 1 + self.testsfailed += 1 maxfail = self.config.getvalue("maxfail") - if maxfail and self._testsfailed >= maxfail: + if maxfail and self.testsfailed >= maxfail: self.shouldstop = "stopping after %d failures" % ( - self._testsfailed) + self.testsfailed) pytest_collectreport = pytest_runtest_logreport def isinitpath(self, path): @@ -564,6 +568,7 @@ class Session(FSCollector): config=self.config, items=items) finally: hook.pytest_collection_finish(session=self) + self.testscollected = len(items) return items def _perform_collect(self, args, genitems): diff --git a/_pytest/mark.py b/_pytest/mark.py index c3dc692f0..50581e0a8 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -291,7 +291,7 @@ class MarkInfo: #: positional argument list, empty if none specified self.args = args #: keyword argument dictionary, empty if nothing specified - self.kwargs = kwargs + self.kwargs = kwargs.copy() self._arglist = [(args, kwargs.copy())] def __repr__(self): diff --git a/_pytest/python.py b/_pytest/python.py index e50c7909d..89b726dd4 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1,4 +1,5 @@ """ Python test discovery, setup and run of test functions. """ +import re import fnmatch import functools import py @@ -8,6 +9,12 @@ import pytest from _pytest.mark import MarkDecorator, MarkerError from py._code.code import TerminalRepr +try: + import enum +except ImportError: # pragma: no cover + # Only available in Python 3.4+ or as a backport + enum = None + import _pytest import pluggy @@ -22,13 +29,15 @@ isclass = inspect.isclass callable = py.builtin.callable # used to work around a python2 exception info leak exc_clear = getattr(sys, 'exc_clear', lambda: None) +# The type of re.compile objects is not exposed in Python. +REGEX_TYPE = type(re.compile('')) def filter_traceback(entry): return entry.path != cutdir1 and not entry.path.relto(cutdir2) def get_real_func(obj): - """gets the real function object of the (possibly) wrapped object by + """ gets the real function object of the (possibly) wrapped object by functools.wraps or functools.partial. """ while hasattr(obj, "__wrapped__"): @@ -55,6 +64,17 @@ def getimfunc(func): except AttributeError: return func +def safe_getattr(object, name, default): + """ Like getattr but return default upon any Exception. + + Attribute access can potentially fail for 'evil' Python objects. + See issue214 + """ + try: + return getattr(object, name, default) + except Exception: + return default + class FixtureFunctionMarker: def __init__(self, scope, params, @@ -160,10 +180,13 @@ def pytest_cmdline_main(config): def pytest_generate_tests(metafunc): - # this misspelling is common - raise a specific error to alert the user - if hasattr(metafunc.function, 'parameterize'): - msg = "{0} has 'parameterize', spelling should be 'parametrize'" - raise MarkerError(msg.format(metafunc.function.__name__)) + # those alternative spellings are common - raise a specific error to alert + # the user + alt_spellings = ['parameterize', 'parametrise', 'parameterise'] + for attr in alt_spellings: + if hasattr(metafunc.function, attr): + msg = "{0} has '{1}', spelling should be 'parametrize'" + raise MarkerError(msg.format(metafunc.function.__name__, attr)) try: markers = metafunc.function.parametrize except AttributeError: @@ -245,11 +268,10 @@ def pytest_pycollect_makeitem(collector, name, obj): raise StopIteration # nothing was collected elsewhere, let's do it here if isclass(obj): - if collector.classnamefilter(name): + if collector.istestclass(obj, name): Class = collector._getcustomclass("Class") outcome.force_result(Class(name, parent=collector)) - elif collector.funcnamefilter(name) and hasattr(obj, "__call__") and\ - getfixturemarker(obj) is None: + elif collector.istestfunction(obj, name): # mock seems to store unbound methods (issue473), normalize it obj = getattr(obj, "__func__", obj) if not isfunction(obj): @@ -335,9 +357,24 @@ class PyCollector(PyobjMixin, pytest.Collector): def funcnamefilter(self, name): return self._matches_prefix_or_glob_option('python_functions', name) + def isnosetest(self, obj): + """ Look for the __test__ attribute, which is applied by the + @nose.tools.istest decorator + """ + return safe_getattr(obj, '__test__', False) + def classnamefilter(self, name): return self._matches_prefix_or_glob_option('python_classes', name) + def istestfunction(self, obj, name): + return ( + (self.funcnamefilter(name) or self.isnosetest(obj)) + and safe_getattr(obj, "__call__", False) and getfixturemarker(obj) is None + ) + + def istestclass(self, obj, name): + return self.classnamefilter(name) or self.isnosetest(obj) + def _matches_prefix_or_glob_option(self, option_name, name): """ checks if the given name matches the prefix or glob-pattern defined @@ -480,6 +517,19 @@ class FuncFixtureInfo: self.names_closure = names_closure self.name2fixturedefs = name2fixturedefs + +def _marked(func, mark): + """ Returns True if :func: is already marked with :mark:, False otherwise. + This can happen if marker is applied to class and the test file is + invoked more than once. + """ + try: + func_mark = getattr(func, mark.name) + except AttributeError: + return False + return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs + + def transfer_markers(funcobj, cls, mod): # XXX this should rather be code in the mark plugin or the mark # plugin should merge with the python plugin. @@ -490,9 +540,11 @@ def transfer_markers(funcobj, cls, mod): continue if isinstance(pytestmark, list): for mark in pytestmark: - mark(funcobj) + if not _marked(funcobj, mark): + mark(funcobj) else: - pytestmark(funcobj) + if not _marked(funcobj, pytestmark): + pytestmark(funcobj) class Module(pytest.File, PyCollector): """ Collector for test classes and functions. """ @@ -973,8 +1025,15 @@ def _idval(val, argname, idx, idfn): return s except Exception: pass + if isinstance(val, (float, int, str, bool, NoneType)): return str(val) + elif isinstance(val, REGEX_TYPE): + return val.pattern + elif enum is not None and isinstance(val, enum.Enum): + return str(val) + elif isclass(val) and hasattr(val, '__name__'): + return val.__name__ return str(argname)+str(idx) def _idvalset(idx, valset, argnames, idfn): @@ -1049,8 +1108,8 @@ def getlocation(function, curdir): # builtin pytest.raises helper -def raises(ExpectedException, *args, **kwargs): - """ assert that a code block/function call raises @ExpectedException +def raises(expected_exception, *args, **kwargs): + """ assert that a code block/function call raises @expected_exception and raise a failure exception otherwise. This helper produces a ``py.code.ExceptionInfo()`` object. @@ -1098,23 +1157,23 @@ def raises(ExpectedException, *args, **kwargs): """ __tracebackhide__ = True - if ExpectedException is AssertionError: + if expected_exception is AssertionError: # we want to catch a AssertionError # replace our subclass with the builtin one # see https://github.com/pytest-dev/pytest/issues/176 from _pytest.assertion.util import BuiltinAssertionError \ - as ExpectedException + as expected_exception msg = ("exceptions must be old-style classes or" " derived from BaseException, not %s") - if isinstance(ExpectedException, tuple): - for exc in ExpectedException: - if not inspect.isclass(exc): + if isinstance(expected_exception, tuple): + for exc in expected_exception: + if not isclass(exc): raise TypeError(msg % type(exc)) - elif not inspect.isclass(ExpectedException): - raise TypeError(msg % type(ExpectedException)) + elif not isclass(expected_exception): + raise TypeError(msg % type(expected_exception)) if not args: - return RaisesContext(ExpectedException) + return RaisesContext(expected_exception) elif isinstance(args[0], str): code, = args assert isinstance(code, str) @@ -1127,19 +1186,19 @@ def raises(ExpectedException, *args, **kwargs): py.builtin.exec_(code, frame.f_globals, loc) # XXX didn'T mean f_globals == f_locals something special? # this is destroyed here ... - except ExpectedException: + except expected_exception: return py.code.ExceptionInfo() else: func = args[0] try: func(*args[1:], **kwargs) - except ExpectedException: + except expected_exception: return py.code.ExceptionInfo() pytest.fail("DID NOT RAISE") class RaisesContext(object): - def __init__(self, ExpectedException): - self.ExpectedException = ExpectedException + def __init__(self, expected_exception): + self.expected_exception = expected_exception self.excinfo = None def __enter__(self): @@ -1158,7 +1217,7 @@ class RaisesContext(object): exc_type, value, traceback = tp tp = exc_type, exc_type(value), traceback self.excinfo.__init__(tp) - return issubclass(self.excinfo.type, self.ExpectedException) + return issubclass(self.excinfo.type, self.expected_exception) # # the basic pytest Function item @@ -1357,7 +1416,7 @@ class FixtureRequest(FuncargnamesCompatAttr): return self._pyfuncitem.session def addfinalizer(self, finalizer): - """add finalizer/teardown function to be called after the + """ add finalizer/teardown function to be called after the last test within the requesting test context finished execution. """ # XXX usually this method is shadowed by fixturedef specific ones @@ -1771,7 +1830,7 @@ class FixtureManager: if fixturedef.params is not None: func_params = getattr(getattr(metafunc.function, 'parametrize', None), 'args', [[None]]) # skip directly parametrized arguments - if argname not in func_params and argname not in func_params[0]: + if argname not in func_params: metafunc.parametrize(argname, fixturedef.params, indirect=True, scope=fixturedef.scope, ids=fixturedef.ids) @@ -2120,4 +2179,3 @@ def get_scope_node(node, scope): return node.session raise ValueError("unknown scope") return node.getparent(cls) - diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 875cb510e..abefdfac1 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -1,10 +1,14 @@ """ recording warnings during test function execution. """ +import inspect +import py import sys import warnings +import pytest -def pytest_funcarg__recwarn(request): +@pytest.yield_fixture +def recwarn(request): """Return a WarningsRecorder instance that provides these methods: * ``pop(category=None)``: return last warning matching the category. @@ -13,83 +17,173 @@ def pytest_funcarg__recwarn(request): See http://docs.python.org/library/warnings.html for information on warning categories. """ - if sys.version_info >= (2,7): - oldfilters = warnings.filters[:] - warnings.simplefilter('default') - def reset_filters(): - warnings.filters[:] = oldfilters - request.addfinalizer(reset_filters) wrec = WarningsRecorder() - request.addfinalizer(wrec.finalize) - return wrec + with wrec: + warnings.simplefilter('default') + yield wrec + def pytest_namespace(): - return {'deprecated_call': deprecated_call} + return {'deprecated_call': deprecated_call, + 'warns': warns} + def deprecated_call(func, *args, **kwargs): - """ assert that calling ``func(*args, **kwargs)`` - triggers a DeprecationWarning. + """Assert that ``func(*args, **kwargs)`` triggers a DeprecationWarning. """ - l = [] - oldwarn_explicit = getattr(warnings, 'warn_explicit') - def warn_explicit(*args, **kwargs): - l.append(args) - oldwarn_explicit(*args, **kwargs) - oldwarn = getattr(warnings, 'warn') - def warn(*args, **kwargs): - l.append(args) - oldwarn(*args, **kwargs) - - warnings.warn_explicit = warn_explicit - warnings.warn = warn - try: + wrec = WarningsRecorder() + with wrec: + warnings.simplefilter('always') # ensure all warnings are triggered ret = func(*args, **kwargs) - finally: - warnings.warn_explicit = oldwarn_explicit - warnings.warn = oldwarn - if not l: + + if not any(r.category is DeprecationWarning for r in wrec): __tracebackhide__ = True - raise AssertionError("%r did not produce DeprecationWarning" %(func,)) + raise AssertionError("%r did not produce DeprecationWarning" % (func,)) + return ret -class RecordedWarning: - def __init__(self, message, category, filename, lineno, line): +def warns(expected_warning, *args, **kwargs): + """Assert that code raises a particular class of warning. + + Specifically, the input @expected_warning can be a warning class or + tuple of warning classes, and the code must return that warning + (if a single class) or one of those warnings (if a tuple). + + This helper produces a list of ``warnings.WarningMessage`` objects, + one for each warning raised. + + This function can be used as a context manager, or any of the other ways + ``pytest.raises`` can be used:: + + >>> with warns(RuntimeWarning): + ... warnings.warn("my warning", RuntimeWarning) + """ + wcheck = WarningsChecker(expected_warning) + if not args: + return wcheck + elif isinstance(args[0], str): + code, = args + assert isinstance(code, str) + frame = sys._getframe(1) + loc = frame.f_locals.copy() + loc.update(kwargs) + + with wcheck: + code = py.code.Source(code).compile() + py.builtin.exec_(code, frame.f_globals, loc) + else: + func = args[0] + with wcheck: + return func(*args[1:], **kwargs) + + +class RecordedWarning(object): + def __init__(self, message, category, filename, lineno, file, line): self.message = message self.category = category self.filename = filename self.lineno = lineno + self.file = file self.line = line -class WarningsRecorder: - def __init__(self): - self.list = [] - def showwarning(message, category, filename, lineno, line=0): - self.list.append(RecordedWarning( - message, category, filename, lineno, line)) - try: - self.old_showwarning(message, category, - filename, lineno, line=line) - except TypeError: - # < python2.6 - self.old_showwarning(message, category, filename, lineno) - self.old_showwarning = warnings.showwarning - warnings.showwarning = showwarning + +class WarningsRecorder(object): + """A context manager to record raised warnings. + + Adapted from `warnings.catch_warnings`. + """ + + def __init__(self, module=None): + self._module = sys.modules['warnings'] if module is None else module + self._entered = False + self._list = [] + + @property + def list(self): + """The list of recorded warnings.""" + return self._list + + def __getitem__(self, i): + """Get a recorded warning by index.""" + return self._list[i] + + def __iter__(self): + """Iterate through the recorded warnings.""" + return iter(self._list) + + def __len__(self): + """The number of recorded warnings.""" + return len(self._list) def pop(self, cls=Warning): - """ pop the first recorded warning, raise exception if not exists.""" - for i, w in enumerate(self.list): + """Pop the first recorded warning, raise exception if not exists.""" + for i, w in enumerate(self._list): if issubclass(w.category, cls): - return self.list.pop(i) + return self._list.pop(i) __tracebackhide__ = True - assert 0, "%r not found in %r" %(cls, self.list) - - #def resetregistry(self): - # warnings.onceregistry.clear() - # warnings.__warningregistry__.clear() + raise AssertionError("%r not found in warning list" % cls) def clear(self): - self.list[:] = [] + """Clear the list of recorded warnings.""" + self._list[:] = [] - def finalize(self): - warnings.showwarning = self.old_showwarning + def __enter__(self): + if self._entered: + __tracebackhide__ = True + raise RuntimeError("Cannot enter %r twice" % self) + self._entered = True + self._filters = self._module.filters + self._module.filters = self._filters[:] + self._showwarning = self._module.showwarning + + def showwarning(message, category, filename, lineno, + file=None, line=None): + self._list.append(RecordedWarning( + message, category, filename, lineno, file, line)) + + # still perform old showwarning functionality + self._showwarning( + message, category, filename, lineno, file=file, line=line) + + self._module.showwarning = showwarning + + # allow the same warning to be raised more than once + self._module.simplefilter('always', append=True) + + return self + + def __exit__(self, *exc_info): + if not self._entered: + __tracebackhide__ = True + raise RuntimeError("Cannot exit %r without entering first" % self) + self._module.filters = self._filters + self._module.showwarning = self._showwarning + + +class WarningsChecker(WarningsRecorder): + def __init__(self, expected_warning=None, module=None): + super(WarningsChecker, self).__init__(module=module) + + msg = ("exceptions must be old-style classes or " + "derived from Warning, not %s") + if isinstance(expected_warning, tuple): + for exc in expected_warning: + if not inspect.isclass(exc): + raise TypeError(msg % type(exc)) + elif inspect.isclass(expected_warning): + expected_warning = (expected_warning,) + elif expected_warning is not None: + raise TypeError(msg % type(expected_warning)) + + self.expected_warning = expected_warning + + def __exit__(self, *exc_info): + super(WarningsChecker, self).__exit__(*exc_info) + + # only check if we're not currently handling an exception + if all(a is None for a in exc_info): + if self.expected_warning is not None: + if not any(r.category in self.expected_warning for r in self): + __tracebackhide__ = True + pytest.fail("DID NOT WARN") diff --git a/_pytest/standalonetemplate.py b/_pytest/standalonetemplate.py index 46d5e41f4..484d5d1b2 100755 --- a/_pytest/standalonetemplate.py +++ b/_pytest/standalonetemplate.py @@ -68,6 +68,11 @@ class DictImporter(object): return res if __name__ == "__main__": + try: + import pkg_resources # noqa + except ImportError: + sys.stderr.write("ERROR: setuptools not installed\n") + sys.exit(2) if sys.version_info >= (3, 0): exec("def do_exec(co, loc): exec(co, loc)\n") import pickle @@ -80,6 +85,5 @@ if __name__ == "__main__": importer = DictImporter(sources) sys.meta_path.insert(0, importer) - entry = "@ENTRY@" do_exec(entry, locals()) # noqa diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 5365b4300..3fd7f00c5 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -2,6 +2,8 @@ This is a good source for looking at the various reporting hooks. """ +from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \ + EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED import pytest import pluggy import py @@ -298,13 +300,9 @@ class TerminalReporter: plugininfo = config.pluginmanager.list_plugin_distinfo() if plugininfo: - l = [] - for plugin, dist in plugininfo: - name = dist.project_name - if name.startswith("pytest-"): - name = name[7:] - l.append(name) - lines.append("plugins: %s" % ", ".join(l)) + + lines.append( + "plugins: %s" % ", ".join(_plugin_nameversions(plugininfo))) return lines def pytest_collection_finish(self, session): @@ -359,12 +357,15 @@ class TerminalReporter: outcome = yield outcome.get_result() self._tw.line("") - if exitstatus in (0, 1, 2, 4): + summary_exit_codes = ( + EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, EXIT_USAGEERROR, + EXIT_NOTESTSCOLLECTED) + if exitstatus in summary_exit_codes: self.summary_errors() self.summary_failures() self.summary_warnings() self.config.hook.pytest_terminal_summary(terminalreporter=self) - if exitstatus == 2: + if exitstatus == EXIT_INTERRUPTED: self._report_keyboardinterrupt() del self._keyboardinterrupt_memo self.summary_deselected() @@ -549,3 +550,18 @@ def build_summary_stats_line(stats): color = 'yellow' return (line, color) + + +def _plugin_nameversions(plugininfo): + l = [] + for plugin, dist in plugininfo: + # gets us name and version! + name = '{dist.project_name}-{dist.version}'.format(dist=dist) + # questionable convenience, but it keeps things short + if name.startswith("pytest-"): + name = name[7:] + # we decided to print python package names + # they can have more than one plugin + if name not in l: + l.append(name) + return l diff --git a/_pytest/tmpdir.py b/_pytest/tmpdir.py index 9f9c4da6b..44e980e2e 100644 --- a/_pytest/tmpdir.py +++ b/_pytest/tmpdir.py @@ -56,7 +56,7 @@ class TempdirFactory: # make_numbered_dir() call import getpass temproot = py.path.local.get_temproot() - rootdir = temproot.join('pytest-%s' % getpass.getuser()) + rootdir = temproot.join('pytest-of-%s' % getpass.getuser()) rootdir.ensure(dir=1) basetemp = py.path.local.make_numbered_dir(prefix='pytest-', rootdir=rootdir) diff --git a/doc/en/Makefile b/doc/en/Makefile index ce62f0ce6..e54b6b09a 100644 --- a/doc/en/Makefile +++ b/doc/en/Makefile @@ -46,7 +46,7 @@ installall: clean install installpdf @echo "done" regen: - PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.txt */*.txt + PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.rst */*.rst html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html diff --git a/doc/en/_getdoctarget.py b/doc/en/_getdoctarget.py index 70427f745..20e487bb7 100755 --- a/doc/en/_getdoctarget.py +++ b/doc/en/_getdoctarget.py @@ -6,7 +6,7 @@ def get_version_string(): fn = py.path.local(__file__).join("..", "..", "..", "_pytest", "__init__.py") for line in fn.readlines(): - if "version" in line: + if "version" in line and not line.strip().startswith('#'): return eval(line.split("=")[-1]) def get_minor_version_string(): diff --git a/doc/en/adopt.txt b/doc/en/adopt.rst similarity index 100% rename from doc/en/adopt.txt rename to doc/en/adopt.rst diff --git a/doc/en/announce/index.txt b/doc/en/announce/index.rst similarity index 100% rename from doc/en/announce/index.txt rename to doc/en/announce/index.rst diff --git a/doc/en/announce/release-2.0.0.txt b/doc/en/announce/release-2.0.0.rst similarity index 100% rename from doc/en/announce/release-2.0.0.txt rename to doc/en/announce/release-2.0.0.rst diff --git a/doc/en/announce/release-2.0.1.txt b/doc/en/announce/release-2.0.1.rst similarity index 100% rename from doc/en/announce/release-2.0.1.txt rename to doc/en/announce/release-2.0.1.rst diff --git a/doc/en/announce/release-2.0.2.txt b/doc/en/announce/release-2.0.2.rst similarity index 100% rename from doc/en/announce/release-2.0.2.txt rename to doc/en/announce/release-2.0.2.rst diff --git a/doc/en/announce/release-2.0.3.txt b/doc/en/announce/release-2.0.3.rst similarity index 100% rename from doc/en/announce/release-2.0.3.txt rename to doc/en/announce/release-2.0.3.rst diff --git a/doc/en/announce/release-2.1.0.txt b/doc/en/announce/release-2.1.0.rst similarity index 100% rename from doc/en/announce/release-2.1.0.txt rename to doc/en/announce/release-2.1.0.rst diff --git a/doc/en/announce/release-2.1.1.txt b/doc/en/announce/release-2.1.1.rst similarity index 100% rename from doc/en/announce/release-2.1.1.txt rename to doc/en/announce/release-2.1.1.rst diff --git a/doc/en/announce/release-2.1.2.txt b/doc/en/announce/release-2.1.2.rst similarity index 100% rename from doc/en/announce/release-2.1.2.txt rename to doc/en/announce/release-2.1.2.rst diff --git a/doc/en/announce/release-2.1.3.txt b/doc/en/announce/release-2.1.3.rst similarity index 100% rename from doc/en/announce/release-2.1.3.txt rename to doc/en/announce/release-2.1.3.rst diff --git a/doc/en/announce/release-2.2.0.txt b/doc/en/announce/release-2.2.0.rst similarity index 100% rename from doc/en/announce/release-2.2.0.txt rename to doc/en/announce/release-2.2.0.rst diff --git a/doc/en/announce/release-2.2.1.txt b/doc/en/announce/release-2.2.1.rst similarity index 100% rename from doc/en/announce/release-2.2.1.txt rename to doc/en/announce/release-2.2.1.rst diff --git a/doc/en/announce/release-2.2.2.txt b/doc/en/announce/release-2.2.2.rst similarity index 100% rename from doc/en/announce/release-2.2.2.txt rename to doc/en/announce/release-2.2.2.rst diff --git a/doc/en/announce/release-2.2.4.txt b/doc/en/announce/release-2.2.4.rst similarity index 100% rename from doc/en/announce/release-2.2.4.txt rename to doc/en/announce/release-2.2.4.rst diff --git a/doc/en/announce/release-2.3.0.txt b/doc/en/announce/release-2.3.0.rst similarity index 100% rename from doc/en/announce/release-2.3.0.txt rename to doc/en/announce/release-2.3.0.rst diff --git a/doc/en/announce/release-2.3.1.txt b/doc/en/announce/release-2.3.1.rst similarity index 100% rename from doc/en/announce/release-2.3.1.txt rename to doc/en/announce/release-2.3.1.rst diff --git a/doc/en/announce/release-2.3.2.txt b/doc/en/announce/release-2.3.2.rst similarity index 100% rename from doc/en/announce/release-2.3.2.txt rename to doc/en/announce/release-2.3.2.rst diff --git a/doc/en/announce/release-2.3.3.txt b/doc/en/announce/release-2.3.3.rst similarity index 100% rename from doc/en/announce/release-2.3.3.txt rename to doc/en/announce/release-2.3.3.rst diff --git a/doc/en/announce/release-2.3.4.txt b/doc/en/announce/release-2.3.4.rst similarity index 100% rename from doc/en/announce/release-2.3.4.txt rename to doc/en/announce/release-2.3.4.rst diff --git a/doc/en/announce/release-2.3.5.txt b/doc/en/announce/release-2.3.5.rst similarity index 100% rename from doc/en/announce/release-2.3.5.txt rename to doc/en/announce/release-2.3.5.rst diff --git a/doc/en/announce/release-2.4.0.txt b/doc/en/announce/release-2.4.0.rst similarity index 100% rename from doc/en/announce/release-2.4.0.txt rename to doc/en/announce/release-2.4.0.rst diff --git a/doc/en/announce/release-2.4.1.txt b/doc/en/announce/release-2.4.1.rst similarity index 100% rename from doc/en/announce/release-2.4.1.txt rename to doc/en/announce/release-2.4.1.rst diff --git a/doc/en/announce/release-2.4.2.txt b/doc/en/announce/release-2.4.2.rst similarity index 100% rename from doc/en/announce/release-2.4.2.txt rename to doc/en/announce/release-2.4.2.rst diff --git a/doc/en/announce/release-2.5.0.txt b/doc/en/announce/release-2.5.0.rst similarity index 100% rename from doc/en/announce/release-2.5.0.txt rename to doc/en/announce/release-2.5.0.rst diff --git a/doc/en/announce/release-2.5.1.txt b/doc/en/announce/release-2.5.1.rst similarity index 100% rename from doc/en/announce/release-2.5.1.txt rename to doc/en/announce/release-2.5.1.rst diff --git a/doc/en/announce/release-2.5.2.txt b/doc/en/announce/release-2.5.2.rst similarity index 100% rename from doc/en/announce/release-2.5.2.txt rename to doc/en/announce/release-2.5.2.rst diff --git a/doc/en/announce/release-2.6.0.txt b/doc/en/announce/release-2.6.0.rst similarity index 100% rename from doc/en/announce/release-2.6.0.txt rename to doc/en/announce/release-2.6.0.rst diff --git a/doc/en/announce/release-2.6.1.txt b/doc/en/announce/release-2.6.1.rst similarity index 100% rename from doc/en/announce/release-2.6.1.txt rename to doc/en/announce/release-2.6.1.rst diff --git a/doc/en/announce/release-2.6.2.txt b/doc/en/announce/release-2.6.2.rst similarity index 100% rename from doc/en/announce/release-2.6.2.txt rename to doc/en/announce/release-2.6.2.rst diff --git a/doc/en/announce/release-2.6.3.txt b/doc/en/announce/release-2.6.3.rst similarity index 100% rename from doc/en/announce/release-2.6.3.txt rename to doc/en/announce/release-2.6.3.rst diff --git a/doc/en/announce/release-2.7.0.txt b/doc/en/announce/release-2.7.0.rst similarity index 100% rename from doc/en/announce/release-2.7.0.txt rename to doc/en/announce/release-2.7.0.rst diff --git a/doc/en/announce/release-2.7.1.txt b/doc/en/announce/release-2.7.1.rst similarity index 100% rename from doc/en/announce/release-2.7.1.txt rename to doc/en/announce/release-2.7.1.rst diff --git a/doc/en/announce/release-2.7.2.txt b/doc/en/announce/release-2.7.2.rst similarity index 100% rename from doc/en/announce/release-2.7.2.txt rename to doc/en/announce/release-2.7.2.rst diff --git a/doc/en/apiref.rst b/doc/en/apiref.rst new file mode 100644 index 000000000..6b9a6a5e3 --- /dev/null +++ b/doc/en/apiref.rst @@ -0,0 +1,27 @@ + +.. _apiref: + +pytest reference documentation +================================================ + +.. toctree:: + :maxdepth: 2 + + builtin + customize + assert + fixture + yieldfixture + parametrize + xunit_setup + capture + monkeypatch + xdist + tmpdir + mark + skipping + recwarn + unittest + nose + doctest + diff --git a/doc/en/apiref.txt b/doc/en/apiref.txt deleted file mode 100644 index 5cf1c0d98..000000000 --- a/doc/en/apiref.txt +++ /dev/null @@ -1,27 +0,0 @@ - -.. _apiref: - -pytest reference documentation -================================================ - -.. toctree:: - :maxdepth: 2 - - builtin.txt - customize.txt - assert.txt - fixture.txt - yieldfixture.txt - parametrize.txt - xunit_setup.txt - capture.txt - monkeypatch.txt - xdist.txt - tmpdir.txt - mark.txt - skipping.txt - recwarn.txt - unittest.txt - nose.txt - doctest.txt - diff --git a/doc/en/assert.txt b/doc/en/assert.rst similarity index 97% rename from doc/en/assert.txt rename to doc/en/assert.rst index aed43803f..c0576ec5d 100644 --- a/doc/en/assert.txt +++ b/doc/en/assert.rst @@ -114,6 +114,16 @@ like documenting unfixed bugs (where the test describes what "should" happen) or bugs in dependencies. +.. _`assertwarns`: + +Assertions about expected warnings +----------------------------------------- + +.. versionadded:: 2.8 + +You can check that code raises a particular warning using +:ref:`pytest.warns `. + .. _newreport: @@ -228,9 +238,7 @@ Reporting details about a failing assertion is achieved either by rewriting assert statements before they are run or re-evaluating the assert expression and recording the intermediate values. Which technique is used depends on the location of the assert, ``pytest`` configuration, and Python version being used -to run ``pytest``. Note that for assert statements with a manually provided -message, i.e. ``assert expr, message``, no assertion introspection takes place -and the manually provided message will be rendered in tracebacks. +to run ``pytest``. By default, if the Python version is greater than or equal to 2.6, ``pytest`` rewrites assert statements in test modules. Rewritten assert statements put diff --git a/doc/en/bash-completion.txt b/doc/en/bash-completion.rst similarity index 100% rename from doc/en/bash-completion.txt rename to doc/en/bash-completion.rst diff --git a/doc/en/builtin.txt b/doc/en/builtin.rst similarity index 100% rename from doc/en/builtin.txt rename to doc/en/builtin.rst diff --git a/doc/en/capture.txt b/doc/en/capture.rst similarity index 100% rename from doc/en/capture.txt rename to doc/en/capture.rst diff --git a/doc/en/changelog.txt b/doc/en/changelog.rst similarity index 100% rename from doc/en/changelog.txt rename to doc/en/changelog.rst diff --git a/doc/en/conf.py b/doc/en/conf.py index 3df505434..aca0442c5 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -47,7 +47,7 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.autosummary', templates_path = ['_templates'] # The suffix of source filenames. -source_suffix = '.txt' +source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' @@ -73,13 +73,13 @@ copyright = u'2015, holger krekel and pytest-dev team' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['links.inc', '_build', 'naming20.txt', 'test/*', +exclude_patterns = ['links.inc', '_build', 'naming20.rst', 'test/*', "old_*", '*attic*', '*/attic*', - 'funcargs.txt', - 'setup.txt', - 'example/remoteinterp.txt', + 'funcargs.rst', + 'setup.rst', + 'example/remoteinterp.rst', ] diff --git a/doc/en/contact.txt b/doc/en/contact.rst similarity index 100% rename from doc/en/contact.txt rename to doc/en/contact.rst diff --git a/doc/en/contents.txt b/doc/en/contents.rst similarity index 90% rename from doc/en/contents.txt rename to doc/en/contents.rst index 4a85d9cc0..dabfcbecb 100644 --- a/doc/en/contents.txt +++ b/doc/en/contents.rst @@ -17,11 +17,11 @@ Full pytest documentation example/index talks contributing - funcarg_compare.txt + funcarg_compare announce/index .. toctree:: :hidden: - changelog.txt + changelog diff --git a/doc/en/contributing.txt b/doc/en/contributing.rst similarity index 100% rename from doc/en/contributing.txt rename to doc/en/contributing.rst diff --git a/doc/en/customize.txt b/doc/en/customize.rst similarity index 96% rename from doc/en/customize.txt rename to doc/en/customize.rst index b92194999..1c1655697 100644 --- a/doc/en/customize.txt +++ b/doc/en/customize.rst @@ -219,3 +219,10 @@ Builtin configuration file options One or more doctest flag names from the standard ``doctest`` module. :doc:`See how py.test handles doctests `. + +.. confval:: confcutdir + + Sets a directory where search upwards for ``conftest.py`` files stops. + By default, pytest will stop searching for ``conftest.py`` files upwards + from ``pytest.ini``/``tox.ini``/``setup.cfg`` of the project if any, + or up to the file-system root. diff --git a/doc/en/doctest.txt b/doc/en/doctest.rst similarity index 79% rename from doc/en/doctest.txt rename to doc/en/doctest.rst index e33fed676..a456488e3 100644 --- a/doc/en/doctest.txt +++ b/doc/en/doctest.rst @@ -72,3 +72,18 @@ ignore lengthy exception stack traces you can just write:: # content of pytest.ini [pytest] doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL + + +py.test also introduces a new ``ALLOW_UNICODE`` option flag: when enabled, the +``u`` prefix is stripped from unicode strings in expected doctest output. This +allows doctests which use unicode to run in Python 2 and 3 unchanged. + +As with any other option flag, this flag can be enabled in ``pytest.ini`` using +the ``doctest_optionflags`` ini option or by an inline comment in the doc test +itself:: + + # content of example.rst + >>> get_unicode_greeting() # doctest: +ALLOW_UNICODE + 'Hello' + + diff --git a/doc/en/example/attic.txt b/doc/en/example/attic.rst similarity index 100% rename from doc/en/example/attic.txt rename to doc/en/example/attic.rst diff --git a/doc/en/example/index.txt b/doc/en/example/index.rst similarity index 87% rename from doc/en/example/index.txt rename to doc/en/example/index.rst index eb02e6c69..363de5ab7 100644 --- a/doc/en/example/index.txt +++ b/doc/en/example/index.rst @@ -25,10 +25,10 @@ The following examples aim at various use cases you might encounter. .. toctree:: :maxdepth: 2 - reportingdemo.txt - simple.txt - parametrize.txt - markers.txt - special.txt - pythoncollection.txt - nonpython.txt + reportingdemo + simple + parametrize + markers + special + pythoncollection + nonpython diff --git a/doc/en/example/markers.txt b/doc/en/example/markers.rst similarity index 100% rename from doc/en/example/markers.txt rename to doc/en/example/markers.rst diff --git a/doc/en/example/nonpython.txt b/doc/en/example/nonpython.rst similarity index 100% rename from doc/en/example/nonpython.txt rename to doc/en/example/nonpython.rst diff --git a/doc/en/example/parametrize.txt b/doc/en/example/parametrize.rst similarity index 99% rename from doc/en/example/parametrize.txt rename to doc/en/example/parametrize.rst index 71c37699d..3d0c778f5 100644 --- a/doc/en/example/parametrize.txt +++ b/doc/en/example/parametrize.rst @@ -81,7 +81,7 @@ Numbers, strings, booleans and None will have their usual string representation used in the test ID. For other objects, pytest will make a string based on the argument name:: - # contents of test_time.py + # content of test_time.py from datetime import datetime, timedelta diff --git a/doc/en/example/pythoncollection.txt b/doc/en/example/pythoncollection.rst similarity index 100% rename from doc/en/example/pythoncollection.txt rename to doc/en/example/pythoncollection.rst diff --git a/doc/en/example/reportingdemo.txt b/doc/en/example/reportingdemo.rst similarity index 100% rename from doc/en/example/reportingdemo.txt rename to doc/en/example/reportingdemo.rst diff --git a/doc/en/example/simple.txt b/doc/en/example/simple.rst similarity index 97% rename from doc/en/example/simple.txt rename to doc/en/example/simple.rst index 34211d055..3c770eed9 100644 --- a/doc/en/example/simple.txt +++ b/doc/en/example/simple.rst @@ -534,23 +534,24 @@ case we just write some informations out to a ``failures`` file:: import pytest import os.path - @pytest.hookimpl(tryfirst=True) - def pytest_runtest_makereport(item, call, __multicall__): + @pytest.hookimpl(tryfirst=True, hookwrapper=True) + def pytest_runtest_makereport(item, call): # execute all other hooks to obtain the report object - rep = __multicall__.execute() + outcome = yield + rep = outcome.get_result() # we only look at actual failing test calls, not setup/teardown if rep.when == "call" and rep.failed: mode = "a" if os.path.exists("failures") else "w" with open("failures", mode) as f: # let's also access a fixture for the fun of it - if "tmpdir" in item.funcargs: + if "tmpdir" in item.fixturenames: extra = " (%s)" % item.funcargs["tmpdir"] else: extra = "" f.write(rep.nodeid + extra + "\n") - return rep + if you then have failing tests:: @@ -606,16 +607,16 @@ here is a little example implemented via a local plugin:: import pytest - @pytest.hookimpl(tryfirst=True) - def pytest_runtest_makereport(item, call, __multicall__): + @pytest.hookimpl(tryfirst=True, hookwrapper=True) + def pytest_runtest_makereport(item, call): # execute all other hooks to obtain the report object - rep = __multicall__.execute() + outcome = yield + rep = outcome.get_result() # set an report attribute for each phase of a call, which can # be "setup", "call", "teardown" setattr(item, "rep_" + rep.when, rep) - return rep @pytest.fixture @@ -742,5 +743,4 @@ over to ``pytest`` instead. For example:: This makes it convenient to execute your tests from within your frozen application, using standard ``py.test`` command-line options:: - $ ./app_main --pytest --verbose --tb=long --junit-xml=results.xml test-suite/ - /bin/sh: ./app_main: No such file or directory + ./app_main --pytest --verbose --tb=long --junit-xml=results.xml test-suite/ diff --git a/doc/en/example/special.txt b/doc/en/example/special.rst similarity index 100% rename from doc/en/example/special.txt rename to doc/en/example/special.rst diff --git a/doc/en/faq.txt b/doc/en/faq.rst similarity index 100% rename from doc/en/faq.txt rename to doc/en/faq.rst diff --git a/doc/en/fixture.txt b/doc/en/fixture.rst similarity index 100% rename from doc/en/fixture.txt rename to doc/en/fixture.rst diff --git a/doc/en/funcarg_compare.txt b/doc/en/funcarg_compare.rst similarity index 100% rename from doc/en/funcarg_compare.txt rename to doc/en/funcarg_compare.rst diff --git a/doc/en/funcargs.txt b/doc/en/funcargs.rst similarity index 100% rename from doc/en/funcargs.txt rename to doc/en/funcargs.rst diff --git a/doc/en/getting-started.txt b/doc/en/getting-started.rst similarity index 100% rename from doc/en/getting-started.txt rename to doc/en/getting-started.rst diff --git a/doc/en/goodpractises.txt b/doc/en/goodpractises.rst similarity index 99% rename from doc/en/goodpractises.txt rename to doc/en/goodpractises.rst index 0f36c5e6b..675ac6a9f 100644 --- a/doc/en/goodpractises.txt +++ b/doc/en/goodpractises.rst @@ -154,8 +154,8 @@ to create a JUnitXML file that Jenkins_ can pick up and generate reports. .. _standalone: .. _`genscript method`: -Create a pytest standalone script -------------------------------------------- +(deprecated) Create a pytest standalone script +----------------------------------------------- If you are a maintainer or application developer and want people who don't deal with python much to easily run tests you may generate diff --git a/doc/en/index.txt b/doc/en/index.rst similarity index 100% rename from doc/en/index.txt rename to doc/en/index.rst diff --git a/doc/en/mark.txt b/doc/en/mark.rst similarity index 100% rename from doc/en/mark.txt rename to doc/en/mark.rst diff --git a/doc/en/monkeypatch.txt b/doc/en/monkeypatch.rst similarity index 100% rename from doc/en/monkeypatch.txt rename to doc/en/monkeypatch.rst diff --git a/doc/en/naming20.txt b/doc/en/naming20.rst similarity index 100% rename from doc/en/naming20.txt rename to doc/en/naming20.rst diff --git a/doc/en/nose.txt b/doc/en/nose.rst similarity index 100% rename from doc/en/nose.txt rename to doc/en/nose.rst diff --git a/doc/en/overview.txt b/doc/en/overview.rst similarity index 61% rename from doc/en/overview.txt rename to doc/en/overview.rst index 321d79d8c..b0003effb 100644 --- a/doc/en/overview.txt +++ b/doc/en/overview.rst @@ -5,10 +5,10 @@ Getting started basics .. toctree:: :maxdepth: 2 - index.txt - getting-started.txt - usage.txt - goodpractises.txt - projects.txt - faq.txt + index + getting-started + usage + goodpractises + projects + faq diff --git a/doc/en/parametrize.txt b/doc/en/parametrize.rst similarity index 95% rename from doc/en/parametrize.txt rename to doc/en/parametrize.rst index 35f93f3dd..05f18b23b 100644 --- a/doc/en/parametrize.txt +++ b/doc/en/parametrize.rst @@ -114,6 +114,18 @@ Let's run this:: The one parameter set which caused a failure previously now shows up as an "xfailed (expected to fail)" test. +To get all combinations of multiple parametrized arguments you can stack +``parametrize`` decorators:: + + import pytest + @pytest.mark.parametrize("x", [0, 1]) + @pytest.mark.parametrize("y", [2, 3]) + def test_foo(x, y): + pass + +This will run the test with the arguments set to x=0/y=2, x=0/y=3, x=1/y=2 and +x=1/y=3. + .. note:: In versions prior to 2.4 one needed to specify the argument diff --git a/doc/en/plugins.txt b/doc/en/plugins.rst similarity index 100% rename from doc/en/plugins.txt rename to doc/en/plugins.rst diff --git a/doc/en/plugins_index/index.txt b/doc/en/plugins_index/index.rst similarity index 100% rename from doc/en/plugins_index/index.txt rename to doc/en/plugins_index/index.rst diff --git a/doc/en/projects.txt b/doc/en/projects.rst similarity index 100% rename from doc/en/projects.txt rename to doc/en/projects.rst diff --git a/doc/en/recwarn.rst b/doc/en/recwarn.rst new file mode 100644 index 000000000..c2a1e65fa --- /dev/null +++ b/doc/en/recwarn.rst @@ -0,0 +1,116 @@ + +Asserting Warnings +===================================================== + +.. _warns: + +Asserting warnings with the warns function +----------------------------------------------- + +.. versionadded:: 2.8 + +You can check that code raises a particular warning using ``pytest.warns``, +which works in a similar manner to :ref:`raises `:: + + import warnings + import pytest + + def test_warning(): + with pytest.warns(UserWarning): + warnings.warn("my warning", UserWarning) + +The test will fail if the warning in question is not raised. + +You can also call ``pytest.warns`` on a function or code string:: + + pytest.warns(expected_warning, func, *args, **kwargs) + pytest.warns(expected_warning, "func(*args, **kwargs)") + +The function also returns a list of all raised warnings (as +``warnings.WarningMessage`` objects), which you can query for +additional information:: + + with pytest.warns(RuntimeWarning) as record: + warnings.warn("another warning", RuntimeWarning) + + # check that only one warning was raised + assert len(record) == 1 + # check that the message matches + assert record[0].message.args[0] == "another warning" + +Alternatively, you can examine raised warnings in detail using the +:ref:`recwarn ` fixture (see below). + +.. _recwarn: + +Recording warnings +------------------------ + +You can record raised warnings either using ``pytest.warns`` or with +the ``recwarn`` fixture. + +To record with ``pytest.warns`` without asserting anything about the warnings, +pass ``None`` as the expected warning type:: + + with pytest.warns(None) as record: + warnings.warn("user", UserWarning) + warnings.warn("runtime", RuntimeWarning) + + assert len(record) == 2 + assert str(record[0].message) == "user" + assert str(record[1].message) == "runtime" + +The ``recwarn`` fixture will record warnings for the whole function:: + + import warnings + + def test_hello(recwarn): + warnings.warn("hello", UserWarning) + assert len(recwarn) == 1 + w = recwarn.pop(UserWarning) + assert issubclass(w.category, UserWarning) + assert str(w.message) == "hello" + assert w.filename + assert w.lineno + +Both ``recwarn`` and ``pytest.warns`` return the same interface for recorded +warnings: a WarningsRecorder instance. To view the recorded warnings, you can +iterate over this instance, call ``len`` on it to get the number of recorded +warnings, or index into it to get a particular recorded warning. It also +provides these methods: + +.. autoclass:: _pytest.recwarn.WarningsRecorder() + :members: + +Each recorded warning has the attributes ``message``, ``category``, +``filename``, ``lineno``, ``file``, and ``line``. The ``category`` is the +class of the warning. The ``message`` is the warning itself; calling +``str(message)`` will return the actual message of the warning. + + +.. _ensuring_function_triggers: + +Ensuring a function triggers a deprecation warning +------------------------------------------------------- + +You can also call a global helper for checking +that a certain function call triggers a ``DeprecationWarning``:: + + import pytest + + def test_global(): + pytest.deprecated_call(myfunction, 17) + +By default, deprecation warnings will not be caught when using ``pytest.warns`` +or ``recwarn``, since the default Python warnings filters hide +DeprecationWarnings. If you wish to record them in your own code, use the +command ``warnings.simplefilter('always')``:: + + import warnings + import pytest + + def test_deprecation(recwarn): + warnings.simplefilter('always') + warnings.warn("deprecated", DeprecationWarning) + assert len(recwarn) == 1 + assert recwarn.pop(DeprecationWarning) diff --git a/doc/en/recwarn.txt b/doc/en/recwarn.txt deleted file mode 100644 index c07a2cbe7..000000000 --- a/doc/en/recwarn.txt +++ /dev/null @@ -1,46 +0,0 @@ - -Asserting deprecation and other warnings -===================================================== - -.. _function_argument: - -The recwarn function argument ------------------------------------- - -You can use the ``recwarn`` funcarg to assert that code triggers -warnings through the Python warnings system. Here is a simple -self-contained test:: - - # content of test_recwarn.py - def test_hello(recwarn): - from warnings import warn - warn("hello", DeprecationWarning) - w = recwarn.pop(DeprecationWarning) - assert issubclass(w.category, DeprecationWarning) - assert 'hello' in str(w.message) - assert w.filename - assert w.lineno - -The ``recwarn`` function argument provides these methods: - -.. method:: pop(category=None) - - Return last warning matching the category. - -.. method:: clear() - - Clear list of warnings - - -.. _ensuring_function_triggers: - -Ensuring a function triggers a deprecation warning -------------------------------------------------------- - -You can also call a global helper for checking -that a certain function call triggers a ``DeprecationWarning``:: - - import pytest - - def test_global(): - pytest.deprecated_call(myfunction, 17) diff --git a/doc/en/release.txt b/doc/en/release.txt deleted file mode 100644 index b8345773a..000000000 --- a/doc/en/release.txt +++ /dev/null @@ -1,54 +0,0 @@ -pytest release checklist -------------------------- - -For doing a release of pytest (status April 2015) this rough checklist is used: - -1. change version numbers in ``_pytest/__init__.py`` to the to-be-released version. - (the version number in ``setup.py`` reads from that init file as well) - -2. finalize ``./CHANGELOG`` (don't forget the the header). - -3. write ``doc/en/announce/release-VERSION.txt`` - (usually copying from an earlier release version). - -4. regenerate doc examples with ``tox -e regen`` and check with ``git diff`` - if the differences show regressions. It's a bit of a manual process because - there a large part of the diff is about pytest headers or differences in - speed ("tests took X.Y seconds"). (XXX automate doc/example diffing to ignore - such changes and integrate it into "tox -e regen"). - -5. ``devpi upload`` to `your developer devpi index `_. You can create your own user and index on https://devpi.net, - an inofficial service from the devpi authors. - -6. run ``devpi use INDEX`` and ``devpi test`` from linux and windows machines - and verify test results on the index. On linux typically all environments - pass (April 2015 there is a setup problem with a cx_freeze environment) - but on windows all involving ``pexpect`` fail because pexpect does not exist - on windows and tox does not allow to have platform-specific environments. - Also on windows ``py33-trial`` fails but should probably pass (March 2015). - In any case, py26,py27,py33,py34 are required to pass for all platforms. - -7. You can fix tests/code and repeat number 6. until everything passes. - -8. Once you have sufficiently passing tox tests you can do the actual release:: - - cd doc/en/ - make install # will install to 2.7, 2.8, ... according to _pytest/__init__.py - make install-pdf # optional, requires latex packages installed - ssh pytest-dev@pytest.org # MANUAL: symlink "pytest.org/latest" to the just - # installed release docs - # browse to pytest.org to see - - devpi push pytest-VERSION pypi:NAME - git commit -a -m "... finalized pytest-VERSION" - git tag VERSION - git push - -9. send out release announcement to pytest-dev@python.org, - testing-in-python@lists.idyll.org and python-announce-list@python.org . - -10. **after the release** bump the version number in ``_pytest/__init__.py``, - to the next Minor release version (i.e. if you released ``pytest-2.8.0``, - set it to ``pytest-2.9.0.dev1``). - -11. already done :) diff --git a/doc/en/setup.txt b/doc/en/setup.rst similarity index 100% rename from doc/en/setup.txt rename to doc/en/setup.rst diff --git a/doc/en/skipping.txt b/doc/en/skipping.rst similarity index 98% rename from doc/en/skipping.txt rename to doc/en/skipping.rst index e8a36186a..77456e2de 100644 --- a/doc/en/skipping.txt +++ b/doc/en/skipping.rst @@ -83,7 +83,7 @@ As with all function :ref:`marking ` you can skip test functions at the `whole class- or module level`_. If your code targets python2.6 or above you use the skipif decorator (and any other marker) on classes:: - @pytest.mark.skipif(sys.platform == 'win32', + @pytest.mark.skipif(sys.platform != 'win32', reason="requires windows") class TestPosixCalls: @@ -97,7 +97,7 @@ If your code targets python2.5 where class-decorators are not available, you can set the ``pytestmark`` attribute of a class:: class TestPosixCalls: - pytestmark = pytest.mark.skipif(sys.platform == 'win32', + pytestmark = pytest.mark.skipif(sys.platform != 'win32', reason="requires Windows") def test_function(self): diff --git a/doc/en/status.txt b/doc/en/status.rst similarity index 100% rename from doc/en/status.txt rename to doc/en/status.rst diff --git a/doc/en/talks.txt b/doc/en/talks.rst similarity index 100% rename from doc/en/talks.txt rename to doc/en/talks.rst diff --git a/doc/en/test/attic.txt b/doc/en/test/attic.rst similarity index 100% rename from doc/en/test/attic.txt rename to doc/en/test/attic.rst diff --git a/doc/en/test/index.txt b/doc/en/test/index.rst similarity index 100% rename from doc/en/test/index.txt rename to doc/en/test/index.rst diff --git a/doc/en/test/mission.txt b/doc/en/test/mission.rst similarity index 100% rename from doc/en/test/mission.txt rename to doc/en/test/mission.rst diff --git a/doc/en/test/plugin/cov.txt b/doc/en/test/plugin/cov.rst similarity index 100% rename from doc/en/test/plugin/cov.txt rename to doc/en/test/plugin/cov.rst diff --git a/doc/en/test/plugin/coverage.txt b/doc/en/test/plugin/coverage.rst similarity index 100% rename from doc/en/test/plugin/coverage.txt rename to doc/en/test/plugin/coverage.rst diff --git a/doc/en/test/plugin/django.txt b/doc/en/test/plugin/django.rst similarity index 100% rename from doc/en/test/plugin/django.txt rename to doc/en/test/plugin/django.rst diff --git a/doc/en/test/plugin/figleaf.txt b/doc/en/test/plugin/figleaf.rst similarity index 100% rename from doc/en/test/plugin/figleaf.txt rename to doc/en/test/plugin/figleaf.rst diff --git a/doc/en/test/plugin/genscript.txt b/doc/en/test/plugin/genscript.rst similarity index 86% rename from doc/en/test/plugin/genscript.txt rename to doc/en/test/plugin/genscript.rst index b2e1c58af..ee80f233f 100644 --- a/doc/en/test/plugin/genscript.txt +++ b/doc/en/test/plugin/genscript.rst @@ -1,5 +1,5 @@ -generate standalone test script to be distributed along with an application. +(deprecated) generate standalone test script to be distributed along with an application. ============================================================================ diff --git a/doc/en/test/plugin/helpconfig.txt b/doc/en/test/plugin/helpconfig.rst similarity index 100% rename from doc/en/test/plugin/helpconfig.txt rename to doc/en/test/plugin/helpconfig.rst diff --git a/doc/en/test/plugin/index.txt b/doc/en/test/plugin/index.rst similarity index 100% rename from doc/en/test/plugin/index.txt rename to doc/en/test/plugin/index.rst diff --git a/doc/en/test/plugin/links.txt b/doc/en/test/plugin/links.rst similarity index 100% rename from doc/en/test/plugin/links.txt rename to doc/en/test/plugin/links.rst diff --git a/doc/en/test/plugin/nose.txt b/doc/en/test/plugin/nose.rst similarity index 100% rename from doc/en/test/plugin/nose.txt rename to doc/en/test/plugin/nose.rst diff --git a/doc/en/test/plugin/oejskit.txt b/doc/en/test/plugin/oejskit.rst similarity index 100% rename from doc/en/test/plugin/oejskit.txt rename to doc/en/test/plugin/oejskit.rst diff --git a/doc/en/test/plugin/terminal.txt b/doc/en/test/plugin/terminal.rst similarity index 100% rename from doc/en/test/plugin/terminal.txt rename to doc/en/test/plugin/terminal.rst diff --git a/doc/en/test/plugin/xdist.txt b/doc/en/test/plugin/xdist.rst similarity index 100% rename from doc/en/test/plugin/xdist.txt rename to doc/en/test/plugin/xdist.rst diff --git a/doc/en/tmpdir.txt b/doc/en/tmpdir.rst similarity index 100% rename from doc/en/tmpdir.txt rename to doc/en/tmpdir.rst diff --git a/doc/en/unittest.txt b/doc/en/unittest.rst similarity index 100% rename from doc/en/unittest.txt rename to doc/en/unittest.rst diff --git a/doc/en/usage.txt b/doc/en/usage.rst similarity index 89% rename from doc/en/usage.txt rename to doc/en/usage.rst index 9984a2ac9..85478d51c 100644 --- a/doc/en/usage.txt +++ b/doc/en/usage.rst @@ -153,6 +153,36 @@ integration servers, use this invocation:: to create an XML file at ``path``. +record_xml_property +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 2.8 + +If you want to log additional information for a test, you can use the +``record_xml_property`` fixture: + +.. code-block:: python + + def test_function(record_xml_property): + record_xml_property("example_key", 1) + assert 0 + +This will add an extra property ``example_key="1"`` to the generated +``testcase`` tag: + +.. code-block:: xml + + + +.. warning:: + + This is an experimental feature, and its interface might be replaced + by something more powerful and general in future versions. The + functionality per-se will be kept, however. + + Also please note that using this feature will break any schema verification. + This might be a problem when used with some CI servers. + Creating resultlog format files ---------------------------------------------------- diff --git a/doc/en/writing_plugins.txt b/doc/en/writing_plugins.rst similarity index 100% rename from doc/en/writing_plugins.txt rename to doc/en/writing_plugins.rst diff --git a/doc/en/xdist.txt b/doc/en/xdist.rst similarity index 100% rename from doc/en/xdist.txt rename to doc/en/xdist.rst diff --git a/doc/en/xunit_setup.txt b/doc/en/xunit_setup.rst similarity index 100% rename from doc/en/xunit_setup.txt rename to doc/en/xunit_setup.rst diff --git a/doc/en/yieldfixture.txt b/doc/en/yieldfixture.rst similarity index 100% rename from doc/en/yieldfixture.txt rename to doc/en/yieldfixture.rst diff --git a/pytest.py b/pytest.py index 8549ba781..161c44822 100644 --- a/pytest.py +++ b/pytest.py @@ -15,7 +15,7 @@ from _pytest.config import ( main, UsageError, _preloadplugins, cmdline, hookspec, hookimpl ) -from _pytest import __version__ +from _pytest import version as __version__ _preloadplugins() # to populate pytest.* namespace so help(pytest) works diff --git a/setup.py b/setup.py index 218c14da8..69b6f5e5c 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,7 @@ def main(): name='pytest', description='pytest: simple powerful testing with Python', long_description=long_description, - version=get_version(), + use_scm_version={'write_to': '_pytest/__init__.py'}, url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], @@ -75,6 +75,7 @@ def main(): # the following should be enabled for release install_requires=install_requires, extras_require=extras_require, + setup_requires=['setuptools_scm'], packages=['_pytest', '_pytest.assertion'], py_modules=['pytest'], zip_safe=False, diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index d845fd46a..b9a3fa381 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,5 +1,7 @@ import sys import py, pytest +from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR + class TestGeneralUsage: def test_config_error(self, testdir): @@ -147,7 +149,7 @@ class TestGeneralUsage: pytest.skip("early") """) result = testdir.runpytest() - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED result.stdout.fnmatch_lines([ "*1 skip*" ]) @@ -177,7 +179,7 @@ class TestGeneralUsage: sys.stderr.write("stder42\\n") """) result = testdir.runpytest() - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED assert "should not be seen" not in result.stdout.str() assert "stderr42" not in result.stderr.str() @@ -212,13 +214,13 @@ class TestGeneralUsage: sub2 = testdir.tmpdir.mkdir("sub2") sub1.join("conftest.py").write("assert 0") result = testdir.runpytest(sub2) - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED sub2.ensure("__init__.py") p = sub2.ensure("test_hello.py") result = testdir.runpytest(p) - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED result = testdir.runpytest(sub1) - assert result.ret != 0 + assert result.ret == EXIT_USAGEERROR def test_directory_skipped(self, testdir): testdir.makeconftest(""" @@ -228,7 +230,7 @@ class TestGeneralUsage: """) testdir.makepyfile("def test_hello(): pass") result = testdir.runpytest() - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED result.stdout.fnmatch_lines([ "*1 skipped*" ]) @@ -479,7 +481,7 @@ class TestInvocationVariants: def test_invoke_with_path(self, tmpdir, capsys): retcode = pytest.main(tmpdir) - assert not retcode + assert retcode == EXIT_NOTESTSCOLLECTED out, err = capsys.readouterr() def test_invoke_plugin_api(self, testdir, capsys): @@ -586,6 +588,11 @@ class TestInvocationVariants: assert type(_pytest.config.get_plugin_manager()) is _pytest.config.PytestPluginManager + def test_has_plugin(self, request): + """Test hasplugin function of the plugin manager (#932).""" + assert request.config.pluginmanager.hasplugin('python') + + class TestDurations: source = """ import time diff --git a/testing/python/collect.py b/testing/python/collect.py index dc073b103..6a302f291 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1,6 +1,8 @@ import sys from textwrap import dedent import pytest, py +from _pytest.main import EXIT_NOTESTSCOLLECTED + class TestModule: def test_failing_import(self, testdir): @@ -412,9 +414,19 @@ class TestFunction: ['overridden']) def test_overridden_via_param(value): assert value == 'overridden' + + @pytest.mark.parametrize('somevalue', ['overridden']) + def test_not_overridden(value, somevalue): + assert value == 'value' + assert somevalue == 'overridden' + + @pytest.mark.parametrize('other,value', [('foo', 'overridden')]) + def test_overridden_via_multiparam(other, value): + assert other == 'foo' + assert value == 'overridden' """) rec = testdir.inline_run() - rec.assertoutcome(passed=1) + rec.assertoutcome(passed=3) def test_parametrize_overrides_parametrized_fixture(self, testdir): @@ -472,6 +484,38 @@ class TestFunction: config.pluginmanager.register(MyPlugin2()) config.hook.pytest_pyfunc_call(pyfuncitem=item) + def test_multiple_parametrize(self, testdir): + modcol = testdir.getmodulecol(""" + import pytest + @pytest.mark.parametrize('x', [0, 1]) + @pytest.mark.parametrize('y', [2, 3]) + def test1(x, y): + pass + """) + colitems = modcol.collect() + assert colitems[0].name == 'test1[2-0]' + assert colitems[1].name == 'test1[2-1]' + assert colitems[2].name == 'test1[3-0]' + assert colitems[3].name == 'test1[3-1]' + + def test_issue751_multiple_parametrize_with_ids(self, testdir): + modcol = testdir.getmodulecol(""" + import pytest + @pytest.mark.parametrize('x', [0], ids=['c']) + @pytest.mark.parametrize('y', [0, 1], ids=['a', 'b']) + class Test(object): + def test1(self, x, y): + pass + def test2(self, x, y): + pass + """) + colitems = modcol.collect()[0].collect()[0].collect() + assert colitems[0].name == 'test1[a-c]' + assert colitems[1].name == 'test1[b-c]' + assert colitems[2].name == 'test2[a-c]' + assert colitems[3].name == 'test2[b-c]' + + class TestSorting: def test_check_equality(self, testdir): modcol = testdir.getmodulecol(""" @@ -864,7 +908,7 @@ def test_unorderable_types(testdir): """) result = testdir.runpytest() assert "TypeError" not in result.stdout.str() - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED def test_collect_functools_partial(testdir): diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 8bf738c5d..48f52d2a0 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1598,6 +1598,22 @@ class TestFixtureMarker: reprec = testdir.inline_run() reprec.assertoutcome(passed=4) + def test_multiple_parametrization_issue_736(self, testdir): + testdir.makepyfile(""" + import pytest + + @pytest.fixture(params=[1,2,3]) + def foo(request): + return request.param + + @pytest.mark.parametrize('foobar', [4,5,6]) + def test_issue(foo, foobar): + assert foo in [1,2,3] + assert foobar in [4,5,6] + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=9) + def test_scope_session(self, testdir): testdir.makepyfile(""" import pytest diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 6ef80eeea..d072d2aeb 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1,3 +1,4 @@ +import re import pytest, py from _pytest import python as funcargs @@ -138,6 +139,8 @@ class TestMetafunc: ("three", "three hundred"), (True, False), (None, None), + (re.compile('foo'), re.compile('bar')), + (str, int), (list("six"), [66, 66]), (set([7]), set("seven")), (tuple("eight"), (8, -8, 8)) @@ -147,9 +150,18 @@ class TestMetafunc: "three-three hundred", "True-False", "None-None", - "a5-b5", - "a6-b6", - "a7-b7"] + "foo-bar", + "str-int", + "a7-b7", + "a8-b8", + "a9-b9"] + + def test_idmaker_enum(self): + from _pytest.python import idmaker + enum = pytest.importorskip("enum") + e = enum.Enum("Foo", "one, two") + result = idmaker(("a", "b"), [(e.one, e.two)]) + assert result == ["Foo.one-Foo.two"] @pytest.mark.issue351 def test_idmaker_idfn(self): @@ -742,18 +754,20 @@ class TestMetafuncFunctional: reprec.assert_outcomes(passed=4) @pytest.mark.issue463 - def test_parameterize_misspelling(self, testdir): + @pytest.mark.parametrize('attr', ['parametrise', 'parameterize', + 'parameterise']) + def test_parametrize_misspelling(self, testdir, attr): testdir.makepyfile(""" import pytest - @pytest.mark.parameterize("x", range(2)) + @pytest.mark.{0}("x", range(2)) def test_foo(x): pass - """) + """.format(attr)) reprec = testdir.inline_run('--collectonly') failures = reprec.getfailures() assert len(failures) == 1 - expectederror = "MarkerError: test_foo has 'parameterize', spelling should be 'parametrize'" + expectederror = "MarkerError: test_foo has '{0}', spelling should be 'parametrize'".format(attr) assert expectederror in failures[0].longrepr.reprcrash.message diff --git a/testing/python/raises.py b/testing/python/raises.py index 5ba56bb71..edeb52226 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -34,7 +34,6 @@ class TestRaises: raise BuiltinAssertionError """) - @pytest.mark.skipif('sys.version < "2.5"') def test_raises_as_contextmanager(self, testdir): testdir.makepyfile(""" from __future__ import with_statement diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index fbac2b9c1..544250ad5 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -12,6 +12,7 @@ if sys.platform.startswith("java"): from _pytest.assertion import util from _pytest.assertion.rewrite import rewrite_asserts, PYTEST_TAG +from _pytest.main import EXIT_NOTESTSCOLLECTED def setup_module(mod): @@ -429,7 +430,7 @@ class TestRewriteOnImport: import sys sys.path.append(%r) import test_gum.test_lizard""" % (z_fn,)) - assert testdir.runpytest().ret == 0 + assert testdir.runpytest().ret == EXIT_NOTESTSCOLLECTED def test_readonly(self, testdir): sub = testdir.mkdir("testing") @@ -497,7 +498,7 @@ def test_rewritten(): pkg = testdir.mkdir('a_package_without_init_py') pkg.join('module.py').ensure() testdir.makepyfile("import a_package_without_init_py.module") - assert testdir.runpytest().ret == 0 + assert testdir.runpytest().ret == EXIT_NOTESTSCOLLECTED class TestAssertionRewriteHookDetails(object): def test_loader_is_package_false_for_module(self, testdir): diff --git a/testing/test_capture.py b/testing/test_capture.py index b20961c03..539333525 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -10,6 +10,7 @@ import contextlib from _pytest import capture from _pytest.capture import CaptureManager +from _pytest.main import EXIT_NOTESTSCOLLECTED from py.builtin import print_ needsosdup = pytest.mark.xfail("not hasattr(os, 'dup')") @@ -365,7 +366,7 @@ class TestLoggingInteraction: """) # make sure that logging is still captured in tests result = testdir.runpytest_subprocess("-s", "-p", "no:capturelog") - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED result.stderr.fnmatch_lines([ "WARNING*hello435*", ]) @@ -566,6 +567,25 @@ def test_capture_binary_output(testdir): result.assert_outcomes(passed=2) +def test_error_during_readouterr(testdir): + """Make sure we suspend capturing if errors occurr during readouterr""" + testdir.makepyfile(pytest_xyz=""" + from _pytest.capture import FDCapture + def bad_snap(self): + raise Exception('boom') + assert FDCapture.snap + FDCapture.snap = bad_snap + """) + result = testdir.runpytest_subprocess( + "-p", "pytest_xyz", "--version", syspathinsert=True + ) + result.stderr.fnmatch_lines([ + "*in bad_snap", + " raise Exception('boom')", + "Exception: boom", + ]) + + class TestTextIO: def test_text(self): f = capture.TextIO() diff --git a/testing/test_collection.py b/testing/test_collection.py index a7cb8a8c4..749c5b7ce 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,6 +1,6 @@ import pytest, py -from _pytest.main import Session +from _pytest.main import Session, EXIT_NOTESTSCOLLECTED class TestCollector: def test_collect_versus_item(self): @@ -247,10 +247,10 @@ class TestCustomConftests: p = testdir.makepyfile("def test_hello(): pass") result = testdir.runpytest(p) assert result.ret == 0 - assert "1 passed" in result.stdout.str() + result.stdout.fnmatch_lines("*1 passed*") result = testdir.runpytest() - assert result.ret == 0 - assert "1 passed" not in result.stdout.str() + assert result.ret == EXIT_NOTESTSCOLLECTED + result.stdout.fnmatch_lines("*collected 0 items*") def test_collectignore_exclude_on_option(self, testdir): testdir.makeconftest(""" @@ -264,7 +264,7 @@ class TestCustomConftests: testdir.mkdir("hello") testdir.makepyfile(test_world="def test_hello(): pass") result = testdir.runpytest() - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED assert "passed" not in result.stdout.str() result = testdir.runpytest("--XX") assert result.ret == 0 diff --git a/testing/test_config.py b/testing/test_config.py index 490fa96d0..9d3f7632c 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,6 +1,7 @@ import py, pytest from _pytest.config import getcfg, get_common_ancestor, determine_setup +from _pytest.main import EXIT_NOTESTSCOLLECTED class TestParseIni: def test_getcfg_and_config(self, testdir, tmpdir): @@ -343,7 +344,7 @@ def test_invalid_options_show_extra_information(testdir): @pytest.mark.skipif("sys.platform == 'win32'") def test_toolongargs_issue224(testdir): result = testdir.runpytest("-m", "hello" * 500) - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED def test_notify_exception(testdir, capfd): config = testdir.parseconfig() diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 513ba43a5..6700502c4 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,6 +1,7 @@ from textwrap import dedent import py, pytest from _pytest.config import PytestPluginManager +from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR @pytest.fixture(scope="module", params=["global", "inpackage"]) @@ -166,7 +167,10 @@ def test_conftest_confcutdir(testdir): def test_no_conftest(testdir): testdir.makeconftest("assert 0") result = testdir.runpytest("--noconftest") - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED + + result = testdir.runpytest() + assert result.ret == EXIT_USAGEERROR def test_conftest_existing_resultlog(testdir): x = testdir.mkdir("tests") @@ -343,3 +347,44 @@ class TestConftestVisibility: with dirs[chdir].as_cwd(): reprec = testdir.inline_run(testarg, "-q", "--traceconfig") reprec.assertoutcome(passed=expect_ntests_passed) + + +@pytest.mark.parametrize('confcutdir,passed,error', [ + ('.', 2, 0), + ('src', 1, 1), + (None, 1, 1), +]) +def test_search_conftest_up_to_inifile(testdir, confcutdir, passed, error): + """Test that conftest files are detected only up to a ini file, unless + an explicit --confcutdir option is given. + """ + root = testdir.tmpdir + src = root.join('src').ensure(dir=1) + src.join('pytest.ini').write('[pytest]') + src.join('conftest.py').write(py.code.Source(""" + import pytest + @pytest.fixture + def fix1(): pass + """)) + src.join('test_foo.py').write(py.code.Source(""" + def test_1(fix1): + pass + def test_2(out_of_reach): + pass + """)) + root.join('conftest.py').write(py.code.Source(""" + import pytest + @pytest.fixture + def out_of_reach(): pass + """)) + + args = [str(src)] + if confcutdir: + args = ['--confcutdir=%s' % root.join(confcutdir)] + result = testdir.runpytest(*args) + match = '' + if passed: + match += '*%d passed*' % passed + if error: + match += '*%d error*' % error + result.stdout.fnmatch_lines(match) diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 882747b9e..6975ecc2c 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,5 +1,7 @@ +import sys from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile import py +import pytest class TestDoctests: @@ -401,3 +403,46 @@ class TestDoctests: result = testdir.runpytest("--doctest-modules") result.stdout.fnmatch_lines('*2 passed*') + @pytest.mark.parametrize('config_mode', ['ini', 'comment']) + def test_allow_unicode(self, testdir, config_mode): + """Test that doctests which output unicode work in all python versions + tested by pytest when the ALLOW_UNICODE option is used (either in + the ini file or by an inline comment). + """ + if config_mode == 'ini': + testdir.makeini(''' + [pytest] + doctest_optionflags = ALLOW_UNICODE + ''') + comment = '' + else: + comment = '#doctest: +ALLOW_UNICODE' + + testdir.maketxtfile(test_doc=""" + >>> b'12'.decode('ascii') {comment} + '12' + """.format(comment=comment)) + testdir.makepyfile(foo=""" + def foo(): + ''' + >>> b'12'.decode('ascii') {comment} + '12' + ''' + """.format(comment=comment)) + reprec = testdir.inline_run("--doctest-modules") + reprec.assertoutcome(passed=2) + + def test_unicode_string(self, testdir): + """Test that doctests which output unicode fail in Python 2 when + the ALLOW_UNICODE option is not used. The same test should pass + in Python 3. + """ + testdir.maketxtfile(test_doc=""" + >>> b'12'.decode('ascii') + '12' + """) + reprec = testdir.inline_run() + passed = int(sys.version_info[0] >= 3) + reprec.assertoutcome(passed=passed, failed=int(not passed)) + + diff --git a/testing/test_genscript.py b/testing/test_genscript.py index 405bc0236..1260a5a6b 100644 --- a/testing/test_genscript.py +++ b/testing/test_genscript.py @@ -27,13 +27,17 @@ def test_gen(testdir, anypython, standalone): pytest.skip("genscript called from python2.7 cannot work " "earlier python versions") result = standalone.run(anypython, testdir, '--version') - assert result.ret == 0 - result.stderr.fnmatch_lines([ - "*imported from*mypytest*" - ]) - p = testdir.makepyfile("def test_func(): assert 0") - result = standalone.run(anypython, testdir, p) - assert result.ret != 0 + if result.ret == 2: + result.stderr.fnmatch_lines(["*ERROR: setuptools not installed*"]) + elif result.ret == 0: + result.stderr.fnmatch_lines([ + "*imported from*mypytest*" + ]) + p = testdir.makepyfile("def test_func(): assert 0") + result = standalone.run(anypython, testdir, p) + assert result.ret != 0 + else: + pytest.fail("Unexpected return code") def test_freeze_includes(): diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index d9cb52bcb..9f8d87b7c 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -1,3 +1,4 @@ +from _pytest.main import EXIT_NOTESTSCOLLECTED import pytest def test_version(testdir, pytestconfig): @@ -43,7 +44,7 @@ def test_hookvalidation_optional(testdir): pass """) result = testdir.runpytest() - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED def test_traceconfig(testdir): result = testdir.runpytest("--traceconfig") @@ -54,14 +55,14 @@ def test_traceconfig(testdir): def test_debug(testdir, monkeypatch): result = testdir.runpytest_subprocess("--debug") - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED p = testdir.tmpdir.join("pytestdebug.log") assert "pytest_sessionstart" in p.read() def test_PYTEST_DEBUG(testdir, monkeypatch): monkeypatch.setenv("PYTEST_DEBUG", "1") result = testdir.runpytest_subprocess() - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED result.stderr.fnmatch_lines([ "*pytest_plugin_registered*", "*manager*PluginManager*" diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index f8acd1576..cb4d0c444 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from xml.dom import minidom +from _pytest.main import EXIT_NOTESTSCOLLECTED import py, sys, os from _pytest.junitxml import LogXML @@ -298,7 +299,7 @@ class TestPython: def test_collect_skipped(self, testdir): testdir.makepyfile("import pytest; pytest.skip('xyz')") result, dom = runandparse(testdir) - assert not result.ret + assert result.ret == EXIT_NOTESTSCOLLECTED node = dom.getElementsByTagName("testsuite")[0] assert_attr(node, skips=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] @@ -552,4 +553,13 @@ def test_unicode_issue368(testdir): log.append_skipped(report) log.pytest_sessionfinish() - +def test_record_property(testdir): + testdir.makepyfile(""" + def test_record(record_xml_property): + record_xml_property("foo", "<1"); + """) + result, dom = runandparse(testdir, '-rw') + node = dom.getElementsByTagName("testsuite")[0] + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, foo="<1") + result.stdout.fnmatch_lines('*C3*test_record_property.py*experimental*') diff --git a/testing/test_mark.py b/testing/test_mark.py index ec63bedf7..1aa336183 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1,3 +1,5 @@ +import os + import py, pytest from _pytest.mark import MarkGenerator as Mark @@ -84,6 +86,22 @@ class TestMark: assert g.some.kwargs['reason2'] == "456" +def test_marked_class_run_twice(testdir, request): + """Test fails file is run twice that contains marked class. + See issue#683. + """ + py_file = testdir.makepyfile(""" + import pytest + @pytest.mark.parametrize('abc', [1, 2, 3]) + class Test1(object): + def test_1(self, abc): + assert abc in [1, 2, 3] + """) + file_name = os.path.basename(py_file.strpath) + rec = testdir.inline_run(file_name, file_name) + rec.assertoutcome(passed=6) + + def test_ini_markers(testdir): testdir.makeini(""" [pytest] diff --git a/testing/test_nose.py b/testing/test_nose.py index 76873a834..6260aae47 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -347,3 +347,49 @@ def test_SkipTest_in_test(testdir): """) reprec = testdir.inline_run() reprec.assertoutcome(skipped=1) + +def test_istest_function_decorator(testdir): + p = testdir.makepyfile(""" + import nose.tools + @nose.tools.istest + def not_test_prefix(): + pass + """) + result = testdir.runpytest(p) + result.assert_outcomes(passed=1) + +def test_nottest_function_decorator(testdir): + testdir.makepyfile(""" + import nose.tools + @nose.tools.nottest + def test_prefix(): + pass + """) + reprec = testdir.inline_run() + assert not reprec.getfailedcollections() + calls = reprec.getreports("pytest_runtest_logreport") + assert not calls + +def test_istest_class_decorator(testdir): + p = testdir.makepyfile(""" + import nose.tools + @nose.tools.istest + class NotTestPrefix: + def test_method(self): + pass + """) + result = testdir.runpytest(p) + result.assert_outcomes(passed=1) + +def test_nottest_class_decorator(testdir): + testdir.makepyfile(""" + import nose.tools + @nose.tools.nottest + class TestPrefix: + def test_method(self): + pass + """) + reprec = testdir.inline_run() + assert not reprec.getfailedcollections() + calls = reprec.getreports("pytest_runtest_logreport") + assert not calls diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 62ecc544f..92afba9bc 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -3,6 +3,7 @@ import py import os from _pytest.config import get_config, PytestPluginManager +from _pytest.main import EXIT_NOTESTSCOLLECTED @pytest.fixture def pytestpm(): @@ -223,7 +224,7 @@ class TestPytestPluginManager: p.copy(p.dirpath("skipping2.py")) monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") result = testdir.runpytest("-rw", "-p", "skipping1", syspathinsert=True) - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED result.stdout.fnmatch_lines([ "WI1*skipped plugin*skipping1*hello*", "WI1*skipped plugin*skipping2*hello*", diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 21909ae2a..644b09ef7 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -1,24 +1,8 @@ -import py, pytest +import warnings +import py +import pytest from _pytest.recwarn import WarningsRecorder -def test_WarningRecorder(recwarn): - showwarning = py.std.warnings.showwarning - rec = WarningsRecorder() - assert py.std.warnings.showwarning != showwarning - assert not rec.list - py.std.warnings.warn_explicit("hello", UserWarning, "xyz", 13) - assert len(rec.list) == 1 - py.std.warnings.warn(DeprecationWarning("hello")) - assert len(rec.list) == 2 - warn = rec.pop() - assert str(warn.message) == "hello" - l = rec.list - rec.clear() - assert len(rec.list) == 0 - assert l is rec.list - pytest.raises(AssertionError, "rec.pop()") - rec.finalize() - assert showwarning == py.std.warnings.showwarning def test_recwarn_functional(testdir): reprec = testdir.inline_runsource(""" @@ -35,6 +19,50 @@ def test_recwarn_functional(testdir): res = reprec.countoutcomes() assert tuple(res) == (2, 0, 0), res + +class TestWarningsRecorderChecker(object): + def test_recording(self, recwarn): + showwarning = py.std.warnings.showwarning + rec = WarningsRecorder() + with rec: + assert py.std.warnings.showwarning != showwarning + assert not rec.list + py.std.warnings.warn_explicit("hello", UserWarning, "xyz", 13) + assert len(rec.list) == 1 + py.std.warnings.warn(DeprecationWarning("hello")) + assert len(rec.list) == 2 + warn = rec.pop() + assert str(warn.message) == "hello" + l = rec.list + rec.clear() + assert len(rec.list) == 0 + assert l is rec.list + pytest.raises(AssertionError, "rec.pop()") + + assert showwarning == py.std.warnings.showwarning + + def test_typechecking(self): + from _pytest.recwarn import WarningsChecker + with pytest.raises(TypeError): + WarningsChecker(5) + with pytest.raises(TypeError): + WarningsChecker(('hi', RuntimeWarning)) + with pytest.raises(TypeError): + WarningsChecker([DeprecationWarning, RuntimeWarning]) + + def test_invalid_enter_exit(self): + # wrap this test in WarningsRecorder to ensure warning state gets reset + with WarningsRecorder(): + with pytest.raises(RuntimeError): + rec = WarningsRecorder() + rec.__exit__(None, None, None) # can't exit before entering + + with pytest.raises(RuntimeError): + rec = WarningsRecorder() + with rec: + with rec: + pass # can't enter twice + # # ============ test pytest.deprecated_call() ============== # @@ -50,35 +78,107 @@ def dep_explicit(i): py.std.warnings.warn_explicit("dep_explicit", category=DeprecationWarning, filename="hello", lineno=3) -def test_deprecated_call_raises(): - excinfo = pytest.raises(AssertionError, - "pytest.deprecated_call(dep, 3)") - assert str(excinfo).find("did not produce") != -1 +class TestDeprecatedCall(object): + def test_deprecated_call_raises(self): + excinfo = pytest.raises(AssertionError, + "pytest.deprecated_call(dep, 3)") + assert str(excinfo).find("did not produce") != -1 -def test_deprecated_call(): - pytest.deprecated_call(dep, 0) + def test_deprecated_call(self): + pytest.deprecated_call(dep, 0) -def test_deprecated_call_ret(): - ret = pytest.deprecated_call(dep, 0) - assert ret == 42 + def test_deprecated_call_ret(self): + ret = pytest.deprecated_call(dep, 0) + assert ret == 42 -def test_deprecated_call_preserves(): - 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 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_call_preserves(self): + onceregistry = py.std.warnings.onceregistry.copy() + filters = py.std.warnings.filters[:] + warn = py.std.warnings.warn + warn_explicit = py.std.warnings.warn_explicit + self.test_deprecated_call_raises() + self.test_deprecated_call() + 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, - "pytest.deprecated_call(dep_explicit, 3)") + def test_deprecated_explicit_call_raises(self): + pytest.raises(AssertionError, + "pytest.deprecated_call(dep_explicit, 3)") -def test_deprecated_explicit_call(): - pytest.deprecated_call(dep_explicit, 0) - pytest.deprecated_call(dep_explicit, 0) + def test_deprecated_explicit_call(self): + pytest.deprecated_call(dep_explicit, 0) + pytest.deprecated_call(dep_explicit, 0) + + +class TestWarns(object): + def test_strings(self): + # different messages, b/c Python suppresses multiple identical warnings + source1 = "warnings.warn('w1', RuntimeWarning)" + source2 = "warnings.warn('w2', RuntimeWarning)" + source3 = "warnings.warn('w3', RuntimeWarning)" + pytest.warns(RuntimeWarning, source1) + pytest.raises(pytest.fail.Exception, + lambda: pytest.warns(UserWarning, source2)) + pytest.warns(RuntimeWarning, source3) + + def test_function(self): + pytest.warns(SyntaxWarning, + lambda msg: warnings.warn(msg, SyntaxWarning), "syntax") + + def test_warning_tuple(self): + pytest.warns((RuntimeWarning, SyntaxWarning), + lambda: warnings.warn('w1', RuntimeWarning)) + pytest.warns((RuntimeWarning, SyntaxWarning), + lambda: warnings.warn('w2', SyntaxWarning)) + pytest.raises(pytest.fail.Exception, + lambda: pytest.warns( + (RuntimeWarning, SyntaxWarning), + lambda: warnings.warn('w3', UserWarning))) + + def test_as_contextmanager(self): + with pytest.warns(RuntimeWarning): + warnings.warn("runtime", RuntimeWarning) + + with pytest.raises(pytest.fail.Exception): + with pytest.warns(RuntimeWarning): + warnings.warn("user", UserWarning) + + with pytest.raises(pytest.fail.Exception): + with pytest.warns(UserWarning): + warnings.warn("runtime", RuntimeWarning) + + with pytest.warns(UserWarning): + warnings.warn("user", UserWarning) + + def test_record(self): + with pytest.warns(UserWarning) as record: + warnings.warn("user", UserWarning) + + assert len(record) == 1 + assert str(record[0].message) == "user" + + def test_record_only(self): + with pytest.warns(None) as record: + warnings.warn("user", UserWarning) + warnings.warn("runtime", RuntimeWarning) + + assert len(record) == 2 + assert str(record[0].message) == "user" + assert str(record[1].message) == "runtime" + + def test_double_test(self, testdir): + """If a test is run again, the warning should still be raised""" + testdir.makepyfile(''' + import pytest + import warnings + + @pytest.mark.parametrize('run', [1, 2]) + def test(run): + with pytest.warns(RuntimeWarning): + warnings.warn("runtime", RuntimeWarning) + ''') + result = testdir.runpytest() + result.stdout.fnmatch_lines(['*2 passed in*']) diff --git a/testing/test_runner.py b/testing/test_runner.py index 167ddc57b..3641ab8ca 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -293,8 +293,8 @@ class TestExecutionForked(BaseFunctionalTests): def getrunner(self): # XXX re-arrange this test to live in pytest-xdist - xplugin = pytest.importorskip("xdist.plugin") - return xplugin.forked_run_report + boxed = pytest.importorskip("xdist.boxed") + return boxed.forked_run_report def test_suicide(self, testdir): reports = testdir.runitem(""" @@ -431,6 +431,27 @@ def test_pytest_fail_notrace(testdir): ]) assert 'def teardown_function' not in result.stdout.str() + +def test_pytest_no_tests_collected_exit_status(testdir): + result = testdir.runpytest() + result.stdout.fnmatch_lines('*collected 0 items*') + assert result.ret == main.EXIT_NOTESTSCOLLECTED + + testdir.makepyfile(test_foo=""" + def test_foo(): + assert 1 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines('*collected 1 items*') + result.stdout.fnmatch_lines('*1 passed*') + assert result.ret == main.EXIT_OK + + result = testdir.runpytest('-k nonmatch') + result.stdout.fnmatch_lines('*collected 1 items*') + result.stdout.fnmatch_lines('*1 deselected*') + assert result.ret == main.EXIT_NOTESTSCOLLECTED + + def test_exception_printing_skip(): try: pytest.skip("hello") diff --git a/testing/test_session.py b/testing/test_session.py index 0ddb92ac1..76f804b4f 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -1,5 +1,7 @@ import pytest +from _pytest.main import EXIT_NOTESTSCOLLECTED + class SessionTests: def test_basic_testitem_events(self, testdir): tfile = testdir.makepyfile(""" @@ -239,4 +241,4 @@ def test_sessionfinish_with_start(testdir): """) res = testdir.runpytest("--collect-only") - assert res.ret == 0 + assert res.ret == EXIT_NOTESTSCOLLECTED diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 7ad74a921..0493b850d 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1,19 +1,24 @@ """ terminal reporting of the full testing process. """ +import collections import pytest import py import pluggy import sys +from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.terminal import TerminalReporter, repr_pythonversion, getreportopt -from _pytest.terminal import build_summary_stats_line +from _pytest.terminal import build_summary_stats_line, _plugin_nameversions from _pytest import runner 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']) + + class Option: def __init__(self, verbose=False, fulltrace=False): self.verbose = verbose @@ -40,6 +45,21 @@ def pytest_generate_tests(metafunc): funcargs={'option': Option(fulltrace=True)}) +@pytest.mark.parametrize('input,expected', [ + ([DistInfo(project_name='test', version=1)], ['test-1']), + ([DistInfo(project_name='pytest-test', version=1)], ['test-1']), + ([ + DistInfo(project_name='test', version=1), + DistInfo(project_name='test', version=1) + ], ['test-1']), +], ids=['normal', 'prefix-strip', 'deduplicate']) + +def test_plugin_nameversion(input, expected): + pluginlist = [(None, x) for x in input] + result = _plugin_nameversions(pluginlist) + assert result == expected + + class TestTerminal: def test_pass_skip_fail(self, testdir, option): testdir.makepyfile(""" @@ -577,7 +597,7 @@ def test_traceconfig(testdir, monkeypatch): result.stdout.fnmatch_lines([ "*active plugins*" ]) - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED class TestGenericReporting: @@ -783,4 +803,3 @@ 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 - diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 7c2095e6e..05b24fc59 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -95,6 +95,7 @@ def test_tmpdir_always_is_realpath(testdir): result = testdir.runpytest("-s", p, '--basetemp=%s/bt' % linktemp) assert not result.ret + def test_tmpdir_too_long_on_parametrization(testdir): testdir.makepyfile(""" import pytest @@ -104,3 +105,16 @@ def test_tmpdir_too_long_on_parametrization(testdir): """) reprec = testdir.inline_run() reprec.assertoutcome(passed=1) + + +def test_tmpdir_factory(testdir): + testdir.makepyfile(""" + import pytest + @pytest.fixture(scope='session') + def session_dir(tmpdir_factory): + return tmpdir_factory.mktemp('data', numbered=False) + def test_some(session_dir): + session_dir.isdir() + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) \ No newline at end of file diff --git a/testing/test_unittest.py b/testing/test_unittest.py index b9ce7b5fa..aa055f89c 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1,3 +1,4 @@ +from _pytest.main import EXIT_NOTESTSCOLLECTED import pytest def test_simple_unittest(testdir): @@ -41,7 +42,7 @@ def test_isclasscheck_issue53(testdir): E = _E() """) result = testdir.runpytest(testpath) - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED def test_setup(testdir): testpath = testdir.makepyfile(""" @@ -572,7 +573,7 @@ def test_unorderable_types(testdir): """) result = testdir.runpytest() assert "TypeError" not in result.stdout.str() - assert result.ret == 0 + assert result.ret == EXIT_NOTESTSCOLLECTED def test_unittest_typerror_traceback(testdir): testdir.makepyfile(""" diff --git a/tox.ini b/tox.ini index c5dfa03f9..73ecdfb45 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ deps= [testenv:py27-subprocess] changedir=. basepython=python2.7 -deps=pytest-xdist +deps=pytest-xdist>=1.13 mock nose commands= @@ -37,7 +37,7 @@ deps = pytest-flakes>=0.2 commands = py.test --flakes -m flakes _pytest testing [testenv:py27-xdist] -deps=pytest-xdist +deps=pytest-xdist>=1.13 mock nose commands= @@ -63,7 +63,7 @@ commands= py.test -rfsxX test_pdb.py test_terminal.py test_unittest.py [testenv:py27-nobyte] -deps=pytest-xdist +deps=pytest-xdist>=1.13 distribute=true setenv= PYTHONDONTWRITEBYTECODE=1