Merge branch 'master' into features

This commit is contained in:
Ronny Pfannschmidt 2016-01-02 23:56:01 +01:00
commit 6c170201d6
12 changed files with 239 additions and 68 deletions

View File

@ -33,6 +33,7 @@ Endre Galaczi
Elizaveta Shashkova Elizaveta Shashkova
Eric Hunsberger Eric Hunsberger
Eric Siegerman Eric Siegerman
Erik M. Bray
Florian Bruhin Florian Bruhin
Floris Bruynooghe Floris Bruynooghe
Gabriel Reis Gabriel Reis

View File

@ -40,6 +40,20 @@ Bug Fixes
2.8.6.dev1 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 2.8.5
===== =====

View File

@ -1,12 +1,14 @@
====== .. image:: doc/en/img/pytest1.png
pytest :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 .. image:: https://img.shields.io/pypi/v/pytest.svg
:target: https://pypi.python.org/pypi/pytest :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 .. image:: https://img.shields.io/coveralls/pytest-dev/pytest/master.svg
:target: https://coveralls.io/r/pytest-dev/pytest :target: https://coveralls.io/r/pytest-dev/pytest
.. image:: https://travis-ci.org/pytest-dev/pytest.svg?branch=master .. 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 .. image:: https://ci.appveyor.com/api/projects/status/mrgbjaua7t33pg6b?svg=true
:target: https://ci.appveyor.com/project/pytestbot/pytest :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:
.. 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.
Issues: https://github.com/pytest-dev/pytest/issues
Features 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>`_ <http://pytest.org/latest/goodpractises.html#python-test-discovery>`_
of test modules and functions, 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 - `Modular fixtures <http://pytest.org/latest/fixture.html>`_ for
managing small or parametrized long-lived test resources. 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), - Can run `unittest <http://pytest.org/latest/unittest.html>`_ (or trial),
`nose <http://pytest.org/latest/nose.html>`_ `nose <http://pytest.org/latest/nose.html>`_ test suites out of the box;
- single-source compatibility from Python2.6 all the way up to
Python3.5, PyPy-2.3, (jython-2.5 untested) - 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: For full documentation, including installation, tutorials and PDF documents, please see http://pytest.org.
.. 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
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. Licensed under the MIT license.

View File

@ -31,6 +31,7 @@ def pytest_addoption(parser):
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_load_initial_conftests(early_config, parser, args): def pytest_load_initial_conftests(early_config, parser, args):
_readline_workaround()
ns = early_config.known_args_namespace ns = early_config.known_args_namespace
pluginmanager = early_config.pluginmanager pluginmanager = early_config.pluginmanager
capman = CaptureManager(ns.capture) capman = CaptureManager(ns.capture)
@ -442,3 +443,30 @@ class DontReadFromInput:
def close(self): def close(self):
pass 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

View File

@ -282,12 +282,11 @@ def pytest_keyboard_interrupt(excinfo):
""" called for keyboard interrupt. """ """ called for keyboard interrupt. """
def pytest_exception_interact(node, call, report): def pytest_exception_interact(node, call, report):
""" (experimental, new in 2.4) called when """called when an exception was raised which can potentially be
an exception was raised which can potentially be
interactively handled. interactively handled.
This hook is only called if an exception was raised 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): def pytest_enter_pdb(config):

View File

