diff --git a/AUTHORS b/AUTHORS index 17477d59c..700602635 100644 --- a/AUTHORS +++ b/AUTHORS @@ -33,6 +33,7 @@ Endre Galaczi Elizaveta Shashkova Eric Hunsberger Eric Siegerman +Erik M. Bray Florian Bruhin Floris Bruynooghe Gabriel Reis diff --git a/CHANGELOG b/CHANGELOG index 9c41d6a6e..044753664 100644 --- a/CHANGELOG +++ b/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 ===== diff --git a/README.rst b/README.rst index 0e326c72b..34fad1660 100644 --- a/README.rst +++ b/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 `_ for more examples. + Features -------- -- `auto-discovery +- Detailed info on failing `assert statements `_ (no need to remember ``self.assert*`` names); + +- `Auto-discovery `_ - of test modules and functions, -- detailed info on failing `assert statements `_ (no need to remember ``self.assert*`` names) -- `modular fixtures `_ for - managing small or parametrized long-lived test resources. -- multi-paradigm support: you can use ``pytest`` to run test suites based - on `unittest `_ (or trial), - `nose `_ -- 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 `_ for + managing small or parametrized long-lived test resources; + +- Can run `unittest `_ (or trial), + `nose `_ 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 `_ and thriving comminity; -- many `external plugins `_. +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 `_ 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 `_ to submit bugs or request features. + + +Changelog +--------- + +Consult the `Changelog `_ page for fixes and enhancements of each version. + + +License +------- + +Copyright Holger Krekel and others, 2004-2016. Licensed under the MIT license. diff --git a/_pytest/capture.py b/_pytest/capture.py index f0fa72f46..3895a714a 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -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 diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index a9024b1d3..224b7971c 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -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): diff --git a/_pytest/junitxml.py b/_pytest/junitxml.py index 1c114ddcb..995694687 100644 --- a/_pytest/junitxml.py +++ b/_pytest/junitxml.py @@ -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 diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index 0df224cd6..5f9720f1f 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -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] diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 0a9afe58e..79d38ee2f 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -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 diff --git a/doc/en/faq.rst b/doc/en/faq.rst index 3c019b5e1..fd7ca35e9 100644 --- a/doc/en/faq.rst +++ b/doc/en/faq.rst @@ -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 diff --git a/doc/en/overview.rst b/doc/en/overview.rst index b0003effb..9d8390f52 100644 --- a/doc/en/overview.rst +++ b/doc/en/overview.rst @@ -5,7 +5,6 @@ Getting started basics .. toctree:: :maxdepth: 2 - index getting-started usage goodpractises diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 81a8f4e6a..0062ab135 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -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) \ No newline at end of file + 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', + ] diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 690aee556..49db0bada 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.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):