Merge branch 'master' into features
This commit is contained in:
commit
6c170201d6
1
AUTHORS
1
AUTHORS
|
@ -33,6 +33,7 @@ Endre Galaczi
|
|||
Elizaveta Shashkova
|
||||
Eric Hunsberger
|
||||
Eric Siegerman
|
||||
Erik M. Bray
|
||||
Florian Bruhin
|
||||
Floris Bruynooghe
|
||||
Gabriel Reis
|
||||
|
|
14
CHANGELOG
14
CHANGELOG
|
@ -40,6 +40,20 @@ Bug Fixes
|
|||
2.8.6.dev1
|
||||
==========
|
||||
|
||||
- fix #1259: allow for double nodeids in junitxml,
|
||||
this was a regression failing plugins combinations
|
||||
like pytest-pep8 + pytest-flakes
|
||||
|
||||
- Workaround for exception that occurs in pyreadline when using
|
||||
``--pdb`` with standard I/O capture enabled.
|
||||
Thanks Erik M. Bray for the PR.
|
||||
|
||||
- fix #900: Better error message in case the target of a ``monkeypatch`` call
|
||||
raises an ``ImportError``.
|
||||
|
||||
- fix #1292: monkeypatch calls (setattr, setenv, etc.) are now O(1).
|
||||
Thanks David R. MacIver for the report and Bruno Oliveira for the PR.
|
||||
|
||||
|
||||
2.8.5
|
||||
=====
|
||||
|
|
117
README.rst
117
README.rst
|
@ -1,12 +1,14 @@
|
|||
======
|
||||
pytest
|
||||
======
|
||||
.. image:: doc/en/img/pytest1.png
|
||||
:target: http://pytest.org
|
||||
:align: center
|
||||
:alt: pytest
|
||||
|
||||
The ``pytest`` testing tool makes it easy to write small tests, yet
|
||||
scales to support complex functional testing.
|
||||
------
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/pytest.svg
|
||||
:target: https://pypi.python.org/pypi/pytest
|
||||
.. image:: https://img.shields.io/pypi/pyversions/pytest.svg
|
||||
:target: https://pypi.python.org/pypi/pytest
|
||||
.. image:: https://img.shields.io/coveralls/pytest-dev/pytest/master.svg
|
||||
:target: https://coveralls.io/r/pytest-dev/pytest
|
||||
.. image:: https://travis-ci.org/pytest-dev/pytest.svg?branch=master
|
||||
|
@ -14,53 +16,84 @@ scales to support complex functional testing.
|
|||
.. image:: https://ci.appveyor.com/api/projects/status/mrgbjaua7t33pg6b?svg=true
|
||||
:target: https://ci.appveyor.com/project/pytestbot/pytest
|
||||
|
||||
Documentation: http://pytest.org/latest/
|
||||
The ``pytest`` framework makes it easy to write small tests, yet
|
||||
scales to support complex functional testing for applications and libraries.
|
||||
|
||||
Changelog: http://pytest.org/latest/changelog.html
|
||||
An example of a simple test:
|
||||
|
||||
Issues: https://github.com/pytest-dev/pytest/issues
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_sample.py
|
||||
def func(x):
|
||||
return x + 1
|
||||
|
||||
def test_answer():
|
||||
assert func(3) == 5
|
||||
|
||||
|
||||
To execute it::
|
||||
|
||||
$ py.test
|
||||
======= test session starts ========
|
||||
platform linux -- Python 3.4.3, pytest-2.8.5, py-1.4.31, pluggy-0.3.1
|
||||
collected 1 items
|
||||
|
||||
test_sample.py F
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_answer ________
|
||||
|
||||
def test_answer():
|
||||
> assert func(3) == 5
|
||||
E assert 4 == 5
|
||||
E + where 4 = func(3)
|
||||
|
||||
test_sample.py:5: AssertionError
|
||||
======= 1 failed in 0.12 seconds ========
|
||||
|
||||
Due to ``py.test``'s detailed assertion introspection, only plain ``assert`` statements are used. See `getting-started <http://pytest.org/latest/getting-started.html#our-first-test-run>`_ for more examples.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `auto-discovery
|
||||
- Detailed info on failing `assert statements <http://pytest.org/latest/assert.html>`_ (no need to remember ``self.assert*`` names);
|
||||
|
||||
- `Auto-discovery
|
||||
<http://pytest.org/latest/goodpractises.html#python-test-discovery>`_
|
||||
of test modules and functions,
|
||||
- detailed info on failing `assert statements <http://pytest.org/latest/assert.html>`_ (no need to remember ``self.assert*`` names)
|
||||
- `modular fixtures <http://pytest.org/latest/fixture.html>`_ for
|
||||
managing small or parametrized long-lived test resources.
|
||||
- multi-paradigm support: you can use ``pytest`` to run test suites based
|
||||
on `unittest <http://pytest.org/latest/unittest.html>`_ (or trial),
|
||||
`nose <http://pytest.org/latest/nose.html>`_
|
||||
- single-source compatibility from Python2.6 all the way up to
|
||||
Python3.5, PyPy-2.3, (jython-2.5 untested)
|
||||
of test modules and functions;
|
||||
|
||||
- `Modular fixtures <http://pytest.org/latest/fixture.html>`_ for
|
||||
managing small or parametrized long-lived test resources;
|
||||
|
||||
- Can run `unittest <http://pytest.org/latest/unittest.html>`_ (or trial),
|
||||
`nose <http://pytest.org/latest/nose.html>`_ test suites out of the box;
|
||||
|
||||
- Python2.6+, Python3.2+, PyPy-2.3, Jython-2.5 (untested);
|
||||
|
||||
- Rich plugin architecture, with over 150+ `external plugins <http://pytest.org/latest/plugins.html#installing-external-plugins-searching>`_ and thriving comminity;
|
||||
|
||||
|
||||
- many `external plugins <http://pytest.org/latest/plugins.html#installing-external-plugins-searching>`_.
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
A simple example for a test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_module.py
|
||||
def test_function():
|
||||
i = 4
|
||||
assert i == 3
|
||||
|
||||
which can be run with ``py.test test_module.py``. See `getting-started <http://pytest.org/latest/getting-started.html#our-first-test-run>`_ for more examples.
|
||||
|
||||
For much more info, including PDF docs, see
|
||||
|
||||
http://pytest.org
|
||||
|
||||
and report bugs at:
|
||||
|
||||
https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
and checkout or fork repo at:
|
||||
|
||||
https://github.com/pytest-dev/pytest
|
||||
For full documentation, including installation, tutorials and PDF documents, please see http://pytest.org.
|
||||
|
||||
|
||||
Copyright Holger Krekel and others, 2004-2015
|
||||
Bugs/Requests
|
||||
-------------
|
||||
|
||||
Please use the `GitHub issue tracker <https://github.com/pytest-dev/pytest/issues>`_ to submit bugs or request features.
|
||||
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
Consult the `Changelog <http://pytest.org/latest/changelog.html>`_ page for fixes and enhancements of each version.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Copyright Holger Krekel and others, 2004-2016.
|
||||
Licensed under the MIT license.
|
||||
|
|
|
@ -31,6 +31,7 @@ def pytest_addoption(parser):
|
|||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_load_initial_conftests(early_config, parser, args):
|
||||
_readline_workaround()
|
||||
ns = early_config.known_args_namespace
|
||||
pluginmanager = early_config.pluginmanager
|
||||
capman = CaptureManager(ns.capture)
|
||||
|
@ -442,3 +443,30 @@ class DontReadFromInput:
|
|||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
def _readline_workaround():
|
||||
"""
|
||||
Ensure readline is imported so that it attaches to the correct stdio
|
||||
handles on Windows.
|
||||
|
||||
Pdb uses readline support where available--when not running from the Python
|
||||
prompt, the readline module is not imported until running the pdb REPL. If
|
||||
running py.test with the --pdb option this means the readline module is not
|
||||
imported until after I/O capture has been started.
|
||||
|
||||
This is a problem for pyreadline, which is often used to implement readline
|
||||
support on Windows, as it does not attach to the correct handles for stdout
|
||||
and/or stdin if they have been redirected by the FDCapture mechanism. This
|
||||
workaround ensures that readline is imported before I/O capture is setup so
|
||||
that it can attach to the actual stdin/out for the console.
|
||||
|
||||
See https://github.com/pytest-dev/pytest/pull/1281
|
||||
"""
|
||||
|
||||
if not sys.platform.startswith('win32'):
|
||||
return
|
||||
try:
|
||||
import readline # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
|
|
@ -282,12 +282,11 @@ def pytest_keyboard_interrupt(excinfo):
|
|||
""" called for keyboard interrupt. """
|
||||
|
||||
def pytest_exception_interact(node, call, report):
|
||||
""" (experimental, new in 2.4) called when
|
||||
an exception was raised which can potentially be
|
||||
"""called when an exception was raised which can potentially be
|
||||
interactively handled.
|
||||
|
||||
This hook is only called if an exception was raised
|
||||
that is not an internal exception like "skip.Exception".
|
||||
that is not an internal exception like ``skip.Exception``.
|
||||
"""
|
||||
|
||||
def pytest_enter_pdb(config):
|
||||
|
|
|
@ -71,7 +71,6 @@ class _NodeReporter(object):
|
|||
self.testcase = None
|
||||
self.attrs = {}
|
||||
|
||||
|
||||
def append(self, node):
|
||||
self.xml.add_stats(type(node).__name__)
|
||||
self.nodes.append(node)
|
||||
|
@ -82,7 +81,6 @@ class _NodeReporter(object):
|
|||
self.property_insert_order.append(name)
|
||||
self.properties[name] = bin_xml_escape(value)
|
||||
|
||||
|
||||
def make_properties_node(self):
|
||||
"""Return a Junit node containing custom properties, if any.
|
||||
"""
|
||||
|
@ -93,7 +91,6 @@ class _NodeReporter(object):
|
|||
])
|
||||
return ''
|
||||
|
||||
|
||||
def record_testreport(self, testreport):
|
||||
assert not self.testcase
|
||||
names = mangle_testnames(testreport.nodeid.split("::"))
|
||||
|
@ -182,7 +179,6 @@ class _NodeReporter(object):
|
|||
message=skipreason))
|
||||
self._write_captured_output(report)
|
||||
|
||||
|
||||
def finalize(self):
|
||||
data = self.to_xml().unicode(indent=0)
|
||||
self.__dict__.clear()
|
||||
|
@ -262,6 +258,14 @@ class LogXML(object):
|
|||
self.node_reporters = {} # nodeid -> _NodeReporter
|
||||
self.node_reporters_ordered = []
|
||||
|
||||
def finalize(self, report):
|
||||
nodeid = getattr(report, 'nodeid', report)
|
||||
# local hack to handle xdist report order
|
||||
slavenode = getattr(report, 'node', None)
|
||||
reporter = self.node_reporters.pop((nodeid, slavenode))
|
||||
if reporter is not None:
|
||||
reporter.finalize()
|
||||
|
||||
def node_reporter(self, report):
|
||||
nodeid = getattr(report, 'nodeid', report)
|
||||
# local hack to handle xdist report order
|
||||
|
@ -270,7 +274,7 @@ class LogXML(object):
|
|||
key = nodeid, slavenode
|
||||
|
||||
if key in self.node_reporters:
|
||||
#TODO: breasks for --dist=each
|
||||
# TODO: breasks for --dist=each
|
||||
return self.node_reporters[key]
|
||||
reporter = _NodeReporter(nodeid, self)
|
||||
self.node_reporters[key] = reporter
|
||||
|
@ -324,7 +328,7 @@ class LogXML(object):
|
|||
reporter.append_skipped(report)
|
||||
self.update_testcase_duration(report)
|
||||
if report.when == "teardown":
|
||||
self.node_reporter(report).finalize()
|
||||
self.finalize(report)
|
||||
|
||||
def update_testcase_duration(self, report):
|
||||
"""accumulates total duration for nodeid from given report and updates
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
""" monkeypatching and mocking functionality. """
|
||||
|
||||
import os, sys
|
||||
import re
|
||||
|
||||
from py.builtin import _basestring
|
||||
|
||||
|
||||
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
|
||||
|
||||
|
||||
def pytest_funcarg__monkeypatch(request):
|
||||
"""The returned ``monkeypatch`` funcarg provides these
|
||||
helper methods to modify objects, dictionaries or os.environ::
|
||||
|
@ -34,14 +40,28 @@ def derive_importpath(import_path, raising):
|
|||
(import_path,))
|
||||
rest = []
|
||||
target = import_path
|
||||
target_parts = set(target.split("."))
|
||||
while target:
|
||||
try:
|
||||
obj = __import__(target, None, None, "__doc__")
|
||||
except ImportError:
|
||||
except ImportError as ex:
|
||||
if hasattr(ex, 'name'):
|
||||
# Python >= 3.3
|
||||
failed_name = ex.name
|
||||
else:
|
||||
match = RE_IMPORT_ERROR_NAME.match(ex.args[0])
|
||||
assert match
|
||||
failed_name = match.group(1)
|
||||
|
||||
if "." not in target:
|
||||
__tracebackhide__ = True
|
||||
pytest.fail("could not import any sub part: %s" %
|
||||
import_path)
|
||||
elif failed_name != target \
|
||||
and not any(p == failed_name for p in target_parts):
|
||||
# target is importable but causes ImportError itself
|
||||
__tracebackhide__ = True
|
||||
pytest.fail("import error in %s: %s" % (target, ex.args[0]))
|
||||
target, name = target.rsplit(".", 1)
|
||||
rest.append(name)
|
||||
else:
|
||||
|
@ -106,7 +126,7 @@ class monkeypatch:
|
|||
# avoid class descriptors like staticmethod/classmethod
|
||||
if inspect.isclass(target):
|
||||
oldval = target.__dict__.get(name, notset)
|
||||
self._setattr.insert(0, (target, name, oldval))
|
||||
self._setattr.append((target, name, oldval))
|
||||
setattr(target, name, value)
|
||||
|
||||
def delattr(self, target, name=notset, raising=True):
|
||||
|
@ -132,13 +152,12 @@ class monkeypatch:
|
|||
if raising:
|
||||
raise AttributeError(name)
|
||||
else:
|
||||
self._setattr.insert(0, (target, name,
|
||||
getattr(target, name, notset)))
|
||||
self._setattr.append((target, name, getattr(target, name, notset)))
|
||||
delattr(target, name)
|
||||
|
||||
def setitem(self, dic, name, value):
|
||||
""" Set dictionary entry ``name`` to value. """
|
||||
self._setitem.insert(0, (dic, name, dic.get(name, notset)))
|
||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||
dic[name] = value
|
||||
|
||||
def delitem(self, dic, name, raising=True):
|
||||
|
@ -151,7 +170,7 @@ class monkeypatch:
|
|||
if raising:
|
||||
raise KeyError(name)
|
||||
else:
|
||||
self._setitem.insert(0, (dic, name, dic.get(name, notset)))
|
||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||
del dic[name]
|
||||
|
||||
def setenv(self, name, value, prepend=None):
|
||||
|
@ -203,13 +222,13 @@ class monkeypatch:
|
|||
calling `undo()` will undo all of the changes made in
|
||||
both functions.
|
||||
"""
|
||||
for obj, name, value in self._setattr:
|
||||
for obj, name, value in reversed(self._setattr):
|
||||
if value is not notset:
|
||||
setattr(obj, name, value)
|
||||
else:
|
||||
delattr(obj, name)
|
||||
self._setattr[:] = []
|
||||
for dictionary, name, value in self._setitem:
|
||||
for dictionary, name, value in reversed(self._setitem):
|
||||
if value is notset:
|
||||
try:
|
||||
del dictionary[name]
|
||||
|
|
|
@ -333,7 +333,7 @@ The result of this test will be successful::
|
|||
Parametrizing test methods through per-class configuration
|
||||
--------------------------------------------------------------
|
||||
|
||||
.. _`unittest parametrizer`: http://code.google.com/p/unittest-ext/source/browse/trunk/params.py
|
||||
.. _`unittest parametrizer`: https://github.com/testing-cabal/unittest-ext/blob/master/params.py
|
||||
|
||||
|
||||
Here is an example ``pytest_generate_function`` function implementing a
|
||||
|
|
|
@ -120,7 +120,7 @@ in a managed class/module/function scope.
|
|||
|
||||
.. _`Convention over Configuration`: http://en.wikipedia.org/wiki/Convention_over_Configuration
|
||||
|
||||
Can I yield multiple values from a fixture function function?
|
||||
Can I yield multiple values from a fixture function?
|
||||
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
There are two conceptual reasons why yielding from a factory function
|
||||
|
|
|
@ -5,7 +5,6 @@ Getting started basics
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
index
|
||||
getting-started
|
||||
usage
|
||||
goodpractises
|
||||
|
|
|
@ -18,6 +18,7 @@ def runandparse(testdir, *args):
|
|||
|
||||
def assert_attr(node, **kwargs):
|
||||
__tracebackhide__ = True
|
||||
|
||||
def nodeval(node, name):
|
||||
anode = node.getAttributeNode(name)
|
||||
if anode is not None:
|
||||
|
@ -667,10 +668,13 @@ def test_runs_twice(testdir):
|
|||
pass
|
||||
''')
|
||||
|
||||
result = testdir.runpytest(f, f, '--junitxml', testdir.tmpdir.join("test.xml"))
|
||||
assert 'INTERNALERROR' not in str(result.stdout)
|
||||
result, dom = runandparse(testdir, f, f)
|
||||
assert 'INTERNALERROR' not in result.stdout.str()
|
||||
first, second = [x['classname'] for x in dom.find_by_tag("testcase")]
|
||||
assert first == second
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='hangs', run=False)
|
||||
def test_runs_twice_xdist(testdir):
|
||||
pytest.importorskip('xdist')
|
||||
f = testdir.makepyfile('''
|
||||
|
@ -678,7 +682,60 @@ def test_runs_twice_xdist(testdir):
|
|||
pass
|
||||
''')
|
||||
|
||||
result = testdir.runpytest(f,
|
||||
'--dist', 'each', '--tx', '2*popen',
|
||||
'--junitxml', testdir.tmpdir.join("test.xml"))
|
||||
assert 'INTERNALERROR' not in str(result.stdout)
|
||||
result, dom = runandparse(
|
||||
testdir, f,
|
||||
'--dist', 'each', '--tx', '2*popen',)
|
||||
assert 'INTERNALERROR' not in result.stdout.str()
|
||||
first, second = [x['classname'] for x in dom.find_by_tag("testcase")]
|
||||
assert first == second
|
||||
|
||||
|
||||
def test_fancy_items_regression(testdir):
|
||||
# issue 1259
|
||||
testdir.makeconftest("""
|
||||
import pytest
|
||||
class FunItem(pytest.Item):
|
||||
def runtest(self):
|
||||
pass
|
||||
class NoFunItem(pytest.Item):
|
||||
def runtest(self):
|
||||
pass
|
||||
|
||||
class FunCollector(pytest.File):
|
||||
def collect(self):
|
||||
return [
|
||||
FunItem('a', self),
|
||||
NoFunItem('a', self),
|
||||
NoFunItem('b', self),
|
||||
]
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
if path.check(ext='.py'):
|
||||
return FunCollector(path, parent)
|
||||
""")
|
||||
|
||||
testdir.makepyfile('''
|
||||
def test_pass():
|
||||
pass
|
||||
''')
|
||||
|
||||
result, dom = runandparse(testdir)
|
||||
|
||||
assert 'INTERNALERROR' not in result.stdout.str()
|
||||
|
||||
items = sorted(
|
||||
'%(classname)s %(name)s %(file)s' % x
|
||||
|
||||
for x in dom.find_by_tag("testcase"))
|
||||
import pprint
|
||||
pprint.pprint(items)
|
||||
assert items == [
|
||||
u'conftest a conftest.py',
|
||||
u'conftest a conftest.py',
|
||||
u'conftest b conftest.py',
|
||||
u'test_fancy_items_regression a test_fancy_items_regression.py',
|
||||
u'test_fancy_items_regression a test_fancy_items_regression.py',
|
||||
u'test_fancy_items_regression b test_fancy_items_regression.py',
|
||||
u'test_fancy_items_regression test_pass'
|
||||
u' test_fancy_items_regression.py',
|
||||
]
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import os, sys
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
from _pytest.monkeypatch import monkeypatch as MonkeyPatch
|
||||
|
||||
|
@ -245,6 +247,21 @@ def test_issue185_time_breaks(testdir):
|
|||
*1 passed*
|
||||
""")
|
||||
|
||||
def test_importerror(testdir):
|
||||
p = testdir.mkpydir("package")
|
||||
p.join("a.py").write(textwrap.dedent("""\
|
||||
import doesnotexist
|
||||
|
||||
x = 1
|
||||
"""))
|
||||
testdir.tmpdir.join("test_importerror.py").write(textwrap.dedent("""\
|
||||
def test_importerror(monkeypatch):
|
||||
monkeypatch.setattr('package.a.x', 2)
|
||||
"""))
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines("""
|
||||
*import error in package.a.x: No module named {0}doesnotexist{0}*
|
||||
""".format("'" if sys.version_info > (3, 0) else ""))
|
||||
|
||||
|
||||
class SampleNew(object):
|
||||
|
|
Loading…
Reference in New Issue