@ -71,7 +71,6 @@ class _NodeReporter(object):
self.testcase = None self.testcase = None
self.attrs = {} self.attrs = {}
def append(self, node): def append(self, node):
self.xml.add_stats(type(node).__name__) self.xml.add_stats(type(node).__name__)
self.nodes.append(node) self.nodes.append(node)
@ -82,7 +81,6 @@ class _NodeReporter(object):
self.property_insert_order.append(name) self.property_insert_order.append(name)
self.properties[name] = bin_xml_escape(value) self.properties[name] = bin_xml_escape(value)
def make_properties_node(self): def make_properties_node(self):
"""Return a Junit node containing custom properties, if any. """Return a Junit node containing custom properties, if any.
""" """
@ -93,7 +91,6 @@ class _NodeReporter(object):
]) ])
return '' return ''
def record_testreport(self, testreport): def record_testreport(self, testreport):
assert not self.testcase assert not self.testcase
names = mangle_testnames(testreport.nodeid.split("::")) names = mangle_testnames(testreport.nodeid.split("::"))
@ -182,7 +179,6 @@ class _NodeReporter(object):
message=skipreason)) message=skipreason))
self._write_captured_output(report) self._write_captured_output(report)
def finalize(self): def finalize(self):
data = self.to_xml().unicode(indent=0) data = self.to_xml().unicode(indent=0)
self.__dict__.clear() self.__dict__.clear()
@ -262,6 +258,14 @@ class LogXML(object):
self.node_reporters = {} # nodeid -> _NodeReporter self.node_reporters = {} # nodeid -> _NodeReporter
self.node_reporters_ordered = [] 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): def node_reporter(self, report):
nodeid = getattr(report, 'nodeid', report) nodeid = getattr(report, 'nodeid', report)
# local hack to handle xdist report order # local hack to handle xdist report order
@ -270,7 +274,7 @@ class LogXML(object):
key = nodeid, slavenode key = nodeid, slavenode
if key in self.node_reporters: if key in self.node_reporters:
#TODO: breasks for --dist=each # TODO: breasks for --dist=each
return self.node_reporters[key] return self.node_reporters[key]
reporter = _NodeReporter(nodeid, self) reporter = _NodeReporter(nodeid, self)
self.node_reporters[key] = reporter self.node_reporters[key] = reporter
@ -324,7 +328,7 @@ class LogXML(object):
reporter.append_skipped(report) reporter.append_skipped(report)
self.update_testcase_duration(report) self.update_testcase_duration(report)
if report.when == "teardown": if report.when == "teardown":
self.node_reporter(report).finalize() self.finalize(report)
def update_testcase_duration(self, report): def update_testcase_duration(self, report):
"""accumulates total duration for nodeid from given report and updates """accumulates total duration for nodeid from given report and updates

View File

