From 0f918b1a9dd14a2d046d19be6ec239e8e2df4cb2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 6 Oct 2018 20:30:18 -0300 Subject: [PATCH 01/29] xunit-style functions and methods are invoked by autouse fixtures Fix #3094, Fix #517 --- changelog/3094.feature.rst | 5 + doc/en/xunit_setup.rst | 8 ++ src/_pytest/python.py | 188 ++++++++++++++++++++++++++++--------- src/_pytest/unittest.py | 64 +++++++++---- testing/python/collect.py | 3 - testing/python/fixture.py | 39 ++++++++ 6 files changed, 239 insertions(+), 68 deletions(-) create mode 100644 changelog/3094.feature.rst diff --git a/changelog/3094.feature.rst b/changelog/3094.feature.rst new file mode 100644 index 000000000..9004b6c0a --- /dev/null +++ b/changelog/3094.feature.rst @@ -0,0 +1,5 @@ +`Class xunit-style `__ functions and methods +now obey the scope of *autouse* fixtures. + +This fixes a number of surprising issues like ``setup_method`` being called before session-scoped +autouse fixtures (see `#517 `__ for an example). diff --git a/doc/en/xunit_setup.rst b/doc/en/xunit_setup.rst index 7a6c099f5..466873302 100644 --- a/doc/en/xunit_setup.rst +++ b/doc/en/xunit_setup.rst @@ -93,7 +93,15 @@ Remarks: * It is possible for setup/teardown pairs to be invoked multiple times per testing process. + * teardown functions are not called if the corresponding setup function existed and failed/was skipped. +* Prior to pytest-4.2, xunit-style functions did not obey the scope rules of fixtures, so + it was possible, for example, for a ``setup_method`` to be called before a + session-scoped autouse fixture. + + Now the xunit-style functions are integrated with the fixture mechanism and obey the proper + scope rules of fixtures involved in the call. + .. _`unittest.py module`: http://docs.python.org/library/unittest.html diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 48a50178f..62e622ef2 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -9,6 +9,7 @@ import inspect import os import sys import warnings +from functools import partial from textwrap import dedent import py @@ -435,9 +436,66 @@ class Module(nodes.File, PyCollector): return self._importtestmodule() def collect(self): + self._inject_setup_module_fixture() + self._inject_setup_function_fixture() self.session._fixturemanager.parsefactories(self) return super(Module, self).collect() + def _inject_setup_module_fixture(self): + """Injects a hidden autouse, module scoped fixture into the collected module object + that invokes setUpModule/tearDownModule if either or both are available. + + Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with + other fixtures (#517). + """ + setup_module = _get_non_fixture_func(self.obj, "setUpModule") + if setup_module is None: + setup_module = _get_non_fixture_func(self.obj, "setup_module") + + teardown_module = _get_non_fixture_func(self.obj, "tearDownModule") + if teardown_module is None: + teardown_module = _get_non_fixture_func(self.obj, "teardown_module") + + if setup_module is None and teardown_module is None: + return + + @fixtures.fixture(autouse=True, scope="module") + def xunit_setup_module_fixture(request): + if setup_module is not None: + _call_with_optional_argument(setup_module, request.module) + yield + if teardown_module is not None: + _call_with_optional_argument(teardown_module, request.module) + + self.obj.__pytest_setup_module = xunit_setup_module_fixture + + def _inject_setup_function_fixture(self): + """Injects a hidden autouse, function scoped fixture into the collected module object + that invokes setup_function/teardown_function if either or both are available. + + Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with + other fixtures (#517). + """ + setup_function = _get_non_fixture_func(self.obj, "setup_function") + teardown_function = _get_non_fixture_func(self.obj, "teardown_function") + if setup_function is None and teardown_function is None: + return + + @fixtures.fixture(autouse=True, scope="function") + def xunit_setup_function_fixture(request): + if request.instance is not None: + # in this case we are bound to an instance, so we need to let + # setup_method handle this + yield + return + if setup_function is not None: + _call_with_optional_argument(setup_function, request.function) + yield + if teardown_function is not None: + _call_with_optional_argument(teardown_function, request.function) + + self.obj.__pytest_setup_function = xunit_setup_function_fixture + def _importtestmodule(self): # we assume we are only called once per module importmode = self.config.getoption("--import-mode") @@ -488,19 +546,6 @@ class Module(nodes.File, PyCollector): self.config.pluginmanager.consider_module(mod) return mod - def setup(self): - setup_module = _get_xunit_setup_teardown(self.obj, "setUpModule") - if setup_module is None: - setup_module = _get_xunit_setup_teardown(self.obj, "setup_module") - if setup_module is not None: - setup_module() - - teardown_module = _get_xunit_setup_teardown(self.obj, "tearDownModule") - if teardown_module is None: - teardown_module = _get_xunit_setup_teardown(self.obj, "teardown_module") - if teardown_module is not None: - self.addfinalizer(teardown_module) - class Package(Module): def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): @@ -513,6 +558,22 @@ class Package(Module): self._norecursepatterns = session._norecursepatterns self.fspath = fspath + def setup(self): + # not using fixtures to call setup_module here because autouse fixtures + # from packages are not called automatically (#4085) + setup_module = _get_non_fixture_func(self.obj, "setUpModule") + if setup_module is None: + setup_module = _get_non_fixture_func(self.obj, "setup_module") + if setup_module is not None: + _call_with_optional_argument(setup_module, self.obj) + + teardown_module = _get_non_fixture_func(self.obj, "tearDownModule") + if teardown_module is None: + teardown_module = _get_non_fixture_func(self.obj, "teardown_module") + if teardown_module is not None: + func = partial(_call_with_optional_argument, teardown_module, self.obj) + self.addfinalizer(func) + def _recurse(self, dirpath): if dirpath.basename == "__pycache__": return False @@ -599,8 +660,9 @@ def _get_xunit_setup_teardown(holder, attr_name, param_obj=None): when the callable is called without arguments, defaults to the ``holder`` object. Return ``None`` if a suitable callable is not found. """ + # TODO: only needed because of Package! param_obj = param_obj if param_obj is not None else holder - result = _get_xunit_func(holder, attr_name) + result = _get_non_fixture_func(holder, attr_name) if result is not None: arg_count = result.__code__.co_argcount if inspect.ismethod(result): @@ -611,7 +673,19 @@ def _get_xunit_setup_teardown(holder, attr_name, param_obj=None): return result -def _get_xunit_func(obj, name): +def _call_with_optional_argument(func, arg): + """Call the given function with the given argument if func accepts one argument, otherwise + calls func without arguments""" + arg_count = func.__code__.co_argcount + if inspect.ismethod(func): + arg_count -= 1 + if arg_count: + func(arg) + else: + func() + + +def _get_non_fixture_func(obj, name): """Return the attribute from the given object to be used as a setup/teardown xunit-style function, but only if not marked as a fixture to avoid calling it twice. @@ -643,18 +717,60 @@ class Class(PyCollector): ) ) return [] + + self._inject_setup_class_fixture() + self._inject_setup_method_fixture() + return [Instance(name="()", parent=self)] - def setup(self): - setup_class = _get_xunit_func(self.obj, "setup_class") - if setup_class is not None: - setup_class = getimfunc(setup_class) - setup_class(self.obj) + def _inject_setup_class_fixture(self): + """Injects a hidden autouse, class scoped fixture into the collected class object + that invokes setup_class/teardown_class if either or both are available. - fin_class = getattr(self.obj, "teardown_class", None) - if fin_class is not None: - fin_class = getimfunc(fin_class) - self.addfinalizer(lambda: fin_class(self.obj)) + Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with + other fixtures (#517). + """ + setup_class = _get_non_fixture_func(self.obj, "setup_class") + teardown_class = getattr(self.obj, "teardown_class", None) + if setup_class is None and teardown_class is None: + return + + @fixtures.fixture(autouse=True, scope="class") + def xunit_setup_class_fixture(cls): + if setup_class is not None: + func = getimfunc(setup_class) + _call_with_optional_argument(func, self.obj) + yield + if teardown_class is not None: + func = getimfunc(teardown_class) + _call_with_optional_argument(func, self.obj) + + self.obj.__pytest_setup_class = xunit_setup_class_fixture + + def _inject_setup_method_fixture(self): + """Injects a hidden autouse, function scoped fixture into the collected class object + that invokes setup_method/teardown_method if either or both are available. + + Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with + other fixtures (#517). + """ + setup_method = _get_non_fixture_func(self.obj, "setup_method") + teardown_method = getattr(self.obj, "teardown_method", None) + if setup_method is None and teardown_method is None: + return + + @fixtures.fixture(autouse=True, scope="function") + def xunit_setup_method_fixture(self, request): + method = request.function + if setup_method is not None: + func = getattr(self, "setup_method") + _call_with_optional_argument(func, method) + yield + if teardown_method is not None: + func = getattr(self, "teardown_method") + _call_with_optional_argument(func, method) + + self.obj.__pytest_setup_method = xunit_setup_method_fixture class Instance(PyCollector): @@ -681,29 +797,9 @@ class FunctionMixin(PyobjMixin): def setup(self): """ perform setup for this test function. """ - if hasattr(self, "_preservedparent"): - obj = self._preservedparent - elif isinstance(self.parent, Instance): - obj = self.parent.newinstance() + if isinstance(self.parent, Instance): + self.parent.newinstance() self.obj = self._getobj() - else: - obj = self.parent.obj - if inspect.ismethod(self.obj): - setup_name = "setup_method" - teardown_name = "teardown_method" - else: - setup_name = "setup_function" - teardown_name = "teardown_function" - setup_func_or_method = _get_xunit_setup_teardown( - obj, setup_name, param_obj=self.obj - ) - if setup_func_or_method is not None: - setup_func_or_method() - teardown_func_or_method = _get_xunit_setup_teardown( - obj, teardown_name, param_obj=self.obj - ) - if teardown_func_or_method is not None: - self.addfinalizer(teardown_func_or_method) def _prunetraceback(self, excinfo): if hasattr(self, "_obj") and not self.config.option.fulltrace: diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 4a886c2e1..e00636d46 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -7,6 +7,7 @@ import sys import traceback import _pytest._code +import pytest from _pytest.compat import getimfunc from _pytest.config import hookimpl from _pytest.outcomes import fail @@ -32,24 +33,18 @@ class UnitTestCase(Class): # to declare that our children do not support funcargs nofuncargs = True - def setup(self): - cls = self.obj - if getattr(cls, "__unittest_skip__", False): - return # skipped - setup = getattr(cls, "setUpClass", None) - if setup is not None: - setup() - teardown = getattr(cls, "tearDownClass", None) - if teardown is not None: - self.addfinalizer(teardown) - super(UnitTestCase, self).setup() - def collect(self): from unittest import TestLoader cls = self.obj if not getattr(cls, "__test__", True): return + + skipped = getattr(cls, "__unittest_skip__", False) + if not skipped: + self._inject_setup_teardown_fixtures(cls) + self._inject_setup_class_fixture() + self.session._fixturemanager.parsefactories(self, unittest=True) loader = TestLoader() foundsomething = False @@ -68,6 +63,44 @@ class UnitTestCase(Class): if ut is None or runtest != ut.TestCase.runTest: yield TestCaseFunction("runTest", parent=self) + def _inject_setup_teardown_fixtures(self, cls): + """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding + teardown functions (#517)""" + class_fixture = _make_xunit_fixture( + cls, "setUpClass", "tearDownClass", scope="class", pass_self=False + ) + if class_fixture: + cls.__pytest_class_setup = class_fixture + + method_fixture = _make_xunit_fixture( + cls, "setup_method", "teardown_method", scope="function", pass_self=True + ) + if method_fixture: + cls.__pytest_method_setup = method_fixture + + +def _make_xunit_fixture(obj, setup_name, teardown_name, scope, pass_self): + setup = getattr(obj, setup_name, None) + teardown = getattr(obj, teardown_name, None) + if setup is None and teardown is None: + return None + + @pytest.fixture(scope=scope, autouse=True) + def fixture(self, request): + if setup is not None: + if pass_self: + setup(self, request.function) + else: + setup() + yield + if teardown is not None: + if pass_self: + teardown(self, request.function) + else: + teardown() + + return fixture + class TestCaseFunction(Function): nofuncargs = True @@ -77,9 +110,6 @@ class TestCaseFunction(Function): def setup(self): self._testcase = self.parent.obj(self.name) self._fix_unittest_skip_decorator() - self._obj = getattr(self._testcase, self.name) - if hasattr(self._testcase, "setup_method"): - self._testcase.setup_method(self._obj) if hasattr(self, "_request"): self._request._fillfixtures() @@ -97,11 +127,7 @@ class TestCaseFunction(Function): setattr(self._testcase, "__name__", self.name) def teardown(self): - if hasattr(self._testcase, "teardown_method"): - self._testcase.teardown_method(self._obj) - # Allow garbage collection on TestCase instance attributes. self._testcase = None - self._obj = None def startTest(self, testcase): pass diff --git a/testing/python/collect.py b/testing/python/collect.py index 3147ee9e2..b9954c3f0 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -240,9 +240,6 @@ class TestClass(object): assert result.ret == EXIT_NOTESTSCOLLECTED -@pytest.mark.filterwarnings( - "ignore:usage of Generator.Function is deprecated, please use pytest.Function instead" -) class TestFunction(object): def test_getmodulecollector(self, testdir): item = testdir.getitem("def test_func(): pass") diff --git a/testing/python/fixture.py b/testing/python/fixture.py index b6692ac9b..196b28c51 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1226,6 +1226,45 @@ class TestFixtureUsages(object): values = reprec.getcalls("pytest_runtest_call")[0].item.module.values assert values == [1, 2, 10, 20] + def test_setup_functions_as_fixtures(self, testdir): + """Ensure setup_* methods obey fixture scope rules (#517, #3094).""" + testdir.makepyfile( + """ + import pytest + + DB_INITIALIZED = None + + @pytest.yield_fixture(scope="session", autouse=True) + def db(): + global DB_INITIALIZED + DB_INITIALIZED = True + yield + DB_INITIALIZED = False + + def setup_module(): + assert DB_INITIALIZED + + def teardown_module(): + assert DB_INITIALIZED + + class TestClass(object): + + def setup_method(self, method): + assert DB_INITIALIZED + + def teardown_method(self, method): + assert DB_INITIALIZED + + def test_printer_1(self): + pass + + def test_printer_2(self): + pass + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["* 2 passed in *"]) + class TestFixtureManagerParseFactories(object): @pytest.fixture From 16546b73420ed69401e19495e9f6dafe11a06968 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Jan 2019 20:46:57 -0800 Subject: [PATCH 02/29] Remove some dead code - I wrote a thing: https://github.com/asottile/dead - wanted to try it out, there's lots of false positives and I didn't look through all the things it pointed out but here's some --- src/_pytest/_code/code.py | 5 ----- src/_pytest/assertion/rewrite.py | 13 ------------- src/_pytest/fixtures.py | 6 ------ src/_pytest/junitxml.py | 17 +++++++---------- src/_pytest/python.py | 5 ----- src/_pytest/reports.py | 10 ---------- src/_pytest/resultlog.py | 24 ------------------------ src/_pytest/terminal.py | 9 --------- testing/python/metafunc.py | 1 - testing/test_resultlog.py | 29 ----------------------------- testing/test_runner.py | 7 +------ testing/test_session.py | 1 - testing/test_terminal.py | 11 ----------- 13 files changed, 8 insertions(+), 130 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 1b49fe75b..4473547d6 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -23,8 +23,6 @@ from _pytest.compat import _PY3 from _pytest.compat import PY35 from _pytest.compat import safe_str -builtin_repr = repr - if _PY3: from traceback import format_exception_only else: @@ -947,8 +945,6 @@ class ReprEntryNative(TerminalRepr): class ReprEntry(TerminalRepr): - localssep = "_ " - def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style): self.lines = lines self.reprfuncargs = reprfuncargs @@ -970,7 +966,6 @@ class ReprEntry(TerminalRepr): red = line.startswith("E ") tw.line(line, bold=True, red=red) if self.reprlocals: - # tw.sep(self.localssep, "Locals") tw.line("") self.reprlocals.toterminal(tw) if self.reprfileloc: diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 1d2c27ed1..eae63acee 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -51,19 +51,6 @@ else: return ast.Call(a, b, c, None, None) -def ast_Call_helper(func_name, *args, **kwargs): - """ - func_name: str - args: Iterable[ast.expr] - kwargs: Dict[str,ast.expr] - """ - return ast.Call( - ast.Name(func_name, ast.Load()), - list(args), - [ast.keyword(key, val) for key, val in kwargs.items()], - ) - - class AssertionRewritingHook(object): """PEP302 Import hook which rewrites asserts.""" diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 0a1f258e5..5f288787e 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -660,12 +660,6 @@ class SubRequest(FixtureRequest): self._fixturedef.addfinalizer(finalizer) -class ScopeMismatchError(Exception): - """ A fixture function tries to use a different fixture function which - which has a lower scope (e.g. a Session one calls a function one) - """ - - scopes = "session package module class function".split() scopenum_function = scopes.index("function") diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 1a06ea6d1..a426f537b 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -19,6 +19,7 @@ import sys import time import py +import six import pytest from _pytest import nodes @@ -27,10 +28,6 @@ from _pytest.config import filename_arg # Python 2.X and 3.X compatibility if sys.version_info[0] < 3: from codecs import open -else: - unichr = chr - unicode = str - long = int class Junit(py.xml.Namespace): @@ -45,12 +42,12 @@ class Junit(py.xml.Namespace): _legal_chars = (0x09, 0x0A, 0x0D) _legal_ranges = ((0x20, 0x7E), (0x80, 0xD7FF), (0xE000, 0xFFFD), (0x10000, 0x10FFFF)) _legal_xml_re = [ - unicode("%s-%s") % (unichr(low), unichr(high)) + u"%s-%s" % (six.unichr(low), six.unichr(high)) for (low, high) in _legal_ranges if low < sys.maxunicode ] -_legal_xml_re = [unichr(x) for x in _legal_chars] + _legal_xml_re -illegal_xml_re = re.compile(unicode("[^%s]") % unicode("").join(_legal_xml_re)) +_legal_xml_re = [six.unichr(x) for x in _legal_chars] + _legal_xml_re +illegal_xml_re = re.compile(u"[^%s]" % u"".join(_legal_xml_re)) del _legal_chars del _legal_ranges del _legal_xml_re @@ -62,9 +59,9 @@ def bin_xml_escape(arg): def repl(matchobj): i = ord(matchobj.group()) if i <= 0xFF: - return unicode("#x%02X") % i + return u"#x%02X" % i else: - return unicode("#x%04X") % i + return u"#x%04X" % i return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg))) @@ -194,7 +191,7 @@ class _NodeReporter(object): else: if hasattr(report.longrepr, "reprcrash"): message = report.longrepr.reprcrash.message - elif isinstance(report.longrepr, (unicode, str)): + elif isinstance(report.longrepr, six.string_types): message = report.longrepr else: message = str(report.longrepr) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 48a50178f..1da422b5d 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -283,9 +283,6 @@ class PyobjMixin(PyobjContext): s = ".".join(parts) return s.replace(".[", "[") - def _getfslineno(self): - return getfslineno(self.obj) - def reportinfo(self): # XXX caching? obj = self.obj @@ -1252,7 +1249,6 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): Python test function. """ - _genid = None # disable since functions handle it themselves _ALLOW_MARKERS = False @@ -1327,7 +1323,6 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): if hasattr(self, "callspec"): callspec = self.callspec assert not callspec.funcargs - self._genid = callspec.id if hasattr(callspec, "param"): self.param = callspec.param self._request = fixtures.FixtureRequest(self) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index b2010cc2e..6ea5798cf 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -158,16 +158,6 @@ class TestReport(BaseReport): ) -class TeardownErrorReport(BaseReport): - outcome = "failed" - when = "teardown" - - def __init__(self, longrepr, **extra): - self.longrepr = longrepr - self.sections = [] - self.__dict__.update(extra) - - class CollectReport(BaseReport): def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra): self.nodeid = nodeid diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index bdf8130fd..a2dd73094 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -47,30 +47,6 @@ def pytest_unconfigure(config): config.pluginmanager.unregister(resultlog) -def generic_path(item): - chain = item.listchain() - gpath = [chain[0].name] - fspath = chain[0].fspath - fspart = False - for node in chain[1:]: - newfspath = node.fspath - if newfspath == fspath: - if fspart: - gpath.append(":") - fspart = False - else: - gpath.append(".") - else: - gpath.append("/") - fspart = True - name = node.name - if name[0] in "([": - gpath.pop() - gpath.append(name) - fspath = newfspath - return "".join(gpath) - - class ResultLog(object): def __init__(self, config, logfile): self.config = config diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index bea02306b..c1d4d1f91 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -841,15 +841,6 @@ class TerminalReporter(object): self.write_line(msg, **markup) -def repr_pythonversion(v=None): - if v is None: - v = sys.version_info - try: - return "%s.%s.%s-%s-%s" % v - except (TypeError, ValueError): - return str(v) - - def build_summary_stats_line(stats): keys = ("failed passed skipped deselected xfailed xpassed warnings error").split() unknown_key_seen = False diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 54a6ecb91..5c352efd1 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -784,7 +784,6 @@ class TestMetafuncFunctional(object): @pytest.fixture def metafunc(request): - assert request._pyfuncitem._genid == "0" return request.param def test_function(metafunc, pytestconfig): diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py index cb7b0cd3c..2c5175246 100644 --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -8,10 +8,6 @@ import py import _pytest._code import pytest -from _pytest.nodes import FSCollector -from _pytest.nodes import Item -from _pytest.nodes import Node -from _pytest.resultlog import generic_path from _pytest.resultlog import pytest_configure from _pytest.resultlog import pytest_unconfigure from _pytest.resultlog import ResultLog @@ -20,31 +16,6 @@ from _pytest.resultlog import ResultLog pytestmark = pytest.mark.filterwarnings("ignore:--result-log is deprecated") -def test_generic_path(testdir): - from _pytest.main import Session - - config = testdir.parseconfig() - session = Session(config) - p1 = Node("a", config=config, session=session, nodeid="a") - # assert p1.fspath is None - p2 = Node("B", parent=p1) - p3 = Node("()", parent=p2) - item = Item("c", parent=p3) - - res = generic_path(item) - assert res == "a.B().c" - - p0 = FSCollector("proj/test", config=config, session=session) - p1 = FSCollector("proj/test/a", parent=p0) - p2 = Node("B", parent=p1) - p3 = Node("()", parent=p2) - p4 = Node("c", parent=p3) - item = Item("[1]", parent=p4) - - res = generic_path(item) - assert res == "test/a:B().c[1]" - - def test_write_log_entry(): reslog = ResultLog(None, None) reslog.logfile = py.io.TextIO() diff --git a/testing/test_runner.py b/testing/test_runner.py index 91f7d2700..66ae702a4 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -465,12 +465,7 @@ class TestSessionReports(object): assert res[1].name == "TestClass" -reporttypes = [ - reports.BaseReport, - reports.TestReport, - reports.TeardownErrorReport, - reports.CollectReport, -] +reporttypes = [reports.BaseReport, reports.TestReport, reports.CollectReport] @pytest.mark.parametrize( diff --git a/testing/test_session.py b/testing/test_session.py index d68fc9d41..d64a1a519 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -181,7 +181,6 @@ class TestNewSession(SessionTests): passed, skipped, failed = reprec.countoutcomes() assert failed == skipped == 0 assert passed == 7 - # also test listnames() here ... def test_collect_only_with_various_situations(self, testdir): p = testdir.makepyfile( diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 06345f88d..c0dd21bc2 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -18,7 +18,6 @@ from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.terminal import _plugin_nameversions from _pytest.terminal import build_summary_stats_line from _pytest.terminal import getreportopt -from _pytest.terminal import repr_pythonversion from _pytest.terminal import TerminalReporter DistInfo = collections.namedtuple("DistInfo", ["project_name", "version"]) @@ -361,16 +360,6 @@ class TestCollectonly(object): result.stdout.fnmatch_lines(["*test_fun.py: 1*"]) -def test_repr_python_version(monkeypatch): - try: - monkeypatch.setattr(sys, "version_info", (2, 5, 1, "final", 0)) - assert repr_pythonversion() == "2.5.1-final-0" - sys.version_info = x = (2, 3) - assert repr_pythonversion() == str(x) - finally: - monkeypatch.undo() # do this early as pytest can get confused - - class TestFixtureReporting(object): def test_setup_fixture_error(self, testdir): testdir.makepyfile( From af2ee1e80a2e261754e16148960b0424000458ee Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Sun, 2 Dec 2018 02:45:31 -0500 Subject: [PATCH 03/29] Emit JUnit compatible XML * Remove non-standard testcase elements: 'file' and 'line' * Replace testcase element 'skips' with 'skipped' * Time resolution uses the standard format: 0.000 * Tests use corrected XML output with proper attributes --- AUTHORS | 1 + changelog/3547.bugfix.rst | 1 + src/_pytest/junitxml.py | 12 +--- testing/test_junitxml.py | 126 ++++++++++---------------------------- 4 files changed, 36 insertions(+), 104 deletions(-) create mode 100644 changelog/3547.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 06947d17b..ad185729b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -118,6 +118,7 @@ Jonas Obrist Jordan Guymon Jordan Moldow Jordan Speicher +Joseph Hunkeler Joshua Bronson Jurko Gospodnetić Justyna Janczyszyn diff --git a/changelog/3547.bugfix.rst b/changelog/3547.bugfix.rst new file mode 100644 index 000000000..2c20661d5 --- /dev/null +++ b/changelog/3547.bugfix.rst @@ -0,0 +1 @@ +``--junitxml`` emits XML data compatible with JUnit's offical schema releases. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index a426f537b..6286e442a 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -107,20 +107,14 @@ class _NodeReporter(object): classnames = names[:-1] if self.xml.prefix: classnames.insert(0, self.xml.prefix) - attrs = { - "classname": ".".join(classnames), - "name": bin_xml_escape(names[-1]), - "file": testreport.location[0], - } - if testreport.location[1] is not None: - attrs["line"] = testreport.location[1] + attrs = {"classname": ".".join(classnames), "name": bin_xml_escape(names[-1])} if hasattr(testreport, "url"): attrs["url"] = testreport.url self.attrs = attrs self.attrs.update(existing_attrs) # restore any user-defined attributes def to_xml(self): - testcase = Junit.testcase(time=self.duration, **self.attrs) + testcase = Junit.testcase(time="%.3f" % self.duration, **self.attrs) testcase.append(self.make_properties_node()) for node in self.nodes: testcase.append(node) @@ -545,7 +539,7 @@ class LogXML(object): name=self.suite_name, errors=self.stats["error"], failures=self.stats["failure"], - skips=self.stats["skipped"], + skipped=self.stats["skipped"], tests=numtests, time="%.3f" % suite_time_delta, ).unicode(indent=0) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 59c11fa00..1021c728b 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -107,7 +107,7 @@ class TestPython(object): result, dom = runandparse(testdir) assert result.ret node = dom.find_first_by_tag("testsuite") - node.assert_attr(name="pytest", errors=0, failures=1, skips=2, tests=5) + node.assert_attr(name="pytest", errors=0, failures=1, skipped=2, tests=5) def test_summing_simple_with_errors(self, testdir): testdir.makepyfile( @@ -133,7 +133,7 @@ class TestPython(object): result, dom = runandparse(testdir) assert result.ret node = dom.find_first_by_tag("testsuite") - node.assert_attr(name="pytest", errors=1, failures=2, skips=1, tests=5) + node.assert_attr(name="pytest", errors=1, failures=2, skipped=1, tests=5) def test_timing_function(self, testdir): testdir.makepyfile( @@ -201,12 +201,7 @@ class TestPython(object): node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) tnode = node.find_first_by_tag("testcase") - tnode.assert_attr( - file="test_setup_error.py", - line="5", - classname="test_setup_error", - name="test_function", - ) + tnode.assert_attr(classname="test_setup_error", name="test_function") fnode = tnode.find_first_by_tag("error") fnode.assert_attr(message="test setup failure") assert "ValueError" in fnode.toxml() @@ -228,12 +223,7 @@ class TestPython(object): assert result.ret node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") - tnode.assert_attr( - file="test_teardown_error.py", - line="6", - classname="test_teardown_error", - name="test_function", - ) + tnode.assert_attr(classname="test_teardown_error", name="test_function") fnode = tnode.find_first_by_tag("error") fnode.assert_attr(message="test teardown failure") assert "ValueError" in fnode.toxml() @@ -274,14 +264,9 @@ class TestPython(object): result, dom = runandparse(testdir) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") - node.assert_attr(skips=1) + node.assert_attr(skipped=1) tnode = node.find_first_by_tag("testcase") - tnode.assert_attr( - file="test_skip_contains_name_reason.py", - line="1", - classname="test_skip_contains_name_reason", - name="test_skip", - ) + tnode.assert_attr(classname="test_skip_contains_name_reason", name="test_skip") snode = tnode.find_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello23") @@ -297,13 +282,10 @@ class TestPython(object): result, dom = runandparse(testdir) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") - node.assert_attr(skips=1) + node.assert_attr(skipped=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( - file="test_mark_skip_contains_name_reason.py", - line="1", - classname="test_mark_skip_contains_name_reason", - name="test_skip", + classname="test_mark_skip_contains_name_reason", name="test_skip" ) snode = tnode.find_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello24") @@ -321,13 +303,10 @@ class TestPython(object): result, dom = runandparse(testdir) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") - node.assert_attr(skips=1) + node.assert_attr(skipped=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( - file="test_mark_skipif_contains_name_reason.py", - line="2", - classname="test_mark_skipif_contains_name_reason", - name="test_skip", + classname="test_mark_skipif_contains_name_reason", name="test_skip" ) snode = tnode.find_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello25") @@ -360,10 +339,7 @@ class TestPython(object): node.assert_attr(failures=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( - file="test_classname_instance.py", - line="1", - classname="test_classname_instance.TestClass", - name="test_method", + classname="test_classname_instance.TestClass", name="test_method" ) def test_classname_nested_dir(self, testdir): @@ -374,12 +350,7 @@ class TestPython(object): node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1) tnode = node.find_first_by_tag("testcase") - tnode.assert_attr( - file=os.path.join("sub", "test_hello.py"), - line="0", - classname="sub.test_hello", - name="test_func", - ) + tnode.assert_attr(classname="sub.test_hello", name="test_func") def test_internal_error(self, testdir): testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0") @@ -415,12 +386,7 @@ class TestPython(object): node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1, tests=1) tnode = node.find_first_by_tag("testcase") - tnode.assert_attr( - file="test_failure_function.py", - line="3", - classname="test_failure_function", - name="test_fail", - ) + tnode.assert_attr(classname="test_failure_function", name="test_fail") fnode = tnode.find_first_by_tag("failure") fnode.assert_attr(message="ValueError: 42") assert "ValueError" in fnode.toxml() @@ -477,10 +443,7 @@ class TestPython(object): tnode = node.find_nth_by_tag("testcase", index) tnode.assert_attr( - file="test_failure_escape.py", - line="1", - classname="test_failure_escape", - name="test_func[%s]" % char, + classname="test_failure_escape", name="test_func[%s]" % char ) sysout = tnode.find_first_by_tag("system-out") text = sysout.text @@ -501,18 +464,10 @@ class TestPython(object): node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1, tests=2) tnode = node.find_first_by_tag("testcase") - tnode.assert_attr( - file="test_junit_prefixing.py", - line="0", - classname="xyz.test_junit_prefixing", - name="test_func", - ) + tnode.assert_attr(classname="xyz.test_junit_prefixing", name="test_func") tnode = node.find_nth_by_tag("testcase", 1) tnode.assert_attr( - file="test_junit_prefixing.py", - line="3", - classname="xyz.test_junit_prefixing.TestHello", - name="test_hello", + classname="xyz.test_junit_prefixing.TestHello", name="test_hello" ) def test_xfailure_function(self, testdir): @@ -526,14 +481,9 @@ class TestPython(object): result, dom = runandparse(testdir) assert not result.ret node = dom.find_first_by_tag("testsuite") - node.assert_attr(skips=1, tests=1) + node.assert_attr(skipped=1, tests=1) tnode = node.find_first_by_tag("testcase") - tnode.assert_attr( - file="test_xfailure_function.py", - line="1", - classname="test_xfailure_function", - name="test_xfail", - ) + tnode.assert_attr(classname="test_xfailure_function", name="test_xfail") fnode = tnode.find_first_by_tag("skipped") fnode.assert_attr(message="expected test failure") # assert "ValueError" in fnode.toxml() @@ -569,14 +519,9 @@ class TestPython(object): result, dom = runandparse(testdir) # assert result.ret node = dom.find_first_by_tag("testsuite") - node.assert_attr(skips=0, tests=1) + node.assert_attr(skipped=0, tests=1) tnode = node.find_first_by_tag("testcase") - tnode.assert_attr( - file="test_xfailure_xpass.py", - line="1", - classname="test_xfailure_xpass", - name="test_xpass", - ) + tnode.assert_attr(classname="test_xfailure_xpass", name="test_xpass") def test_xfailure_xpass_strict(self, testdir): testdir.makepyfile( @@ -590,14 +535,9 @@ class TestPython(object): result, dom = runandparse(testdir) # assert result.ret node = dom.find_first_by_tag("testsuite") - node.assert_attr(skips=0, tests=1) + node.assert_attr(skipped=0, tests=1) tnode = node.find_first_by_tag("testcase") - tnode.assert_attr( - file="test_xfailure_xpass_strict.py", - line="1", - classname="test_xfailure_xpass_strict", - name="test_xpass", - ) + tnode.assert_attr(classname="test_xfailure_xpass_strict", name="test_xpass") fnode = tnode.find_first_by_tag("failure") fnode.assert_attr(message="[XPASS(strict)] This needs to fail!") @@ -608,8 +548,6 @@ class TestPython(object): node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) tnode = node.find_first_by_tag("testcase") - tnode.assert_attr(file="test_collect_error.py", name="test_collect_error") - assert tnode["line"] is None fnode = tnode.find_first_by_tag("error") fnode.assert_attr(message="collection failure") assert "SyntaxError" in fnode.toxml() @@ -792,7 +730,7 @@ class TestNonPython(object): result, dom = runandparse(testdir) assert result.ret node = dom.find_first_by_tag("testsuite") - node.assert_attr(errors=0, failures=1, skips=0, tests=1) + node.assert_attr(errors=0, failures=1, skipped=0, tests=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr(name="myfile.xyz") fnode = tnode.find_first_by_tag("failure") @@ -1155,20 +1093,18 @@ def test_fancy_items_regression(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") - ) + items = sorted("%(classname)s %(name)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", + u"conftest a", + u"conftest a", + u"conftest b", + u"test_fancy_items_regression a", + u"test_fancy_items_regression a", + u"test_fancy_items_regression b", + u"test_fancy_items_regression test_pass", ] From 2e551c32b6fd42352c2ef20d324b7a91955c9af6 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 3 Dec 2018 09:56:21 -0500 Subject: [PATCH 04/29] Add junit_family config option --- src/_pytest/junitxml.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 6286e442a..80ca92471 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -309,6 +309,9 @@ def pytest_addoption(parser): "Duration time to report: one of total|call", default="total", ) # choices=['total', 'call']) + parser.addini( + "junit_family", "Emit XML for schema: one of old|xunit1|xunit2", default="old" + ) def pytest_configure(config): @@ -321,6 +324,7 @@ def pytest_configure(config): config.getini("junit_suite_name"), config.getini("junit_logging"), config.getini("junit_duration_report"), + config.getini("junit_family"), ) config.pluginmanager.register(config._xml) From 335cc5d65185a2823528ecf95e908002e544b4f2 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 3 Dec 2018 17:21:11 -0500 Subject: [PATCH 05/29] Handle backwards-compatiblity --- src/_pytest/junitxml.py | 64 ++++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 80ca92471..62e8a772b 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -66,12 +66,37 @@ def bin_xml_escape(arg): return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg))) +def merge_family(left, right): + result = {} + for kl, vl in left.items(): + for kr, vr in right.items(): + if not isinstance(vl, list): + raise NotImplementedError(type(vl)) + result[kl] = vl + vr + left.update(result) + + +families = {} +families["_base"] = {"testcase": ["classname", "name"]} +families["_base_old"] = {"testcase": ["file", "line", "url"]} + +# xUnit 1.x inherits old attributes +families["xunit1"] = families["_base"].copy() +merge_family(families["xunit1"], families["_base_old"]) + +# Alias "old" to xUnit 1.x +families["old"] = families["xunit1"] + +# xUnit 2.x uses strict base attributes +families["xunit2"] = families["_base"] + + class _NodeReporter(object): def __init__(self, nodeid, xml): - self.id = nodeid self.xml = xml self.add_stats = self.xml.add_stats + self.family = self.xml.family self.duration = 0 self.properties = [] self.nodes = [] @@ -107,12 +132,31 @@ class _NodeReporter(object): classnames = names[:-1] if self.xml.prefix: classnames.insert(0, self.xml.prefix) - attrs = {"classname": ".".join(classnames), "name": bin_xml_escape(names[-1])} + attrs = { + "classname": ".".join(classnames), + "name": bin_xml_escape(names[-1]), + "file": testreport.location[0], + } + if testreport.location[1] is not None: + attrs["line"] = testreport.location[1] if hasattr(testreport, "url"): attrs["url"] = testreport.url self.attrs = attrs self.attrs.update(existing_attrs) # restore any user-defined attributes + # Preserve old testcase behavior + if self.family == "old": + return + + # Purge attributes not permitted by this test family + # This includes custom attributes, because they are not valid here. + # TODO: Convert invalid attributes to properties to preserve "something" + temp_attrs = {} + for key in self.attrs.keys(): + if key in families[self.family]["testcase"]: + temp_attrs[key] = self.attrs[key] + self.attrs = temp_attrs + def to_xml(self): testcase = Junit.testcase(time="%.3f" % self.duration, **self.attrs) testcase.append(self.make_properties_node()) @@ -231,7 +275,7 @@ class _NodeReporter(object): def finalize(self): data = self.to_xml().unicode(indent=0) self.__dict__.clear() - self.to_xml = lambda: py.xml.raw(data) + self.raw = lambda: py.xml.raw(data) @pytest.fixture @@ -353,12 +397,7 @@ def mangle_test_address(address): class LogXML(object): def __init__( - self, - logfile, - prefix, - suite_name="pytest", - logging="no", - report_duration="total", + self, logfile, prefix, suite_name="pytest", logging="no", report_duration="total", family="old" ): logfile = os.path.expanduser(os.path.expandvars(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile)) @@ -366,6 +405,7 @@ class LogXML(object): self.suite_name = suite_name self.logging = logging self.report_duration = report_duration + self.family = family self.stats = dict.fromkeys(["error", "passed", "failure", "skipped"], 0) self.node_reporters = {} # nodeid -> _NodeReporter self.node_reporters_ordered = [] @@ -539,7 +579,7 @@ class LogXML(object): logfile.write( Junit.testsuite( self._get_global_properties_node(), - [x.to_xml() for x in self.node_reporters_ordered], + [x.raw() for x in self.node_reporters_ordered], name=self.suite_name, errors=self.stats["error"], failures=self.stats["failure"], @@ -550,6 +590,10 @@ class LogXML(object): ) logfile.close() + # TODO: GET RID OF + with open(self.logfile) as logfile: + print(logfile.read()) + def pytest_terminal_summary(self, terminalreporter): terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile)) From 343430c537a8a7f933f74ff17109c3f7072432f5 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 14 Jan 2019 10:27:47 -0500 Subject: [PATCH 06/29] Replace family "old" with "legacy" --- src/_pytest/junitxml.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 62e8a772b..aabce4734 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -78,14 +78,14 @@ def merge_family(left, right): families = {} families["_base"] = {"testcase": ["classname", "name"]} -families["_base_old"] = {"testcase": ["file", "line", "url"]} +families["_base_legacy"] = {"testcase": ["file", "line", "url"]} -# xUnit 1.x inherits old attributes +# xUnit 1.x inherits legacy attributes families["xunit1"] = families["_base"].copy() -merge_family(families["xunit1"], families["_base_old"]) +merge_family(families["xunit1"], families["_base_legacy"]) -# Alias "old" to xUnit 1.x -families["old"] = families["xunit1"] +# Alias "legacy" to xUnit 1.x +families["legacy"] = families["xunit1"] # xUnit 2.x uses strict base attributes families["xunit2"] = families["_base"] @@ -144,8 +144,8 @@ class _NodeReporter(object): self.attrs = attrs self.attrs.update(existing_attrs) # restore any user-defined attributes - # Preserve old testcase behavior - if self.family == "old": + # Preserve legacy testcase behavior + if self.family == "legacy": return # Purge attributes not permitted by this test family @@ -354,7 +354,9 @@ def pytest_addoption(parser): default="total", ) # choices=['total', 'call']) parser.addini( - "junit_family", "Emit XML for schema: one of old|xunit1|xunit2", default="old" + "junit_family", + "Emit XML for schema: one of legacy|xunit1|xunit2", + default="legacy", ) @@ -397,7 +399,13 @@ def mangle_test_address(address): class LogXML(object): def __init__( - self, logfile, prefix, suite_name="pytest", logging="no", report_duration="total", family="old" + self, + logfile, + prefix, + suite_name="pytest", + logging="no", + report_duration="total", + family="legacy", ): logfile = os.path.expanduser(os.path.expandvars(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile)) From 8937e39afde63016c24bc32b6569a478358c8a57 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 14 Jan 2019 10:34:03 -0500 Subject: [PATCH 07/29] Raise TypeError instead of NotImplementedError if not list type --- src/_pytest/junitxml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index aabce4734..6a02da406 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -71,7 +71,7 @@ def merge_family(left, right): for kl, vl in left.items(): for kr, vr in right.items(): if not isinstance(vl, list): - raise NotImplementedError(type(vl)) + raise TypeError(type(vl)) result[kl] = vl + vr left.update(result) From aaa7d36bc9ffca4970fa779922763e3db402614f Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 14 Jan 2019 13:14:44 -0500 Subject: [PATCH 08/29] Change family behavior: * "legacy" is no longer a copy of "xunit1" * Attempts to use "legacy" will redirect to "xunit1" * record_xml_attribute is not compatible outside of legacy family * Replace call to method/override raw() with to_xml() --- src/_pytest/junitxml.py | 45 ++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 6a02da406..115067b70 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -84,9 +84,6 @@ families["_base_legacy"] = {"testcase": ["file", "line", "url"]} families["xunit1"] = families["_base"].copy() merge_family(families["xunit1"], families["_base_legacy"]) -# Alias "legacy" to xUnit 1.x -families["legacy"] = families["xunit1"] - # xUnit 2.x uses strict base attributes families["xunit2"] = families["_base"] @@ -145,7 +142,7 @@ class _NodeReporter(object): self.attrs.update(existing_attrs) # restore any user-defined attributes # Preserve legacy testcase behavior - if self.family == "legacy": + if self.family == "xunit1": return # Purge attributes not permitted by this test family @@ -275,7 +272,7 @@ class _NodeReporter(object): def finalize(self): data = self.to_xml().unicode(indent=0) self.__dict__.clear() - self.raw = lambda: py.xml.raw(data) + self.to_xml = lambda: py.xml.raw(data) @pytest.fixture @@ -307,16 +304,26 @@ def record_xml_attribute(request): from _pytest.warning_types import PytestWarning request.node.warn(PytestWarning("record_xml_attribute is an experimental feature")) + + # Declare noop + def add_attr_noop(name, value): + pass + + attr_func = add_attr_noop xml = getattr(request.config, "_xml", None) - if xml is not None: + + if xml.family != "xunit1": + request.node.warn( + PytestWarning( + "record_xml_attribute is incompatible with junit_family: " + "%s (use: legacy|xunit1)" % xml.family + ) + ) + elif xml is not None: node_reporter = xml.node_reporter(request.node.nodeid) - return node_reporter.add_attribute - else: + attr_func = node_reporter.add_attribute - def add_attr_noop(name, value): - pass - - return add_attr_noop + return attr_func def pytest_addoption(parser): @@ -356,7 +363,7 @@ def pytest_addoption(parser): parser.addini( "junit_family", "Emit XML for schema: one of legacy|xunit1|xunit2", - default="legacy", + default="xunit1", ) @@ -405,7 +412,7 @@ class LogXML(object): suite_name="pytest", logging="no", report_duration="total", - family="legacy", + family="xunit1", ): logfile = os.path.expanduser(os.path.expandvars(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile)) @@ -422,6 +429,10 @@ class LogXML(object): self.open_reports = [] self.cnt_double_fail_tests = 0 + # Replaces convenience family with real family + if self.family == "legacy": + self.family = "xunit1" + def finalize(self, report): nodeid = getattr(report, "nodeid", report) # local hack to handle xdist report order @@ -587,7 +598,7 @@ class LogXML(object): logfile.write( Junit.testsuite( self._get_global_properties_node(), - [x.raw() for x in self.node_reporters_ordered], + [x.to_xml() for x in self.node_reporters_ordered], name=self.suite_name, errors=self.stats["error"], failures=self.stats["failure"], @@ -598,10 +609,6 @@ class LogXML(object): ) logfile.close() - # TODO: GET RID OF - with open(self.logfile) as logfile: - print(logfile.read()) - def pytest_terminal_summary(self, terminalreporter): terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile)) From 4ecf29380abafd27a01503ed1335d36bc350acf6 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 14 Jan 2019 13:21:06 -0500 Subject: [PATCH 09/29] Adds xunit2 version of test_record_attribute --- testing/test_junitxml.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 1021c728b..2765dfc60 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -980,6 +980,12 @@ def test_record_property_same_name(testdir): @pytest.mark.filterwarnings("default") def test_record_attribute(testdir): + testdir.makeini( + """ + [pytest] + junit_family = xunit1 + """ + ) testdir.makepyfile( """ import pytest @@ -1001,6 +1007,38 @@ def test_record_attribute(testdir): ) +@pytest.mark.filterwarnings("default") +def test_record_attribute_xunit2(testdir): + """Ensure record_xml_attribute drops values when outside of legacy family + """ + testdir.makeini( + """ + [pytest] + junit_family = xunit2 + """ + ) + testdir.makepyfile( + """ + import pytest + + @pytest.fixture + def other(record_xml_attribute): + record_xml_attribute("bar", 1) + def test_record(record_xml_attribute, other): + record_xml_attribute("foo", "<1"); + """ + ) + + result, dom = runandparse(testdir, "-rw") + result.stdout.fnmatch_lines( + [ + "*test_record_attribute_xunit2.py:6:*record_xml_attribute is an experimental feature", + "*test_record_attribute_xunit2.py:6:*record_xml_attribute is incompatible with " + "junit_family: xunit2 (use: legacy|xunit1)", + ] + ) + + def test_random_report_log_xdist(testdir, monkeypatch): """xdist calls pytest_runtest_logreport as they are executed by the slaves, with nodes from several nodes overlapping, so junitxml must cope with that From bcacc40775460306ca16738012fbb3196accb98e Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 14 Jan 2019 14:24:10 -0500 Subject: [PATCH 10/29] Update comment text --- changelog/3547.bugfix.rst | 2 +- src/_pytest/junitxml.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/changelog/3547.bugfix.rst b/changelog/3547.bugfix.rst index 2c20661d5..b53e49c5c 100644 --- a/changelog/3547.bugfix.rst +++ b/changelog/3547.bugfix.rst @@ -1 +1 @@ -``--junitxml`` emits XML data compatible with JUnit's offical schema releases. +``--junitxml`` emits XML data compatible with Jenkins xUnit schemas via ``junit_family`` INI config option diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 115067b70..96755cf5b 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -145,9 +145,8 @@ class _NodeReporter(object): if self.family == "xunit1": return - # Purge attributes not permitted by this test family - # This includes custom attributes, because they are not valid here. - # TODO: Convert invalid attributes to properties to preserve "something" + # Filter out attributes not permitted by this test family. + # Including custom attributes because they are not valid here. temp_attrs = {} for key in self.attrs.keys(): if key in families[self.family]["testcase"]: From 8967976443f6eb13f037e031b015b1764006033c Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 14 Jan 2019 14:38:58 -0500 Subject: [PATCH 11/29] Ensure xml object is viable before testing family type --- src/_pytest/junitxml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 96755cf5b..e2e1d43bb 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -311,7 +311,7 @@ def record_xml_attribute(request): attr_func = add_attr_noop xml = getattr(request.config, "_xml", None) - if xml.family != "xunit1": + if xml is not None and xml.family != "xunit1": request.node.warn( PytestWarning( "record_xml_attribute is incompatible with junit_family: " From 85c5fa9f64dbdae04729b38ca495eb38d4bb17cd Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 14 Jan 2019 15:07:40 -0500 Subject: [PATCH 12/29] Update changelog --- changelog/3547.bugfix.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog/3547.bugfix.rst b/changelog/3547.bugfix.rst index b53e49c5c..b2ec00364 100644 --- a/changelog/3547.bugfix.rst +++ b/changelog/3547.bugfix.rst @@ -1 +1,2 @@ -``--junitxml`` emits XML data compatible with Jenkins xUnit schemas via ``junit_family`` INI config option +``--junitxml`` can emit XML compatible with Jenkins xUnit. +``junit_family`` INI option accepts ``legacy|xunit1``, which produces old style output, and ``xunit2`` that conforms more strictly to https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd From e69b1255d7bf765d1473e71814c35bb46f72d5b8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 31 Oct 2018 16:20:44 +0100 Subject: [PATCH 13/29] Improve quitting from pdb Regarding tests: it merges ``test_pdb_interaction``, ``test_pdb_print_captured_stdout``, and ``test_pdb_print_captured_stderr`` into ``test_pdb_print_captured_stdout_and_stderr`` (clarity and performance, especially since pexpect tests are slow). --- changelog/4280.bugfix.rst | 3 + src/_pytest/debugging.py | 10 ++- src/_pytest/outcomes.py | 6 +- testing/test_pdb.py | 128 +++++++++++++++++++++----------------- 4 files changed, 85 insertions(+), 62 deletions(-) create mode 100644 changelog/4280.bugfix.rst diff --git a/changelog/4280.bugfix.rst b/changelog/4280.bugfix.rst new file mode 100644 index 000000000..4814823f1 --- /dev/null +++ b/changelog/4280.bugfix.rst @@ -0,0 +1,3 @@ +Improve quitting from pdb, especially with ``--trace``. + +Using ``q[quit]`` after ``pdb.set_trace()`` will quit pytest also. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index adf9d0e54..7a1060ae0 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -118,6 +118,10 @@ class pytestPDB(object): do_c = do_cont = do_continue + def set_quit(self): + super(_PdbWrapper, self).set_quit() + outcomes.exit("Quitting debugger") + def setup(self, f, tb): """Suspend on setup(). @@ -210,8 +214,7 @@ def _enter_pdb(node, excinfo, rep): tw.sep(">", "entering PDB") tb = _postmortem_traceback(excinfo) rep._pdbshown = True - if post_mortem(tb): - outcomes.exit("Quitting debugger") + post_mortem(tb) return rep @@ -242,4 +245,5 @@ def post_mortem(t): p = Pdb() p.reset() p.interaction(None, t) - return p.quitting + if p.quitting: + outcomes.exit("Quitting debugger") diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index d27939e30..14c6e9ab6 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -49,13 +49,13 @@ class Failed(OutcomeException): __module__ = "builtins" -class Exit(SystemExit): +class Exit(Exception): """ raised for immediate program exits (no tracebacks/summaries)""" def __init__(self, msg="unknown reason", returncode=None): self.msg = msg self.returncode = returncode - SystemExit.__init__(self, msg) + super(Exit, self).__init__(msg) # exposed helper methods @@ -63,7 +63,7 @@ class Exit(SystemExit): def exit(msg, returncode=None): """ - Exit testing process as if SystemExit was triggered. + Exit testing process. :param str msg: message to display upon exit. :param int returncode: return code to be used when exiting pytest. diff --git a/testing/test_pdb.py b/testing/test_pdb.py index cb1017ac4..49fbbad72 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -147,29 +147,6 @@ class TestPDB(object): assert rep.failed assert len(pdblist) == 1 - def test_pdb_interaction(self, testdir): - p1 = testdir.makepyfile( - """ - def test_1(): - i = 0 - assert i == 1 - - def test_not_called_due_to_quit(): - pass - """ - ) - child = testdir.spawn_pytest("--pdb %s" % p1) - child.expect(".*def test_1") - child.expect(".*i = 0") - child.expect("Pdb") - child.sendeof() - rest = child.read().decode("utf8") - assert "= 1 failed in" in rest - assert "def test_1" not in rest - assert "Exit: Quitting debugger" in rest - assert "PDB continue (IO-capturing resumed)" not in rest - self.flush(child) - @staticmethod def flush(child): if platform.system() == "Darwin": @@ -214,40 +191,32 @@ class TestPDB(object): child.sendeof() self.flush(child) - def test_pdb_print_captured_stdout(self, testdir): - p1 = testdir.makepyfile( - """ - def test_1(): - print("get\\x20rekt") - assert False - """ - ) - child = testdir.spawn_pytest("--pdb %s" % p1) - child.expect("captured stdout") - child.expect("get rekt") - child.expect("Pdb") - child.sendeof() - rest = child.read().decode("utf8") - assert "1 failed" in rest - assert "get rekt" not in rest - self.flush(child) - - def test_pdb_print_captured_stderr(self, testdir): + def test_pdb_print_captured_stdout_and_stderr(self, testdir): p1 = testdir.makepyfile( """ def test_1(): import sys sys.stderr.write("get\\x20rekt") + print("get\\x20rekt") assert False + + def test_not_called_due_to_quit(): + pass """ ) child = testdir.spawn_pytest("--pdb %s" % p1) + child.expect("captured stdout") + child.expect("get rekt") child.expect("captured stderr") child.expect("get rekt") + child.expect("traceback") + child.expect("def test_1") child.expect("Pdb") child.sendeof() rest = child.read().decode("utf8") - assert "1 failed" in rest + assert "Exit: Quitting debugger" in rest + assert "= 1 failed in" in rest + assert "def test_1" not in rest assert "get rekt" not in rest self.flush(child) @@ -375,15 +344,17 @@ class TestPDB(object): i = 0 print("hello17") pytest.set_trace() - x = 3 + i == 1 + assert 0 """ ) child = testdir.spawn_pytest(str(p1)) - child.expect("test_1") - child.expect("x = 3") + child.expect(r"test_1\(\)") + child.expect("i == 1") child.expect("Pdb") - child.sendeof() + child.sendline("c") rest = child.read().decode("utf-8") + assert "AssertionError" in rest assert "1 failed" in rest assert "def test_1" in rest assert "hello17" in rest # out is captured @@ -398,13 +369,14 @@ class TestPDB(object): print("hello17") pytest.set_trace(header="== my_header ==") x = 3 + assert 0 """ ) child = testdir.spawn_pytest(str(p1)) child.expect("== my_header ==") assert "PDB set_trace" not in child.before.decode() child.expect("Pdb") - child.sendeof() + child.sendline("c") rest = child.read().decode("utf-8") assert "1 failed" in rest assert "def test_1" in rest @@ -424,9 +396,9 @@ class TestPDB(object): child.expect("Pdb") child.sendeof() rest = child.read().decode("utf8") - assert "1 failed" in rest + assert "no tests ran" in rest assert "reading from stdin while output" not in rest - assert "BdbQuit" in rest + assert "BdbQuit" not in rest self.flush(child) def test_pdb_and_capsys(self, testdir): @@ -518,6 +490,7 @@ class TestPDB(object): print("hello18") pytest.set_trace() x = 4 + assert 0 """ ) child = testdir.spawn_pytest(str(p1)) @@ -530,11 +503,11 @@ class TestPDB(object): child.expect(r"PDB set_trace \(IO-capturing turned off\)") child.expect("x = 4") child.expect("Pdb") - child.sendeof() + child.sendline("c") child.expect("_ test_1 _") child.expect("def test_1") - child.expect("Captured stdout call") rest = child.read().decode("utf8") + assert "Captured stdout call" in rest assert "hello17" in rest # out is captured assert "hello18" in rest # out is captured assert "1 failed" in rest @@ -795,7 +768,7 @@ class TestDebuggingBreakpoints(object): child.expect("Pdb") child.sendeof() rest = child.read().decode("utf8") - assert "1 failed" in rest + assert "Quitting debugger" in rest assert "reading from stdin while output" not in rest TestPDB.flush(child) @@ -808,12 +781,13 @@ class TestDebuggingBreakpoints(object): import pdb def test_1(): pdb.set_trace() + assert 0 """ ) child = testdir.spawn_pytest(str(p1)) child.expect("test_1") child.expect("Pdb") - child.sendeof() + child.sendline("c") rest = child.read().decode("utf8") assert "1 failed" in rest assert "reading from stdin while output" not in rest @@ -826,15 +800,29 @@ class TestTraceOption: """ def test_1(): assert True + + def test_2(): + pass + + def test_3(): + pass """ ) child = testdir.spawn_pytest("--trace " + str(p1)) child.expect("test_1") child.expect("Pdb") - child.sendeof() + child.sendline("c") + child.expect("test_2") + child.expect("Pdb") + child.sendline("c") + child.expect("test_3") + child.expect("Pdb") + child.sendline("q") + child.expect_exact("Exit: Quitting debugger") rest = child.read().decode("utf8") - assert "1 passed" in rest + assert "2 passed in" in rest assert "reading from stdin while output" not in rest + assert "Exit: Quitting debugger" in child.before.decode("utf8") TestPDB.flush(child) @@ -863,3 +851,31 @@ def test_trace_after_runpytest(testdir): rest = child.read().decode("utf8") TestPDB.flush(child) assert child.exitstatus == 0, rest + + +def test_quit_with_swallowed_SystemExit(testdir): + """Test that debugging's pytest_configure is re-entrant.""" + p1 = testdir.makepyfile( + """ + def call_pdb_set_trace(): + __import__('pdb').set_trace() + + + def test_1(): + try: + call_pdb_set_trace() + except SystemExit: + pass + + + def test_2(): + pass + """ + ) + child = testdir.spawn_pytest(str(p1)) + child.expect("Pdb") + child.sendline("q") + child.expect_exact("Exit: Quitting debugger") + rest = child.read().decode("utf8") + assert "no tests ran" in rest + TestPDB.flush(child) From 9f86e834782065fea3dae9e31d9a86fe0f5cfb4d Mon Sep 17 00:00:00 2001 From: Kristoffer Nordstroem Date: Fri, 30 Nov 2018 13:19:02 +0100 Subject: [PATCH 14/29] count selected tests --- src/_pytest/terminal.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index c1d4d1f91..db1316c81 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -497,6 +497,7 @@ class TerminalReporter(object): errors = len(self.stats.get("error", [])) skipped = len(self.stats.get("skipped", [])) deselected = len(self.stats.get("deselected", [])) + selected = self._numcollected - errors - skipped - deselected if final: line = "collected " else: @@ -510,6 +511,8 @@ class TerminalReporter(object): line += " / %d deselected" % deselected if skipped: line += " / %d skipped" % skipped + if self._numcollected > selected: + line += " / %d selected" % selected if self.isatty: self.rewrite(line, bold=True, erase=True) if final: From 8723eb16ea377e94d1deacff9e831c46fc9b5c97 Mon Sep 17 00:00:00 2001 From: Kristoffer Nordstroem Date: Thu, 24 Jan 2019 00:00:29 +0100 Subject: [PATCH 15/29] only print selected if any have been selected --- src/_pytest/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index db1316c81..9a0d7686e 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -511,7 +511,7 @@ class TerminalReporter(object): line += " / %d deselected" % deselected if skipped: line += " / %d skipped" % skipped - if self._numcollected > selected: + if self._numcollected > selected > 0: line += " / %d selected" % selected if self.isatty: self.rewrite(line, bold=True, erase=True) From 6d388689500af36bdc10fd7816105433f8f97194 Mon Sep 17 00:00:00 2001 From: Kristoffer Nordstroem Date: Thu, 24 Jan 2019 00:08:43 +0100 Subject: [PATCH 16/29] fix tests by adding additional output to expected responses --- testing/test_cacheprovider.py | 2 +- testing/test_terminal.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 29c2d8a1d..35a7232ab 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -418,7 +418,7 @@ class TestLastFailed(object): result = testdir.runpytest("--lf") result.stdout.fnmatch_lines( [ - "collected 4 items / 2 deselected", + "collected 4 items / 2 deselected / 2 selected", "run-last-failure: rerun previous 2 failures", "*2 failed, 2 deselected in*", ] diff --git a/testing/test_terminal.py b/testing/test_terminal.py index c0dd21bc2..89d455b39 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -474,7 +474,7 @@ class TestTerminalFunctional(object): ) result = testdir.runpytest("-k", "test_two:", testpath) result.stdout.fnmatch_lines( - ["collected 3 items / 1 deselected", "*test_deselected.py ..*"] + ["collected 3 items / 1 deselected / 2 selected", "*test_deselected.py ..*"] ) assert result.ret == 0 @@ -498,7 +498,7 @@ class TestTerminalFunctional(object): result = testdir.runpytest("-m", "not foo") result.stdout.fnmatch_lines( [ - "collected 3 items / 1 deselected", + "collected 3 items / 1 deselected / 2 selected", "*test_show_deselected.py ..*", "*= 2 passed, 1 deselected in * =*", ] From 37aab5dd6bccf86deac36198a7e007b97097756d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 24 Jan 2019 20:07:21 -0200 Subject: [PATCH 17/29] Add CHANGELOG entry for #4660 --- changelog/4660.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4660.feature.rst diff --git a/changelog/4660.feature.rst b/changelog/4660.feature.rst new file mode 100644 index 000000000..a8ae5213b --- /dev/null +++ b/changelog/4660.feature.rst @@ -0,0 +1 @@ +The number of *selected* tests now are also displayed when the ``-k`` or ``-m`` flags are used. From e2cd2cd409f34d732418830ef3064569cbf5da4d Mon Sep 17 00:00:00 2001 From: Kristoffer Nordstroem Date: Thu, 24 Jan 2019 23:18:21 +0100 Subject: [PATCH 18/29] vanity commit --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 06947d17b..20a0fb6bf 100644 --- a/AUTHORS +++ b/AUTHORS @@ -127,6 +127,7 @@ Katerina Koukiou Kevin Cox Kodi B. Arfer Kostis Anagnostopoulos +Kristoffer Nordström Kyle Altendorf Lawrence Mitchell Lee Kamentsky From f7d755552100afe8df3da6da64fff6ac59c52de6 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 27 Jan 2019 13:05:34 +0100 Subject: [PATCH 19/29] fix #4680 - ensure tmpdir and tmp_path are the same --- changelog/4680.bugfix.rst | 1 + src/_pytest/tmpdir.py | 4 ++-- testing/test_tmpdir.py | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelog/4680.bugfix.rst diff --git a/changelog/4680.bugfix.rst b/changelog/4680.bugfix.rst new file mode 100644 index 000000000..9126a70ea --- /dev/null +++ b/changelog/4680.bugfix.rst @@ -0,0 +1 @@ +Ensure the ``tmpdir`` and the ``tmp_path`` fixtures are the same folder. diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 860c2d4af..267328414 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -167,7 +167,7 @@ def _mk_tmp(request, factory): @pytest.fixture -def tmpdir(request, tmpdir_factory): +def tmpdir(tmp_path): """Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary @@ -176,7 +176,7 @@ def tmpdir(request, tmpdir_factory): .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html """ - return _mk_tmp(request, tmpdir_factory) + return py.path.local(tmp_path) @pytest.fixture diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 6040d9444..3e6bde379 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -337,3 +337,7 @@ def attempt_symlink_to(path, to_path): Path(path).symlink_to(Path(to_path)) except OSError: pytest.skip("could not create symbolic link") + + +def test_tmpdir_equals_tmp_path(tmpdir, tmp_path): + assert Path(tmpdir) == tmp_path From c75bd0880729e36d58e61a3da8828c3b47edb030 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 27 Jan 2019 14:08:43 +0100 Subject: [PATCH 20/29] ensure tmp_path is always a realpath --- changelog/4681.bugfix.rst | 1 + src/_pytest/tmpdir.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog/4681.bugfix.rst diff --git a/changelog/4681.bugfix.rst b/changelog/4681.bugfix.rst new file mode 100644 index 000000000..3ece27397 --- /dev/null +++ b/changelog/4681.bugfix.rst @@ -0,0 +1 @@ +Ensure ``tmp_path`` is always a real path. diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 267328414..b8c755ac6 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -61,11 +61,11 @@ class TempPathFactory(object): """ return base temporary directory. """ if self._basetemp is None: if self._given_basetemp is not None: - basetemp = self._given_basetemp + basetemp = self._given_basetemp.resolve() ensure_reset_dir(basetemp) else: from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT") - temproot = Path(from_env or tempfile.gettempdir()) + temproot = Path(from_env or tempfile.gettempdir()).resolve() user = get_user() or "unknown" # use a sub-directory in the temproot to speed-up # make_numbered_dir() call From 5567c772cd23ebab01cfe642e2363bc1661068a3 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 27 Jan 2019 14:19:23 +0100 Subject: [PATCH 21/29] quick&dirty fix fixture tests that rely on tmppath fixture structure --- testing/python/fixture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 196b28c51..3d557cec8 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -739,7 +739,7 @@ class TestRequestBasic(object): def test_function(request, farg): assert set(get_public_names(request.fixturenames)) == \ set(["tmpdir", "sarg", "arg1", "request", "farg", - "tmpdir_factory"]) + "tmp_path", "tmp_path_factory"]) """ ) reprec = testdir.inline_run() From 5b09eb1d742a36b15318988df783204d4dee25eb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 28 Jan 2019 19:00:50 -0200 Subject: [PATCH 22/29] Add config parameter to pytest_report_teststatus hook --- src/_pytest/hookspec.py | 4 +++- src/_pytest/resultlog.py | 4 +++- src/_pytest/terminal.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 2dfbfd0c9..4f346ba25 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -481,9 +481,11 @@ def pytest_report_collectionfinish(config, startdir, items): @hookspec(firstresult=True) -def pytest_report_teststatus(report): +def pytest_report_teststatus(report, config): """ return result-category, shortletter and verbose word for reporting. + :param _pytest.config.Config config: pytest config object + Stops at first non-None result, see :ref:`firstresult` """ diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index a2dd73094..cd3fde80f 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -66,7 +66,9 @@ class ResultLog(object): def pytest_runtest_logreport(self, report): if report.when != "call" and report.passed: return - res = self.config.hook.pytest_report_teststatus(report=report) + res = self.config.hook.pytest_report_teststatus( + report=report, config=self.config + ) code = res[1] if code == "x": longrepr = str(report.longrepr) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 9a0d7686e..3c6504b66 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -363,7 +363,7 @@ class TerminalReporter(object): def pytest_runtest_logreport(self, report): rep = report - res = self.config.hook.pytest_report_teststatus(report=rep) + res = self.config.hook.pytest_report_teststatus(report=rep, config=self.config) category, letter, word = res if isinstance(word, tuple): word, markup = word From d720312df02f3d621e9de8c3bcee8903b07d5595 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 28 Jan 2019 19:03:19 -0200 Subject: [PATCH 23/29] Add CHANGELOG entry for #4688 --- changelog/4688.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4688.feature.rst diff --git a/changelog/4688.feature.rst b/changelog/4688.feature.rst new file mode 100644 index 000000000..966c224a5 --- /dev/null +++ b/changelog/4688.feature.rst @@ -0,0 +1 @@ +``pytest_report_teststatus`` hook now can also receive a ``config`` parameter. From 6c3b86369fe3ab68ac252ed89f3faa3e6496aba1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 29 Jan 2019 15:41:13 -0200 Subject: [PATCH 24/29] Add config to pytest_terminal_summary hook The docs stated that this hook got the 'config' parameter in 3.5, but the docs wre probably changed by mistake. --- src/_pytest/hookspec.py | 5 +++-- src/_pytest/terminal.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 4f346ba25..3b6c9a7d1 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -489,13 +489,14 @@ def pytest_report_teststatus(report, config): Stops at first non-None result, see :ref:`firstresult` """ -def pytest_terminal_summary(terminalreporter, exitstatus): +def pytest_terminal_summary(terminalreporter, exitstatus, config): """Add a section to terminal summary reporting. :param _pytest.terminal.TerminalReporter terminalreporter: the internal terminal reporter object :param int exitstatus: the exit status that will be reported back to the OS + :param _pytest.config.Config config: pytest config object - .. versionadded:: 3.5 + .. versionadded:: 4.2 The ``config`` parameter. """ diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 3c6504b66..bb1c312cd 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -633,7 +633,7 @@ class TerminalReporter(object): ) if exitstatus in summary_exit_codes: self.config.hook.pytest_terminal_summary( - terminalreporter=self, exitstatus=exitstatus + terminalreporter=self, exitstatus=exitstatus, config=self.config ) if exitstatus == EXIT_INTERRUPTED: self._report_keyboardinterrupt() From 7e8e593a454ca2ced9e275a61cf619d5aab65c05 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 29 Jan 2019 15:52:45 -0200 Subject: [PATCH 25/29] Add CHANGELOG entry for #4691 --- changelog/4691.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4691.feature.rst diff --git a/changelog/4691.feature.rst b/changelog/4691.feature.rst new file mode 100644 index 000000000..8206d0569 --- /dev/null +++ b/changelog/4691.feature.rst @@ -0,0 +1 @@ +``pytest_terminal_summary`` hook now can also receive a ``config`` parameter. From 2d7582bd9204797ee01eea62a48e3913b4288274 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 29 Jan 2019 18:58:00 +0100 Subject: [PATCH 26/29] flip around basetemp folder reset to see if it helps on windows --- src/_pytest/tmpdir.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index b8c755ac6..843c8ca37 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -61,8 +61,9 @@ class TempPathFactory(object): """ return base temporary directory. """ if self._basetemp is None: if self._given_basetemp is not None: - basetemp = self._given_basetemp.resolve() + basetemp = self._given_basetemp ensure_reset_dir(basetemp) + basetemp = basetemp.resolve() else: from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT") temproot = Path(from_env or tempfile.gettempdir()).resolve() From eb92e575098f9e2e29bb0e69b173f794d9094498 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 29 Jan 2019 15:31:20 -0200 Subject: [PATCH 27/29] Show deprecation message when running under Python 2.7 and 3.4 Fix #4627 --- changelog/4627.feature.rst | 2 ++ src/_pytest/terminal.py | 15 +++++++++++++++ testing/acceptance_test.py | 4 +++- testing/deprecated_test.py | 19 +++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 changelog/4627.feature.rst diff --git a/changelog/4627.feature.rst b/changelog/4627.feature.rst new file mode 100644 index 000000000..22bb0618f --- /dev/null +++ b/changelog/4627.feature.rst @@ -0,0 +1,2 @@ +Display a message at the end of the test session when running under Python 2.7 and 3.4 that pytest 5.0 will no longer +support those Python versions. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 3c6504b66..3ee36b50b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -649,6 +649,7 @@ class TerminalReporter(object): self.summary_passes() # Display any extra warnings from teardown here (if any). self.summary_warnings() + self.summary_deprecated_python() def pytest_keyboard_interrupt(self, excinfo): self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) @@ -770,6 +771,20 @@ class TerminalReporter(object): self.write_sep("_", msg) self._outrep_summary(rep) + def summary_deprecated_python(self): + if sys.version_info[:2] <= (3, 4) and self.verbosity >= 0: + self.write_sep("=", "deprecated python version", yellow=True, bold=False) + using_version = ".".join(str(x) for x in sys.version_info[:3]) + self.line( + "You are using Python {}, which will no longer be supported in pytest 5.0".format( + using_version + ), + yellow=True, + bold=False, + ) + self.line("For more information, please read:") + self.line(" https://docs.pytest.org/en/latest/py27-py34-deprecation.html") + def print_teardown_sections(self, rep): showcapture = self.config.option.showcapture if showcapture == "no": diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index b7f914335..ff885a0c2 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -854,7 +854,9 @@ class TestDurations(object): result = testdir.runpytest("--durations=2") assert result.ret == 0 lines = result.stdout.get_lines_after("*slowest*durations*") - assert "4 passed" in lines[2] + # account for the "deprecated python version" header + index = 2 if sys.version_info[:2] > (3, 4) else 6 + assert "4 passed" in lines[index] def test_calls_showall(self, testdir): testdir.makepyfile(self.source) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index a6ef4739b..92cfcbff8 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -3,6 +3,7 @@ from __future__ import division from __future__ import print_function import os +import sys import pytest from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG @@ -219,3 +220,21 @@ def test_fixture_named_request(testdir): "*'request' is a reserved name for fixtures and will raise an error in future versions" ] ) + + +def test_python_deprecation(testdir): + result = testdir.runpytest() + python_ver = ".".join(str(x) for x in sys.version_info[:3]) + msg = "You are using Python {}, which will no longer be supported in pytest 5.0".format( + python_ver + ) + if sys.version_info[:2] <= (3, 4): + result.stdout.fnmatch_lines( + [ + msg, + "For more information, please read:", + " https://docs.pytest.org/en/latest/py27-py34-deprecation.html", + ] + ) + else: + assert msg not in result.stdout.str() From c0e6543b5a3233c1fcdf21eed346c33d15656ab8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 30 Jan 2019 09:36:54 -0200 Subject: [PATCH 28/29] Fix pytest_report_teststatus call to pass new config object (#4691) --- src/_pytest/skipping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 49676aa80..a28e95eda 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -285,7 +285,7 @@ def shower(stat): def _get_report_str(terminalreporter, report): _category, _short, verbose = terminalreporter.config.hook.pytest_report_teststatus( - report=report + report=report, config=terminalreporter.config ) return verbose From fa979a4290cdd6ba089091bf898ea65ff1bc2a65 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 30 Jan 2019 14:25:38 -0200 Subject: [PATCH 29/29] Preparing release version 4.2.0 --- CHANGELOG.rst | 78 +++++++++++++++++++++++++++++ changelog/3094.feature.rst | 5 -- changelog/3547.bugfix.rst | 2 - changelog/4280.bugfix.rst | 3 -- changelog/4402.bugfix.rst | 4 -- changelog/4536.bugfix.rst | 1 - changelog/4627.feature.rst | 2 - changelog/4643.trivial.rst | 3 -- changelog/4649.bugfix.rst | 1 - changelog/4653.bugfix.rst | 1 - changelog/4657.trivial.rst | 1 - changelog/4660.feature.rst | 1 - changelog/4667.bugfix.rst | 1 - changelog/4669.bugfix.rst | 1 - changelog/4680.bugfix.rst | 1 - changelog/4681.bugfix.rst | 1 - changelog/4688.feature.rst | 1 - changelog/4691.feature.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.2.0.rst | 37 ++++++++++++++ doc/en/assert.rst | 2 + doc/en/cache.rst | 14 ++++-- doc/en/capture.rst | 1 + doc/en/doctest.rst | 1 + doc/en/example/markers.rst | 56 ++++++++++++--------- doc/en/example/nonpython.rst | 6 ++- doc/en/example/parametrize.rst | 18 ++++--- doc/en/example/pythoncollection.rst | 3 ++ doc/en/example/reportingdemo.rst | 1 + doc/en/example/simple.rst | 16 ++++-- doc/en/fixture.rst | 15 +++--- doc/en/getting-started.rst | 1 + doc/en/index.rst | 1 + doc/en/parametrize.rst | 4 +- doc/en/skipping.rst | 1 + doc/en/tmpdir.rst | 2 + doc/en/unittest.rst | 1 + doc/en/usage.rst | 11 ++-- doc/en/warnings.rst | 1 + doc/en/writing_plugins.rst | 1 + 40 files changed, 222 insertions(+), 80 deletions(-) delete mode 100644 changelog/3094.feature.rst delete mode 100644 changelog/3547.bugfix.rst delete mode 100644 changelog/4280.bugfix.rst delete mode 100644 changelog/4402.bugfix.rst delete mode 100644 changelog/4536.bugfix.rst delete mode 100644 changelog/4627.feature.rst delete mode 100644 changelog/4643.trivial.rst delete mode 100644 changelog/4649.bugfix.rst delete mode 100644 changelog/4653.bugfix.rst delete mode 100644 changelog/4657.trivial.rst delete mode 100644 changelog/4660.feature.rst delete mode 100644 changelog/4667.bugfix.rst delete mode 100644 changelog/4669.bugfix.rst delete mode 100644 changelog/4680.bugfix.rst delete mode 100644 changelog/4681.bugfix.rst delete mode 100644 changelog/4688.feature.rst delete mode 100644 changelog/4691.feature.rst create mode 100644 doc/en/announce/release-4.2.0.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 02ea6e84f..76c947473 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,84 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.2.0 (2019-01-30) +========================= + +Features +-------- + +- `#3094 `_: `Class xunit-style `__ functions and methods + now obey the scope of *autouse* fixtures. + + This fixes a number of surprising issues like ``setup_method`` being called before session-scoped + autouse fixtures (see `#517 `__ for an example). + + +- `#4627 `_: Display a message at the end of the test session when running under Python 2.7 and 3.4 that pytest 5.0 will no longer + support those Python versions. + + +- `#4660 `_: The number of *selected* tests now are also displayed when the ``-k`` or ``-m`` flags are used. + + +- `#4688 `_: ``pytest_report_teststatus`` hook now can also receive a ``config`` parameter. + + +- `#4691 `_: ``pytest_terminal_summary`` hook now can also receive a ``config`` parameter. + + + +Bug Fixes +--------- + +- `#3547 `_: ``--junitxml`` can emit XML compatible with Jenkins xUnit. + ``junit_family`` INI option accepts ``legacy|xunit1``, which produces old style output, and ``xunit2`` that conforms more strictly to https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd + + +- `#4280 `_: Improve quitting from pdb, especially with ``--trace``. + + Using ``q[quit]`` after ``pdb.set_trace()`` will quit pytest also. + + +- `#4402 `_: Warning summary now groups warnings by message instead of by test id. + + This makes the output more compact and better conveys the general idea of how much code is + actually generating warnings, instead of how many tests call that code. + + +- `#4536 `_: ``monkeypatch.delattr`` handles class descriptors like ``staticmethod``/``classmethod``. + + +- `#4649 `_: Restore marks being considered keywords for keyword expressions. + + +- `#4653 `_: ``tmp_path`` fixture and other related ones provides resolved path (a.k.a real path) + + +- `#4667 `_: ``pytest_terminal_summary`` uses result from ``pytest_report_teststatus`` hook, rather than hardcoded strings. + + +- `#4669 `_: Correctly handle ``unittest.SkipTest`` exception containing non-ascii characters on Python 2. + + +- `#4680 `_: Ensure the ``tmpdir`` and the ``tmp_path`` fixtures are the same folder. + + +- `#4681 `_: Ensure ``tmp_path`` is always a real path. + + + +Trivial/Internal Changes +------------------------ + +- `#4643 `_: Use ``a.item()`` instead of the deprecated ``np.asscalar(a)`` in ``pytest.approx``. + + ``np.asscalar`` has been `deprecated `__ in ``numpy 1.16.``. + + +- `#4657 `_: Copy saferepr from pylib + + pytest 4.1.1 (2019-01-12) ========================= diff --git a/changelog/3094.feature.rst b/changelog/3094.feature.rst deleted file mode 100644 index 9004b6c0a..000000000 --- a/changelog/3094.feature.rst +++ /dev/null @@ -1,5 +0,0 @@ -`Class xunit-style `__ functions and methods -now obey the scope of *autouse* fixtures. - -This fixes a number of surprising issues like ``setup_method`` being called before session-scoped -autouse fixtures (see `#517 `__ for an example). diff --git a/changelog/3547.bugfix.rst b/changelog/3547.bugfix.rst deleted file mode 100644 index b2ec00364..000000000 --- a/changelog/3547.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -``--junitxml`` can emit XML compatible with Jenkins xUnit. -``junit_family`` INI option accepts ``legacy|xunit1``, which produces old style output, and ``xunit2`` that conforms more strictly to https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd diff --git a/changelog/4280.bugfix.rst b/changelog/4280.bugfix.rst deleted file mode 100644 index 4814823f1..000000000 --- a/changelog/4280.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Improve quitting from pdb, especially with ``--trace``. - -Using ``q[quit]`` after ``pdb.set_trace()`` will quit pytest also. diff --git a/changelog/4402.bugfix.rst b/changelog/4402.bugfix.rst deleted file mode 100644 index 9b338aaa5..000000000 --- a/changelog/4402.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -Warning summary now groups warnings by message instead of by test id. - -This makes the output more compact and better conveys the general idea of how much code is -actually generating warnings, instead of how many tests call that code. diff --git a/changelog/4536.bugfix.rst b/changelog/4536.bugfix.rst deleted file mode 100644 index 0ec84a62b..000000000 --- a/changelog/4536.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``monkeypatch.delattr`` handles class descriptors like ``staticmethod``/``classmethod``. diff --git a/changelog/4627.feature.rst b/changelog/4627.feature.rst deleted file mode 100644 index 22bb0618f..000000000 --- a/changelog/4627.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Display a message at the end of the test session when running under Python 2.7 and 3.4 that pytest 5.0 will no longer -support those Python versions. diff --git a/changelog/4643.trivial.rst b/changelog/4643.trivial.rst deleted file mode 100644 index 75385f4b3..000000000 --- a/changelog/4643.trivial.rst +++ /dev/null @@ -1,3 +0,0 @@ -Use ``a.item()`` instead of the deprecated ``np.asscalar(a)`` in ``pytest.approx``. - -``np.asscalar`` has been `deprecated `__ in ``numpy 1.16.``. diff --git a/changelog/4649.bugfix.rst b/changelog/4649.bugfix.rst deleted file mode 100644 index 74b241781..000000000 --- a/changelog/4649.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Restore marks being considered keywords for keyword expressions. diff --git a/changelog/4653.bugfix.rst b/changelog/4653.bugfix.rst deleted file mode 100644 index 5b5b36745..000000000 --- a/changelog/4653.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``tmp_path`` fixture and other related ones provides resolved path (a.k.a real path) diff --git a/changelog/4657.trivial.rst b/changelog/4657.trivial.rst deleted file mode 100644 index abdab08eb..000000000 --- a/changelog/4657.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Copy saferepr from pylib diff --git a/changelog/4660.feature.rst b/changelog/4660.feature.rst deleted file mode 100644 index a8ae5213b..000000000 --- a/changelog/4660.feature.rst +++ /dev/null @@ -1 +0,0 @@ -The number of *selected* tests now are also displayed when the ``-k`` or ``-m`` flags are used. diff --git a/changelog/4667.bugfix.rst b/changelog/4667.bugfix.rst deleted file mode 100644 index ac2d8567c..000000000 --- a/changelog/4667.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``pytest_terminal_summary`` uses result from ``pytest_report_teststatus`` hook, rather than hardcoded strings. diff --git a/changelog/4669.bugfix.rst b/changelog/4669.bugfix.rst deleted file mode 100644 index e5c18353c..000000000 --- a/changelog/4669.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Correctly handle ``unittest.SkipTest`` exception containing non-ascii characters on Python 2. diff --git a/changelog/4680.bugfix.rst b/changelog/4680.bugfix.rst deleted file mode 100644 index 9126a70ea..000000000 --- a/changelog/4680.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Ensure the ``tmpdir`` and the ``tmp_path`` fixtures are the same folder. diff --git a/changelog/4681.bugfix.rst b/changelog/4681.bugfix.rst deleted file mode 100644 index 3ece27397..000000000 --- a/changelog/4681.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Ensure ``tmp_path`` is always a real path. diff --git a/changelog/4688.feature.rst b/changelog/4688.feature.rst deleted file mode 100644 index 966c224a5..000000000 --- a/changelog/4688.feature.rst +++ /dev/null @@ -1 +0,0 @@ -``pytest_report_teststatus`` hook now can also receive a ``config`` parameter. diff --git a/changelog/4691.feature.rst b/changelog/4691.feature.rst deleted file mode 100644 index 8206d0569..000000000 --- a/changelog/4691.feature.rst +++ /dev/null @@ -1 +0,0 @@ -``pytest_terminal_summary`` hook now can also receive a ``config`` parameter. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 51a3227ea..96807c438 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.2.0 release-4.1.1 release-4.1.0 release-4.0.2 diff --git a/doc/en/announce/release-4.2.0.rst b/doc/en/announce/release-4.2.0.rst new file mode 100644 index 000000000..6c262c1e0 --- /dev/null +++ b/doc/en/announce/release-4.2.0.rst @@ -0,0 +1,37 @@ +pytest-4.2.0 +======================================= + +The pytest team is proud to announce the 4.2.0 release! + +pytest is a mature Python testing tool with more than a 2000 tests +against itself, passing on many different interpreters and platforms. + +This release contains a number of bugs fixes and improvements, so users are encouraged +to take a look at the CHANGELOG: + + https://docs.pytest.org/en/latest/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/latest/ + +As usual, you can upgrade from pypi via: + + pip install -U pytest + +Thanks to all who contributed to this release, among them: + +* Adam Uhlir +* Anthony Sottile +* Bruno Oliveira +* Christopher Dignam +* Daniel Hahler +* Joseph Hunkeler +* Kristoffer Nordstroem +* Ronny Pfannschmidt +* Thomas Hisch +* wim glenn + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/assert.rst b/doc/en/assert.rst index b13a071f6..110d00738 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -29,6 +29,7 @@ you will see the return value of the function call: $ pytest test_assert1.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item @@ -173,6 +174,7 @@ if you run this module: $ pytest test_assert2.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 2d2d67047..0cb42e6e9 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -81,8 +81,9 @@ If you then run it with ``--lf``: $ pytest --lf =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: - collected 50 items / 48 deselected + collected 50 items / 48 deselected / 2 selected run-last-failure: rerun previous 2 failures test_50.py FF [100%] @@ -124,6 +125,7 @@ of ``FF`` and dots): $ pytest --ff =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 50 items run-last-failure: rerun previous 2 failures first @@ -257,11 +259,17 @@ You can always peek at the content of the cache using the $ pytest --cache-show =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: - cachedir: $REGENDOC_TMPDIR/.pytest_cache + cachedir: $PYTHON_PREFIX/.pytest_cache ------------------------------- cache values ------------------------------- cache/lastfailed contains: - {'test_caching.py::test_function': True} + {'test_50.py::test_num[17]': True, + 'test_50.py::test_num[25]': True, + 'test_assert1.py::test_function': True, + 'test_assert2.py::test_set_comparison': True, + 'test_caching.py::test_function': True, + 'test_foocompare.py::test_compare': True} cache/nodeids contains: ['test_caching.py::test_function'] cache/stepwise contains: diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 488b2b874..78390034a 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -68,6 +68,7 @@ of the failing function and hide the other one: $ pytest =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 125ed3aa7..5aadc1112 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -65,6 +65,7 @@ then you can just invoke ``pytest`` without command line options: $ pytest =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 1 item diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 9d325c30e..864c1e808 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -33,10 +33,10 @@ You can then restrict a test run to only run tests marked with ``webtest``: $ pytest -v -m webtest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: - collecting ... collected 4 items / 3 deselected + collecting ... collected 4 items / 3 deselected / 1 selected test_server.py::test_send_http PASSED [100%] @@ -48,10 +48,10 @@ Or the inverse, running all tests except the webtest ones: $ pytest -v -m "not webtest" =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: - collecting ... collected 4 items / 1 deselected + collecting ... collected 4 items / 1 deselected / 3 selected test_server.py::test_something_quick PASSED [ 33%] test_server.py::test_another PASSED [ 66%] @@ -70,8 +70,8 @@ tests based on their module, class, method, or function name: $ pytest -v test_server.py::TestClass::test_method =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 1 item @@ -85,8 +85,8 @@ You can also select on the class: $ pytest -v test_server.py::TestClass =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 1 item @@ -100,8 +100,8 @@ Or select multiple nodes: $ pytest -v test_server.py::TestClass test_server.py::test_send_http =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 2 items @@ -140,10 +140,10 @@ select tests based on their names: $ pytest -v -k http # running with the above defined example module =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: - collecting ... collected 4 items / 3 deselected + collecting ... collected 4 items / 3 deselected / 1 selected test_server.py::test_send_http PASSED [100%] @@ -155,10 +155,10 @@ And you can also run all tests except the ones that match the keyword: $ pytest -k "not send_http" -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: - collecting ... collected 4 items / 1 deselected + collecting ... collected 4 items / 1 deselected / 3 selected test_server.py::test_something_quick PASSED [ 33%] test_server.py::test_another PASSED [ 66%] @@ -172,10 +172,10 @@ Or to select "http" and "quick" tests: $ pytest -k "http or quick" -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: - collecting ... collected 4 items / 2 deselected + collecting ... collected 4 items / 2 deselected / 2 selected test_server.py::test_send_http PASSED [ 50%] test_server.py::test_something_quick PASSED [100%] @@ -365,6 +365,7 @@ the test needs: $ pytest -E stage2 =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item @@ -379,6 +380,7 @@ and here is one that specifies exactly the environment needed: $ pytest -E stage1 =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item @@ -546,12 +548,13 @@ then you will see two tests skipped and two executed tests as expected: $ pytest -rs # this option reports skip reasons =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 4 items test_plat.py s.s. [100%] ========================= short test summary info ========================== - SKIP [2] $REGENDOC_TMPDIR/conftest.py:12: cannot run on platform linux + SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:12: cannot run on platform linux =================== 2 passed, 2 skipped in 0.12 seconds ==================== @@ -562,8 +565,9 @@ Note that if you specify a platform via the marker-command line option like this $ pytest -m linux =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: - collected 4 items / 3 deselected + collected 4 items / 3 deselected / 1 selected test_plat.py . [100%] @@ -615,8 +619,9 @@ We can now use the ``-m option`` to select one set: $ pytest -m interface --tb=short =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: - collected 4 items / 2 deselected + collected 4 items / 2 deselected / 2 selected test_module.py FF [100%] @@ -638,8 +643,9 @@ or to select both "event" and "interface" tests: $ pytest -m "interface or event" --tb=short =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: - collected 4 items / 1 deselected + collected 4 items / 1 deselected / 3 selected test_module.py FFF [100%] diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index eba8279f3..bf7173ee5 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -30,6 +30,7 @@ now execute the test specification: nonpython $ pytest test_simple.yml =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR/nonpython, inifile: collected 2 items @@ -63,8 +64,8 @@ consulted when reporting in ``verbose`` mode: nonpython $ pytest -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR/nonpython, inifile: collecting ... collected 2 items @@ -88,6 +89,7 @@ interesting to just look at the collection tree: nonpython $ pytest --collect-only =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR/nonpython, inifile: collected 2 items diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 98aaeae3b..787bbe10c 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -145,6 +145,7 @@ objects, they are still using the default pytest representation: $ pytest test_time.py --collect-only =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 8 items @@ -203,6 +204,7 @@ this is a fully self-contained example which you can run with: $ pytest test_scenarios.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 4 items @@ -217,6 +219,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia $ pytest --collect-only test_scenarios.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 4 items @@ -283,6 +286,7 @@ Let's first see how it looks like at collection time: $ pytest test_backends.py --collect-only =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items @@ -348,6 +352,7 @@ The result of this test will be successful: $ pytest test_indirect_list.py --collect-only =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item @@ -433,7 +438,7 @@ Running it results in some skips if we don't have all the python interpreters in . $ pytest -rs -q multipython.py ...sss...sssssssss...sss... [100%] ========================= short test summary info ========================== - SKIP [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.4' not found + SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.4' not found 12 passed, 15 skipped in 0.12 seconds Indirect parametrization of optional implementations/imports @@ -484,12 +489,13 @@ If you run this with reporting for skips enabled: $ pytest -rs test_module.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items test_module.py .s [100%] ========================= short test summary info ========================== - SKIP [1] $REGENDOC_TMPDIR/conftest.py:11: could not import 'opt2' + SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:11: could not import 'opt2' =================== 1 passed, 1 skipped in 0.12 seconds ==================== @@ -540,14 +546,14 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker: $ pytest -v -m basic =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: - collecting ... collected 17 items / 14 deselected + collecting ... collected 17 items / 14 deselected / 3 selected test_pytest_param_example.py::test_eval[1+7-8] PASSED [ 33%] test_pytest_param_example.py::test_eval[basic_2+4] PASSED [ 66%] - test_pytest_param_example.py::test_eval[basic_6*9] xfail [100%] + test_pytest_param_example.py::test_eval[basic_6*9] XFAIL [100%] ============ 2 passed, 14 deselected, 1 xfailed in 0.12 seconds ============ diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 394924e2d..8dcaa97d7 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -132,6 +132,7 @@ The test collection would look like this: $ pytest --collect-only =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 2 items @@ -187,6 +188,7 @@ You can always peek at the collection tree without running tests like this: . $ pytest --collect-only pythoncollection.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 3 items @@ -259,6 +261,7 @@ file will be left out: $ pytest --collect-only =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 0 items diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 15d71caa0..9fcc72ffe 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -14,6 +14,7 @@ get on the terminal - we are working on that): assertion $ pytest failure_demo.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR/assertion, inifile: collected 44 items diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 76a1ddc80..5904ea5a1 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -128,6 +128,7 @@ directory with the above conftest.py: $ pytest =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 0 items @@ -188,12 +189,13 @@ and when running it will see a skipped "slow" test: $ pytest -rs # "-rs" means report details on the little 's' =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items test_module.py .s [100%] ========================= short test summary info ========================== - SKIP [1] test_module.py:8: need --runslow option to run + SKIPPED [1] test_module.py:8: need --runslow option to run =================== 1 passed, 1 skipped in 0.12 seconds ==================== @@ -204,6 +206,7 @@ Or run it including the ``slow`` marked test: $ pytest --runslow =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items @@ -346,6 +349,7 @@ which will add the string to the test header accordingly: $ pytest =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache project deps: mylib-1.1 rootdir: $REGENDOC_TMPDIR, inifile: collected 0 items @@ -373,8 +377,8 @@ which will add info only when run with "--v": $ pytest -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache info1: did you know that ... did you? rootdir: $REGENDOC_TMPDIR, inifile: @@ -389,6 +393,7 @@ and nothing when run plainly: $ pytest =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 0 items @@ -428,6 +433,7 @@ Now we can profile which test functions execute the slowest: $ pytest --durations=3 =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 3 items @@ -502,6 +508,7 @@ If we run this: $ pytest -rx =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 4 items @@ -585,6 +592,7 @@ We can run this: $ pytest =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 7 items @@ -698,6 +706,7 @@ and run them: $ pytest test_module.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items @@ -799,6 +808,7 @@ and run it: $ pytest -s test_module.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 3 items diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 4dd68f8e4..4c8e24b9c 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -73,6 +73,7 @@ marked ``smtp_connection`` fixture function. Running the test looks like this: $ pytest test_smtpsimple.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item @@ -213,6 +214,7 @@ inspect what is going on and can now run the tests: $ pytest test_module.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items @@ -701,6 +703,7 @@ Running the above tests results in the following test IDs being used: $ pytest --collect-only =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 10 items @@ -744,8 +747,8 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``: $ pytest test_fixture_marks.py -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 3 items @@ -789,8 +792,8 @@ Here we declare an ``app`` fixture which receives the previously defined $ pytest -v test_appsetup.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 2 items @@ -860,8 +863,8 @@ Let's run the tests in verbose mode and with looking at the print-output: $ pytest -v -s test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 - cachedir: .pytest_cache + platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 8 items diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 500fc3d93..a9f7d1d11 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -47,6 +47,7 @@ That’s it. You can now execute the test function: $ pytest =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item diff --git a/doc/en/index.rst b/doc/en/index.rst index 7c201fbd7..000793d27 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -29,6 +29,7 @@ To execute it: $ pytest =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index 099b531c2..d1d23c67f 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -57,6 +57,7 @@ them in turn: $ pytest =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 3 items @@ -108,6 +109,7 @@ Let's run this: $ pytest =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 3 items @@ -207,7 +209,7 @@ list: $ pytest -q -rs test_strings.py s [100%] ========================= short test summary info ========================== - SKIP [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:1 + SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:1 1 skipped in 0.12 seconds Note that when calling ``metafunc.parametrize`` multiple times with different parameter sets, all parameter names across diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index ae1dc7149..dd0b57110 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -330,6 +330,7 @@ Running it with the report-on-xfail option gives this output: example $ pytest -rx xfail_demo.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR/example, inifile: collected 7 items diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index 8c21e17e5..3d73d6147 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -42,6 +42,7 @@ Running this would result in a passed test except for the last $ pytest test_tmp_path.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item @@ -104,6 +105,7 @@ Running this would result in a passed test except for the last $ pytest test_tmpdir.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item diff --git a/doc/en/unittest.rst b/doc/en/unittest.rst index 34c8a35db..7eb92bf43 100644 --- a/doc/en/unittest.rst +++ b/doc/en/unittest.rst @@ -129,6 +129,7 @@ the ``self.db`` values in the traceback: $ pytest test_unittest_db.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 3ff6a0dd5..b894e0fd4 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -193,6 +193,7 @@ Example: $ pytest -ra =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 6 items @@ -216,12 +217,12 @@ Example: test_example.py:14: AssertionError ========================= short test summary info ========================== - SKIP [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test + SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test XFAIL test_example.py::test_xfail reason: xfailing this test XPASS test_example.py::test_xpass always xfail ERROR test_example.py::test_error - FAIL test_example.py::test_fail + FAILED test_example.py::test_fail 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds The ``-r`` options accepts a number of characters after it, with ``a`` used above meaning "all except passes". @@ -244,6 +245,7 @@ More than one character can be used, so for example to only see failed and skipp $ pytest -rfs =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 6 items @@ -267,8 +269,8 @@ More than one character can be used, so for example to only see failed and skipp test_example.py:14: AssertionError ========================= short test summary info ========================== - FAIL test_example.py::test_fail - SKIP [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test + FAILED test_example.py::test_fail + SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had @@ -279,6 +281,7 @@ captured output: $ pytest -rpP =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 6 items diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 421491086..8f0244aea 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -25,6 +25,7 @@ Running pytest now produces this output: $ pytest test_show_warnings.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 70bf315aa..bc1bcda0e 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -413,6 +413,7 @@ additionally it is possible to copy examples for an example folder before runnin $ pytest =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y + cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 2 items