@ -1,8 +1,14 @@
""" monkeypatching and mocking functionality. """ """ monkeypatching and mocking functionality. """
import os, sys import os, sys
import re
from py.builtin import _basestring from py.builtin import _basestring
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
def pytest_funcarg__monkeypatch(request): def pytest_funcarg__monkeypatch(request):
"""The returned ``monkeypatch`` funcarg provides these """The returned ``monkeypatch`` funcarg provides these
helper methods to modify objects, dictionaries or os.environ:: helper methods to modify objects, dictionaries or os.environ::
@ -34,14 +40,28 @@ def derive_importpath(import_path, raising):
(import_path,)) (import_path,))
rest = [] rest = []
target = import_path target = import_path
target_parts = set(target.split("."))
while target: while target:
try: try:
obj = __import__(target, None, None, "__doc__") 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: if "." not in target:
__tracebackhide__ = True __tracebackhide__ = True
pytest.fail("could not import any sub part: %s" % pytest.fail("could not import any sub part: %s" %
import_path) 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) target, name = target.rsplit(".", 1)
rest.append(name) rest.append(name)
else: else:
@ -106,7 +126,7 @@ class monkeypatch:
# avoid class descriptors like staticmethod/classmethod # avoid class descriptors like staticmethod/classmethod
if inspect.isclass(target): if inspect.isclass(target):
oldval = target.__dict__.get(name, notset) oldval = target.__dict__.get(name, notset)
self._setattr.insert(0, (target, name, oldval)) self._setattr.append((target, name, oldval))
setattr(target, name, value) setattr(target, name, value)
def delattr(self, target, name=notset, raising=True): def delattr(self, target, name=notset, raising=True):
@ -132,13 +152,12 @@ class monkeypatch:
if raising: if raising:
raise AttributeError(name) raise AttributeError(name)
else: else:
self._setattr.insert(0, (target, name, self._setattr.append((target, name, getattr(target, name, notset)))
getattr(target, name, notset)))
delattr(target, name) delattr(target, name)
def setitem(self, dic, name, value): def setitem(self, dic, name, value):
""" Set dictionary entry ``name`` to 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 dic[name] = value
def delitem(self, dic, name, raising=True): def delitem(self, dic, name, raising=True):
@ -151,7 +170,7 @@ class monkeypatch:
if raising: if raising:
raise KeyError(name) raise KeyError(name)
else: else:
self._setitem.insert(0, (dic, name, dic.get(name, notset))) self._setitem.append((dic, name, dic.get(name, notset)))
del dic[name] del dic[name]
def setenv(self, name, value, prepend=None): def setenv(self, name, value, prepend=None):
@ -203,13 +222,13 @@ class monkeypatch:
calling `undo()` will undo all of the changes made in calling `undo()` will undo all of the changes made in
both functions. both functions.
""" """
for obj, name, value in self._setattr: for obj, name, value in reversed(self._setattr):
if value is not notset: if value is not notset:
setattr(obj, name, value) setattr(obj, name, value)
else: else:
delattr(obj, name) delattr(obj, name)
self._setattr[:] = [] self._setattr[:] = []
for dictionary, name, value in self._setitem: for dictionary, name, value in reversed(self._setitem):
if value is notset: if value is notset:
try: try:
del dictionary[name] del dictionary[name]

View File

@ -333,7 +333,7 @@ The result of this test will be successful::
Parametrizing test methods through per-class configuration 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 Here is an example ``pytest_generate_function`` function implementing a

View File

@ -120,7 +120,7 @@ in a managed class/module/function scope.
.. _`Convention over Configuration`: http://en.wikipedia.org/wiki/Convention_over_Configuration .. _`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 There are two conceptual reasons why yielding from a factory function

View File

@ -5,7 +5,6 @@ Getting started basics
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
index
getting-started getting-started
usage usage
goodpractises goodpractises

View File

@ -18,6 +18,7 @@ def runandparse(testdir, *args):
def assert_attr(node, **kwargs): def assert_attr(node, **kwargs):
__tracebackhide__ = True __tracebackhide__ = True
def nodeval(node, name): def nodeval(node, name):
anode = node.getAttributeNode(name) anode = node.getAttributeNode(name)
if anode is not None: if anode is not None:
@ -667,10 +668,13 @@ def test_runs_twice(testdir):
pass pass
''') ''')
result = testdir.runpytest(f, f, '--junitxml', testdir.tmpdir.join("test.xml")) result, dom = runandparse(testdir, f, f)
assert 'INTERNALERROR' not in str(result.stdout) 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): def test_runs_twice_xdist(testdir):
pytest.importorskip('xdist') pytest.importorskip('xdist')
f = testdir.makepyfile(''' f = testdir.makepyfile('''
@ -678,7 +682,60 @@ def test_runs_twice_xdist(testdir):
pass pass
''') ''')
result = testdir.runpytest(f, result, dom = runandparse(
'--dist', 'each', '--tx', '2*popen', testdir, f,
'--junitxml', testdir.tmpdir.join("test.xml")) '--dist', 'each', '--tx', '2*popen',)
assert 'INTERNALERROR' not in str(result.stdout) 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',
]

View File

@ -1,4 +1,6 @@
import os, sys import os, sys
import textwrap
import pytest import pytest
from _pytest.monkeypatch import monkeypatch as MonkeyPatch from _pytest.monkeypatch import monkeypatch as MonkeyPatch
@ -245,6 +247,21 @@ def test_issue185_time_breaks(testdir):
*1 passed* *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): class SampleNew(object):