From 62381125e74e62e051176afe602feb937dabcef8 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 19 Aug 2019 15:57:39 -0400 Subject: [PATCH 01/27] Fix self reference in function scoped fixtures --- AUTHORS | 1 + changelog/2270.bugfix.rst | 1 + src/_pytest/fixtures.py | 4 ++++ testing/python/fixtures.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 35 insertions(+) create mode 100644 changelog/2270.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 88bbfe352..ad0c5c07b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -209,6 +209,7 @@ Raphael Castaneda Raphael Pierzina Raquel Alegre Ravi Chandra +Robert Holt Roberto Polli Roland Puntaier Romain Dorgueil diff --git a/changelog/2270.bugfix.rst b/changelog/2270.bugfix.rst new file mode 100644 index 000000000..45835deb6 --- /dev/null +++ b/changelog/2270.bugfix.rst @@ -0,0 +1 @@ +Fix ``self`` reference in function scoped fixtures that are in a plugin class diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d5f9ad2d3..0640ebc90 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -897,6 +897,10 @@ def resolve_fixture_function(fixturedef, request): # request.instance so that code working with "fixturedef" behaves # as expected. if request.instance is not None: + if hasattr(fixturefunc, "__self__") and not isinstance( + request.instance, fixturefunc.__self__.__class__ + ): + return fixturefunc fixturefunc = getimfunc(fixturedef.func) if fixturefunc != fixturedef.func: fixturefunc = fixturefunc.__get__(request.instance) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 1f383e752..ee050b909 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3945,6 +3945,35 @@ class TestScopeOrdering: reprec = testdir.inline_run() reprec.assertoutcome(passed=2) + def test_class_fixture_self_instance(self, testdir): + testdir.makeconftest( + """ + import pytest + + def pytest_configure(config): + config.pluginmanager.register(MyPlugin()) + + class MyPlugin(): + def __init__(self): + self.arg = 1 + + @pytest.fixture(scope='function') + def myfix(self): + assert isinstance(self, MyPlugin) + return self.arg + """ + ) + + testdir.makepyfile( + """ + class TestClass(object): + def test_1(self, myfix): + assert myfix == 1 + """ + ) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + def test_call_fixture_function_error(): """Check if an error is raised if a fixture function is called directly (#4545)""" From 3c82b1cb976d77870f80f05f980be8a1d6f77162 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 26 Aug 2019 10:47:21 -0300 Subject: [PATCH 02/27] Refactor report serialization/deserialization code Refactoring this in order to support chained exceptions more easily. Related to #5786 --- src/_pytest/reports.py | 205 +++++++++++++++++++++++------------------ 1 file changed, 116 insertions(+), 89 deletions(-) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 4682d5b6e..b87277c33 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -160,46 +160,7 @@ class BaseReport: Experimental method. """ - - def disassembled_report(rep): - reprtraceback = rep.longrepr.reprtraceback.__dict__.copy() - reprcrash = rep.longrepr.reprcrash.__dict__.copy() - - new_entries = [] - for entry in reprtraceback["reprentries"]: - entry_data = { - "type": type(entry).__name__, - "data": entry.__dict__.copy(), - } - for key, value in entry_data["data"].items(): - if hasattr(value, "__dict__"): - entry_data["data"][key] = value.__dict__.copy() - new_entries.append(entry_data) - - reprtraceback["reprentries"] = new_entries - - return { - "reprcrash": reprcrash, - "reprtraceback": reprtraceback, - "sections": rep.longrepr.sections, - } - - d = self.__dict__.copy() - if hasattr(self.longrepr, "toterminal"): - if hasattr(self.longrepr, "reprtraceback") and hasattr( - self.longrepr, "reprcrash" - ): - d["longrepr"] = disassembled_report(self) - else: - d["longrepr"] = str(self.longrepr) - else: - d["longrepr"] = self.longrepr - for name in d: - if isinstance(d[name], (py.path.local, Path)): - d[name] = str(d[name]) - elif name == "result": - d[name] = None # for now - return d + return _test_report_to_json(self) @classmethod def _from_json(cls, reportdict): @@ -211,55 +172,8 @@ class BaseReport: Experimental method. """ - if reportdict["longrepr"]: - if ( - "reprcrash" in reportdict["longrepr"] - and "reprtraceback" in reportdict["longrepr"] - ): - - reprtraceback = reportdict["longrepr"]["reprtraceback"] - reprcrash = reportdict["longrepr"]["reprcrash"] - - unserialized_entries = [] - reprentry = None - for entry_data in reprtraceback["reprentries"]: - data = entry_data["data"] - entry_type = entry_data["type"] - if entry_type == "ReprEntry": - reprfuncargs = None - reprfileloc = None - reprlocals = None - if data["reprfuncargs"]: - reprfuncargs = ReprFuncArgs(**data["reprfuncargs"]) - if data["reprfileloc"]: - reprfileloc = ReprFileLocation(**data["reprfileloc"]) - if data["reprlocals"]: - reprlocals = ReprLocals(data["reprlocals"]["lines"]) - - reprentry = ReprEntry( - lines=data["lines"], - reprfuncargs=reprfuncargs, - reprlocals=reprlocals, - filelocrepr=reprfileloc, - style=data["style"], - ) - elif entry_type == "ReprEntryNative": - reprentry = ReprEntryNative(data["lines"]) - else: - _report_unserialization_failure(entry_type, cls, reportdict) - unserialized_entries.append(reprentry) - reprtraceback["reprentries"] = unserialized_entries - - exception_info = ReprExceptionInfo( - reprtraceback=ReprTraceback(**reprtraceback), - reprcrash=ReprFileLocation(**reprcrash), - ) - - for section in reportdict["longrepr"]["sections"]: - exception_info.addsection(*section) - reportdict["longrepr"] = exception_info - - return cls(**reportdict) + kwargs = _test_report_kwargs_from_json(reportdict) + return cls(**kwargs) def _report_unserialization_failure(type_name, report_class, reportdict): @@ -424,3 +338,116 @@ def pytest_report_from_serializable(data): assert False, "Unknown report_type unserialize data: {}".format( data["_report_type"] ) + + +def _test_report_to_json(test_report): + """ + This was originally the serialize_report() function from xdist (ca03269). + + Returns the contents of this report as a dict of builtin entries, suitable for + serialization. + """ + + def serialize_repr_entry(entry): + entry_data = {"type": type(entry).__name__, "data": entry.__dict__.copy()} + for key, value in entry_data["data"].items(): + if hasattr(value, "__dict__"): + entry_data["data"][key] = value.__dict__.copy() + return entry_data + + def serialize_repr_traceback(reprtraceback): + result = reprtraceback.__dict__.copy() + result["reprentries"] = [ + serialize_repr_entry(x) for x in reprtraceback.reprentries + ] + return result + + def serialize_repr_crash(reprcrash): + return reprcrash.__dict__.copy() + + def serialize_longrepr(rep): + return { + "reprcrash": serialize_repr_crash(rep.longrepr.reprcrash), + "reprtraceback": serialize_repr_traceback(rep.longrepr.reprtraceback), + "sections": rep.longrepr.sections, + } + + d = test_report.__dict__.copy() + if hasattr(test_report.longrepr, "toterminal"): + if hasattr(test_report.longrepr, "reprtraceback") and hasattr( + test_report.longrepr, "reprcrash" + ): + d["longrepr"] = serialize_longrepr(test_report) + else: + d["longrepr"] = str(test_report.longrepr) + else: + d["longrepr"] = test_report.longrepr + for name in d: + if isinstance(d[name], (py.path.local, Path)): + d[name] = str(d[name]) + elif name == "result": + d[name] = None # for now + return d + + +def _test_report_kwargs_from_json(reportdict): + """ + This was originally the serialize_report() function from xdist (ca03269). + + Factory method that returns either a TestReport or CollectReport, depending on the calling + class. It's the callers responsibility to know which class to pass here. + """ + + def deserialize_repr_entry(entry_data): + data = entry_data["data"] + entry_type = entry_data["type"] + if entry_type == "ReprEntry": + reprfuncargs = None + reprfileloc = None + reprlocals = None + if data["reprfuncargs"]: + reprfuncargs = ReprFuncArgs(**data["reprfuncargs"]) + if data["reprfileloc"]: + reprfileloc = ReprFileLocation(**data["reprfileloc"]) + if data["reprlocals"]: + reprlocals = ReprLocals(data["reprlocals"]["lines"]) + + reprentry = ReprEntry( + lines=data["lines"], + reprfuncargs=reprfuncargs, + reprlocals=reprlocals, + filelocrepr=reprfileloc, + style=data["style"], + ) + elif entry_type == "ReprEntryNative": + reprentry = ReprEntryNative(data["lines"]) + else: + _report_unserialization_failure(entry_type, TestReport, reportdict) + return reprentry + + def deserialize_repr_traceback(repr_traceback_dict): + repr_traceback_dict["reprentries"] = [ + deserialize_repr_entry(x) for x in repr_traceback_dict["reprentries"] + ] + return ReprTraceback(**repr_traceback_dict) + + def deserialize_repr_crash(repr_crash_dict): + return ReprFileLocation(**repr_crash_dict) + + if ( + reportdict["longrepr"] + and "reprcrash" in reportdict["longrepr"] + and "reprtraceback" in reportdict["longrepr"] + ): + exception_info = ReprExceptionInfo( + reprtraceback=deserialize_repr_traceback( + reportdict["longrepr"]["reprtraceback"] + ), + reprcrash=deserialize_repr_crash(reportdict["longrepr"]["reprcrash"]), + ) + + for section in reportdict["longrepr"]["sections"]: + exception_info.addsection(*section) + reportdict["longrepr"] = exception_info + + return reportdict From 7a693654869df8c318bc80f51f1f2a631de63d6a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 26 Aug 2019 11:32:57 -0300 Subject: [PATCH 03/27] Move TWMock class to a fixture Using a relative import like before was not very nice --- testing/code/test_code.py | 10 +- testing/code/test_excinfo.py | 354 ++++++++++++++++------------------- testing/conftest.py | 33 ++++ 3 files changed, 194 insertions(+), 203 deletions(-) diff --git a/testing/code/test_code.py b/testing/code/test_code.py index d45795967..2f55720b4 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -1,8 +1,6 @@ import sys from unittest import mock -from test_excinfo import TWMock - import _pytest._code import pytest @@ -168,17 +166,15 @@ class TestTracebackEntry: class TestReprFuncArgs: - def test_not_raise_exception_with_mixed_encoding(self): + def test_not_raise_exception_with_mixed_encoding(self, tw_mock): from _pytest._code.code import ReprFuncArgs - tw = TWMock() - args = [("unicode_string", "São Paulo"), ("utf8_string", b"S\xc3\xa3o Paulo")] r = ReprFuncArgs(args) - r.toterminal(tw) + r.toterminal(tw_mock) assert ( - tw.lines[0] + tw_mock.lines[0] == r"unicode_string = São Paulo, utf8_string = b'S\xc3\xa3o Paulo'" ) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 7742b4da9..bdd7a5a6f 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -31,33 +31,6 @@ def limited_recursion_depth(): sys.setrecursionlimit(before) -class TWMock: - WRITE = object() - - def __init__(self): - self.lines = [] - self.is_writing = False - - def sep(self, sep, line=None): - self.lines.append((sep, line)) - - def write(self, msg, **kw): - self.lines.append((TWMock.WRITE, msg)) - - def line(self, line, **kw): - self.lines.append(line) - - def markup(self, text, **kw): - return text - - def get_write_msg(self, idx): - flag, msg = self.lines[idx] - assert flag == TWMock.WRITE - return msg - - fullwidth = 80 - - def test_excinfo_simple() -> None: try: raise ValueError @@ -658,7 +631,7 @@ raise ValueError() assert loc.lineno == 3 # assert loc.message == "ValueError: hello" - def test_repr_tracebackentry_lines2(self, importasmod): + def test_repr_tracebackentry_lines2(self, importasmod, tw_mock): mod = importasmod( """ def func1(m, x, y, z): @@ -678,13 +651,12 @@ raise ValueError() p = FormattedExcinfo(funcargs=True) repr_entry = p.repr_traceback_entry(entry) assert repr_entry.reprfuncargs.args == reprfuncargs.args - tw = TWMock() - repr_entry.toterminal(tw) - assert tw.lines[0] == "m = " + repr("m" * 90) - assert tw.lines[1] == "x = 5, y = 13" - assert tw.lines[2] == "z = " + repr("z" * 120) + repr_entry.toterminal(tw_mock) + assert tw_mock.lines[0] == "m = " + repr("m" * 90) + assert tw_mock.lines[1] == "x = 5, y = 13" + assert tw_mock.lines[2] == "z = " + repr("z" * 120) - def test_repr_tracebackentry_lines_var_kw_args(self, importasmod): + def test_repr_tracebackentry_lines_var_kw_args(self, importasmod, tw_mock): mod = importasmod( """ def func1(x, *y, **z): @@ -703,9 +675,8 @@ raise ValueError() p = FormattedExcinfo(funcargs=True) repr_entry = p.repr_traceback_entry(entry) assert repr_entry.reprfuncargs.args == reprfuncargs.args - tw = TWMock() - repr_entry.toterminal(tw) - assert tw.lines[0] == "x = 'a', y = ('b',), z = {'c': 'd'}" + repr_entry.toterminal(tw_mock) + assert tw_mock.lines[0] == "x = 'a', y = ('b',), z = {'c': 'd'}" def test_repr_tracebackentry_short(self, importasmod): mod = importasmod( @@ -842,7 +813,7 @@ raise ValueError() assert p._makepath(__file__) == __file__ p.repr_traceback(excinfo) - def test_repr_excinfo_addouterr(self, importasmod): + def test_repr_excinfo_addouterr(self, importasmod, tw_mock): mod = importasmod( """ def entry(): @@ -852,10 +823,9 @@ raise ValueError() excinfo = pytest.raises(ValueError, mod.entry) repr = excinfo.getrepr() repr.addsection("title", "content") - twmock = TWMock() - repr.toterminal(twmock) - assert twmock.lines[-1] == "content" - assert twmock.lines[-2] == ("-", "title") + repr.toterminal(tw_mock) + assert tw_mock.lines[-1] == "content" + assert tw_mock.lines[-2] == ("-", "title") def test_repr_excinfo_reprcrash(self, importasmod): mod = importasmod( @@ -920,7 +890,7 @@ raise ValueError() x = str(MyRepr()) assert x == "я" - def test_toterminal_long(self, importasmod): + def test_toterminal_long(self, importasmod, tw_mock): mod = importasmod( """ def g(x): @@ -932,27 +902,26 @@ raise ValueError() excinfo = pytest.raises(ValueError, mod.f) excinfo.traceback = excinfo.traceback.filter() repr = excinfo.getrepr() - tw = TWMock() - repr.toterminal(tw) - assert tw.lines[0] == "" - tw.lines.pop(0) - assert tw.lines[0] == " def f():" - assert tw.lines[1] == "> g(3)" - assert tw.lines[2] == "" - line = tw.get_write_msg(3) + repr.toterminal(tw_mock) + assert tw_mock.lines[0] == "" + tw_mock.lines.pop(0) + assert tw_mock.lines[0] == " def f():" + assert tw_mock.lines[1] == "> g(3)" + assert tw_mock.lines[2] == "" + line = tw_mock.get_write_msg(3) assert line.endswith("mod.py") - assert tw.lines[4] == (":5: ") - assert tw.lines[5] == ("_ ", None) - assert tw.lines[6] == "" - assert tw.lines[7] == " def g(x):" - assert tw.lines[8] == "> raise ValueError(x)" - assert tw.lines[9] == "E ValueError: 3" - assert tw.lines[10] == "" - line = tw.get_write_msg(11) + assert tw_mock.lines[4] == (":5: ") + assert tw_mock.lines[5] == ("_ ", None) + assert tw_mock.lines[6] == "" + assert tw_mock.lines[7] == " def g(x):" + assert tw_mock.lines[8] == "> raise ValueError(x)" + assert tw_mock.lines[9] == "E ValueError: 3" + assert tw_mock.lines[10] == "" + line = tw_mock.get_write_msg(11) assert line.endswith("mod.py") - assert tw.lines[12] == ":3: ValueError" + assert tw_mock.lines[12] == ":3: ValueError" - def test_toterminal_long_missing_source(self, importasmod, tmpdir): + def test_toterminal_long_missing_source(self, importasmod, tmpdir, tw_mock): mod = importasmod( """ def g(x): @@ -965,25 +934,24 @@ raise ValueError() tmpdir.join("mod.py").remove() excinfo.traceback = excinfo.traceback.filter() repr = excinfo.getrepr() - tw = TWMock() - repr.toterminal(tw) - assert tw.lines[0] == "" - tw.lines.pop(0) - assert tw.lines[0] == "> ???" - assert tw.lines[1] == "" - line = tw.get_write_msg(2) + repr.toterminal(tw_mock) + assert tw_mock.lines[0] == "" + tw_mock.lines.pop(0) + assert tw_mock.lines[0] == "> ???" + assert tw_mock.lines[1] == "" + line = tw_mock.get_write_msg(2) assert line.endswith("mod.py") - assert tw.lines[3] == ":5: " - assert tw.lines[4] == ("_ ", None) - assert tw.lines[5] == "" - assert tw.lines[6] == "> ???" - assert tw.lines[7] == "E ValueError: 3" - assert tw.lines[8] == "" - line = tw.get_write_msg(9) + assert tw_mock.lines[3] == ":5: " + assert tw_mock.lines[4] == ("_ ", None) + assert tw_mock.lines[5] == "" + assert tw_mock.lines[6] == "> ???" + assert tw_mock.lines[7] == "E ValueError: 3" + assert tw_mock.lines[8] == "" + line = tw_mock.get_write_msg(9) assert line.endswith("mod.py") - assert tw.lines[10] == ":3: ValueError" + assert tw_mock.lines[10] == ":3: ValueError" - def test_toterminal_long_incomplete_source(self, importasmod, tmpdir): + def test_toterminal_long_incomplete_source(self, importasmod, tmpdir, tw_mock): mod = importasmod( """ def g(x): @@ -996,25 +964,24 @@ raise ValueError() tmpdir.join("mod.py").write("asdf") excinfo.traceback = excinfo.traceback.filter() repr = excinfo.getrepr() - tw = TWMock() - repr.toterminal(tw) - assert tw.lines[0] == "" - tw.lines.pop(0) - assert tw.lines[0] == "> ???" - assert tw.lines[1] == "" - line = tw.get_write_msg(2) + repr.toterminal(tw_mock) + assert tw_mock.lines[0] == "" + tw_mock.lines.pop(0) + assert tw_mock.lines[0] == "> ???" + assert tw_mock.lines[1] == "" + line = tw_mock.get_write_msg(2) assert line.endswith("mod.py") - assert tw.lines[3] == ":5: " - assert tw.lines[4] == ("_ ", None) - assert tw.lines[5] == "" - assert tw.lines[6] == "> ???" - assert tw.lines[7] == "E ValueError: 3" - assert tw.lines[8] == "" - line = tw.get_write_msg(9) + assert tw_mock.lines[3] == ":5: " + assert tw_mock.lines[4] == ("_ ", None) + assert tw_mock.lines[5] == "" + assert tw_mock.lines[6] == "> ???" + assert tw_mock.lines[7] == "E ValueError: 3" + assert tw_mock.lines[8] == "" + line = tw_mock.get_write_msg(9) assert line.endswith("mod.py") - assert tw.lines[10] == ":3: ValueError" + assert tw_mock.lines[10] == ":3: ValueError" - def test_toterminal_long_filenames(self, importasmod): + def test_toterminal_long_filenames(self, importasmod, tw_mock): mod = importasmod( """ def f(): @@ -1022,23 +989,22 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.f) - tw = TWMock() path = py.path.local(mod.__file__) old = path.dirpath().chdir() try: repr = excinfo.getrepr(abspath=False) - repr.toterminal(tw) + repr.toterminal(tw_mock) x = py.path.local().bestrelpath(path) if len(x) < len(str(path)): - msg = tw.get_write_msg(-2) + msg = tw_mock.get_write_msg(-2) assert msg == "mod.py" - assert tw.lines[-1] == ":3: ValueError" + assert tw_mock.lines[-1] == ":3: ValueError" repr = excinfo.getrepr(abspath=True) - repr.toterminal(tw) - msg = tw.get_write_msg(-2) + repr.toterminal(tw_mock) + msg = tw_mock.get_write_msg(-2) assert msg == path - line = tw.lines[-1] + line = tw_mock.lines[-1] assert line == ":3: ValueError" finally: old.chdir() @@ -1073,7 +1039,7 @@ raise ValueError() repr.toterminal(tw) assert tw.stringio.getvalue() - def test_traceback_repr_style(self, importasmod): + def test_traceback_repr_style(self, importasmod, tw_mock): mod = importasmod( """ def f(): @@ -1091,35 +1057,34 @@ raise ValueError() excinfo.traceback[1].set_repr_style("short") excinfo.traceback[2].set_repr_style("short") r = excinfo.getrepr(style="long") - tw = TWMock() - r.toterminal(tw) - for line in tw.lines: + r.toterminal(tw_mock) + for line in tw_mock.lines: print(line) - assert tw.lines[0] == "" - assert tw.lines[1] == " def f():" - assert tw.lines[2] == "> g()" - assert tw.lines[3] == "" - msg = tw.get_write_msg(4) + assert tw_mock.lines[0] == "" + assert tw_mock.lines[1] == " def f():" + assert tw_mock.lines[2] == "> g()" + assert tw_mock.lines[3] == "" + msg = tw_mock.get_write_msg(4) assert msg.endswith("mod.py") - assert tw.lines[5] == ":3: " - assert tw.lines[6] == ("_ ", None) - tw.get_write_msg(7) - assert tw.lines[8].endswith("in g") - assert tw.lines[9] == " h()" - tw.get_write_msg(10) - assert tw.lines[11].endswith("in h") - assert tw.lines[12] == " i()" - assert tw.lines[13] == ("_ ", None) - assert tw.lines[14] == "" - assert tw.lines[15] == " def i():" - assert tw.lines[16] == "> raise ValueError()" - assert tw.lines[17] == "E ValueError" - assert tw.lines[18] == "" - msg = tw.get_write_msg(19) + assert tw_mock.lines[5] == ":3: " + assert tw_mock.lines[6] == ("_ ", None) + tw_mock.get_write_msg(7) + assert tw_mock.lines[8].endswith("in g") + assert tw_mock.lines[9] == " h()" + tw_mock.get_write_msg(10) + assert tw_mock.lines[11].endswith("in h") + assert tw_mock.lines[12] == " i()" + assert tw_mock.lines[13] == ("_ ", None) + assert tw_mock.lines[14] == "" + assert tw_mock.lines[15] == " def i():" + assert tw_mock.lines[16] == "> raise ValueError()" + assert tw_mock.lines[17] == "E ValueError" + assert tw_mock.lines[18] == "" + msg = tw_mock.get_write_msg(19) msg.endswith("mod.py") - assert tw.lines[20] == ":9: ValueError" + assert tw_mock.lines[20] == ":9: ValueError" - def test_exc_chain_repr(self, importasmod): + def test_exc_chain_repr(self, importasmod, tw_mock): mod = importasmod( """ class Err(Exception): @@ -1140,72 +1105,71 @@ raise ValueError() ) excinfo = pytest.raises(AttributeError, mod.f) r = excinfo.getrepr(style="long") - tw = TWMock() - r.toterminal(tw) - for line in tw.lines: + r.toterminal(tw_mock) + for line in tw_mock.lines: print(line) - assert tw.lines[0] == "" - assert tw.lines[1] == " def f():" - assert tw.lines[2] == " try:" - assert tw.lines[3] == "> g()" - assert tw.lines[4] == "" - line = tw.get_write_msg(5) + assert tw_mock.lines[0] == "" + assert tw_mock.lines[1] == " def f():" + assert tw_mock.lines[2] == " try:" + assert tw_mock.lines[3] == "> g()" + assert tw_mock.lines[4] == "" + line = tw_mock.get_write_msg(5) assert line.endswith("mod.py") - assert tw.lines[6] == ":6: " - assert tw.lines[7] == ("_ ", None) - assert tw.lines[8] == "" - assert tw.lines[9] == " def g():" - assert tw.lines[10] == "> raise ValueError()" - assert tw.lines[11] == "E ValueError" - assert tw.lines[12] == "" - line = tw.get_write_msg(13) + assert tw_mock.lines[6] == ":6: " + assert tw_mock.lines[7] == ("_ ", None) + assert tw_mock.lines[8] == "" + assert tw_mock.lines[9] == " def g():" + assert tw_mock.lines[10] == "> raise ValueError()" + assert tw_mock.lines[11] == "E ValueError" + assert tw_mock.lines[12] == "" + line = tw_mock.get_write_msg(13) assert line.endswith("mod.py") - assert tw.lines[14] == ":12: ValueError" - assert tw.lines[15] == "" + assert tw_mock.lines[14] == ":12: ValueError" + assert tw_mock.lines[15] == "" assert ( - tw.lines[16] + tw_mock.lines[16] == "The above exception was the direct cause of the following exception:" ) - assert tw.lines[17] == "" - assert tw.lines[18] == " def f():" - assert tw.lines[19] == " try:" - assert tw.lines[20] == " g()" - assert tw.lines[21] == " except Exception as e:" - assert tw.lines[22] == "> raise Err() from e" - assert tw.lines[23] == "E test_exc_chain_repr0.mod.Err" - assert tw.lines[24] == "" - line = tw.get_write_msg(25) + assert tw_mock.lines[17] == "" + assert tw_mock.lines[18] == " def f():" + assert tw_mock.lines[19] == " try:" + assert tw_mock.lines[20] == " g()" + assert tw_mock.lines[21] == " except Exception as e:" + assert tw_mock.lines[22] == "> raise Err() from e" + assert tw_mock.lines[23] == "E test_exc_chain_repr0.mod.Err" + assert tw_mock.lines[24] == "" + line = tw_mock.get_write_msg(25) assert line.endswith("mod.py") - assert tw.lines[26] == ":8: Err" - assert tw.lines[27] == "" + assert tw_mock.lines[26] == ":8: Err" + assert tw_mock.lines[27] == "" assert ( - tw.lines[28] + tw_mock.lines[28] == "During handling of the above exception, another exception occurred:" ) - assert tw.lines[29] == "" - assert tw.lines[30] == " def f():" - assert tw.lines[31] == " try:" - assert tw.lines[32] == " g()" - assert tw.lines[33] == " except Exception as e:" - assert tw.lines[34] == " raise Err() from e" - assert tw.lines[35] == " finally:" - assert tw.lines[36] == "> h()" - assert tw.lines[37] == "" - line = tw.get_write_msg(38) + assert tw_mock.lines[29] == "" + assert tw_mock.lines[30] == " def f():" + assert tw_mock.lines[31] == " try:" + assert tw_mock.lines[32] == " g()" + assert tw_mock.lines[33] == " except Exception as e:" + assert tw_mock.lines[34] == " raise Err() from e" + assert tw_mock.lines[35] == " finally:" + assert tw_mock.lines[36] == "> h()" + assert tw_mock.lines[37] == "" + line = tw_mock.get_write_msg(38) assert line.endswith("mod.py") - assert tw.lines[39] == ":10: " - assert tw.lines[40] == ("_ ", None) - assert tw.lines[41] == "" - assert tw.lines[42] == " def h():" - assert tw.lines[43] == "> raise AttributeError()" - assert tw.lines[44] == "E AttributeError" - assert tw.lines[45] == "" - line = tw.get_write_msg(46) + assert tw_mock.lines[39] == ":10: " + assert tw_mock.lines[40] == ("_ ", None) + assert tw_mock.lines[41] == "" + assert tw_mock.lines[42] == " def h():" + assert tw_mock.lines[43] == "> raise AttributeError()" + assert tw_mock.lines[44] == "E AttributeError" + assert tw_mock.lines[45] == "" + line = tw_mock.get_write_msg(46) assert line.endswith("mod.py") - assert tw.lines[47] == ":15: AttributeError" + assert tw_mock.lines[47] == ":15: AttributeError" @pytest.mark.parametrize("mode", ["from_none", "explicit_suppress"]) - def test_exc_repr_chain_suppression(self, importasmod, mode): + def test_exc_repr_chain_suppression(self, importasmod, mode, tw_mock): """Check that exc repr does not show chained exceptions in Python 3. - When the exception is raised with "from None" - Explicitly suppressed with "chain=False" to ExceptionInfo.getrepr(). @@ -1226,24 +1190,23 @@ raise ValueError() ) excinfo = pytest.raises(AttributeError, mod.f) r = excinfo.getrepr(style="long", chain=mode != "explicit_suppress") - tw = TWMock() - r.toterminal(tw) - for line in tw.lines: + r.toterminal(tw_mock) + for line in tw_mock.lines: print(line) - assert tw.lines[0] == "" - assert tw.lines[1] == " def f():" - assert tw.lines[2] == " try:" - assert tw.lines[3] == " g()" - assert tw.lines[4] == " except Exception:" - assert tw.lines[5] == "> raise AttributeError(){}".format( + assert tw_mock.lines[0] == "" + assert tw_mock.lines[1] == " def f():" + assert tw_mock.lines[2] == " try:" + assert tw_mock.lines[3] == " g()" + assert tw_mock.lines[4] == " except Exception:" + assert tw_mock.lines[5] == "> raise AttributeError(){}".format( raise_suffix ) - assert tw.lines[6] == "E AttributeError" - assert tw.lines[7] == "" - line = tw.get_write_msg(8) + assert tw_mock.lines[6] == "E AttributeError" + assert tw_mock.lines[7] == "" + line = tw_mock.get_write_msg(8) assert line.endswith("mod.py") - assert tw.lines[9] == ":6: AttributeError" - assert len(tw.lines) == 10 + assert tw_mock.lines[9] == ":6: AttributeError" + assert len(tw_mock.lines) == 10 @pytest.mark.parametrize( "reason, description", @@ -1304,7 +1267,7 @@ raise ValueError() ] ) - def test_exc_chain_repr_cycle(self, importasmod): + def test_exc_chain_repr_cycle(self, importasmod, tw_mock): mod = importasmod( """ class Err(Exception): @@ -1325,9 +1288,8 @@ raise ValueError() ) excinfo = pytest.raises(ZeroDivisionError, mod.unreraise) r = excinfo.getrepr(style="short") - tw = TWMock() - r.toterminal(tw) - out = "\n".join(line for line in tw.lines if isinstance(line, str)) + r.toterminal(tw_mock) + out = "\n".join(line for line in tw_mock.lines if isinstance(line, str)) expected_out = textwrap.dedent( """\ :13: in unreraise diff --git a/testing/conftest.py b/testing/conftest.py index 635e7a614..d7f94ce45 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -55,3 +55,36 @@ def pytest_collection_modifyitems(config, items): items[:] = fast_items + neutral_items + slow_items + slowest_items yield + + +@pytest.fixture +def tw_mock(): + """Returns a mock terminal writer""" + + class TWMock: + WRITE = object() + + def __init__(self): + self.lines = [] + self.is_writing = False + + def sep(self, sep, line=None): + self.lines.append((sep, line)) + + def write(self, msg, **kw): + self.lines.append((TWMock.WRITE, msg)) + + def line(self, line, **kw): + self.lines.append(line) + + def markup(self, text, **kw): + return text + + def get_write_msg(self, idx): + flag, msg = self.lines[idx] + assert flag == TWMock.WRITE + return msg + + fullwidth = 80 + + return TWMock() From 505c3340bf852f7636eae1490a8638f5ffab266d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 26 Aug 2019 17:18:46 +0200 Subject: [PATCH 04/27] Fix pytest with mixed up filename casing. --- src/_pytest/config/__init__.py | 19 +++++++++++++------ testing/test_conftest.py | 29 ++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index b861563e9..5d77fa983 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -36,6 +36,10 @@ hookimpl = HookimplMarker("pytest") hookspec = HookspecMarker("pytest") +def _uniquepath(path): + return type(path)(os.path.normcase(str(path.realpath()))) + + class ConftestImportFailure(Exception): def __init__(self, path, excinfo): Exception.__init__(self, path, excinfo) @@ -366,7 +370,7 @@ class PytestPluginManager(PluginManager): """ current = py.path.local() self._confcutdir = ( - current.join(namespace.confcutdir, abs=True) + _uniquepath(current.join(namespace.confcutdir, abs=True)) if namespace.confcutdir else None ) @@ -405,19 +409,18 @@ class PytestPluginManager(PluginManager): else: directory = path + directory = _uniquepath(directory) + # XXX these days we may rather want to use config.rootdir # and allow users to opt into looking into the rootdir parent # directories instead of requiring to specify confcutdir clist = [] - for parent in directory.realpath().parts(): + for parent in directory.parts(): if self._confcutdir and self._confcutdir.relto(parent): continue conftestpath = parent.join("conftest.py") if conftestpath.isfile(): - # Use realpath to avoid loading the same conftest twice - # with build systems that create build directories containing - # symlinks to actual files. - mod = self._importconftest(conftestpath.realpath()) + mod = self._importconftest(conftestpath) clist.append(mod) self._dirpath2confmods[directory] = clist return clist @@ -432,6 +435,10 @@ class PytestPluginManager(PluginManager): raise KeyError(name) def _importconftest(self, conftestpath): + # Use realpath to avoid loading the same conftest twice + # with build systems that create build directories containing + # symlinks to actual files. + conftestpath = _uniquepath(conftestpath) try: return self._conftestpath2mod[conftestpath] except KeyError: diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 447416f10..1f52247d5 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -3,8 +3,9 @@ import textwrap import py import pytest -from _pytest.config import PytestPluginManager +from _pytest.config import PytestPluginManager, _uniquepath from _pytest.main import ExitCode +import os.path def ConftestWithSetinitial(path): @@ -141,11 +142,11 @@ def test_conftestcutdir(testdir): # but we can still import a conftest directly conftest._importconftest(conf) values = conftest._getconftestmodules(conf.dirpath()) - assert values[0].__file__.startswith(str(conf)) + assert values[0].__file__.startswith(str(_uniquepath(conf))) # and all sub paths get updated properly values = conftest._getconftestmodules(p) assert len(values) == 1 - assert values[0].__file__.startswith(str(conf)) + assert values[0].__file__.startswith(str(_uniquepath(conf))) def test_conftestcutdir_inplace_considered(testdir): @@ -154,7 +155,7 @@ def test_conftestcutdir_inplace_considered(testdir): conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath()) values = conftest._getconftestmodules(conf.dirpath()) assert len(values) == 1 - assert values[0].__file__.startswith(str(conf)) + assert values[0].__file__.startswith(str(_uniquepath(conf))) @pytest.mark.parametrize("name", "test tests whatever .dotdir".split()) @@ -164,7 +165,7 @@ def test_setinitial_conftest_subdirs(testdir, name): conftest = PytestPluginManager() conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir) if name not in ("whatever", ".dotdir"): - assert subconftest in conftest._conftestpath2mod + assert _uniquepath(subconftest) in conftest._conftestpath2mod assert len(conftest._conftestpath2mod) == 1 else: assert subconftest not in conftest._conftestpath2mod @@ -274,6 +275,24 @@ def test_conftest_symlink_files(testdir): result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"]) assert result.ret == ExitCode.OK +@pytest.mark.skipif( + os.path.normcase('x') != os.path.normcase('X'), + reason="only relevant for case insensitive file systems", +) +def test_conftest_badcase(testdir): + """Check conftest.py loading when directory casing is wrong.""" + testdir.tmpdir.mkdir("JenkinsRoot").mkdir("test") + source = { + "setup.py": "", + "test/__init__.py": "", + "test/conftest.py": "" + } + testdir.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()}) + + testdir.tmpdir.join("jenkinsroot/test").chdir() + result = testdir.runpytest() + assert result.ret == ExitCode.NO_TESTS_COLLECTED + def test_no_conftest(testdir): testdir.makeconftest("assert 0") From 1aac64573fab858e9c216a8faf9e802fbb1b8303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 27 Aug 2019 16:16:45 +0200 Subject: [PATCH 05/27] black formatting. --- testing/test_conftest.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 1f52247d5..a9af649d0 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,11 +1,12 @@ +import os.path import textwrap import py import pytest -from _pytest.config import PytestPluginManager, _uniquepath +from _pytest.config import _uniquepath +from _pytest.config import PytestPluginManager from _pytest.main import ExitCode -import os.path def ConftestWithSetinitial(path): @@ -275,18 +276,15 @@ def test_conftest_symlink_files(testdir): result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"]) assert result.ret == ExitCode.OK + @pytest.mark.skipif( - os.path.normcase('x') != os.path.normcase('X'), + os.path.normcase("x") != os.path.normcase("X"), reason="only relevant for case insensitive file systems", ) def test_conftest_badcase(testdir): """Check conftest.py loading when directory casing is wrong.""" testdir.tmpdir.mkdir("JenkinsRoot").mkdir("test") - source = { - "setup.py": "", - "test/__init__.py": "", - "test/conftest.py": "" - } + source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""} testdir.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()}) testdir.tmpdir.join("jenkinsroot/test").chdir() From a98270eac078db7d78a50b35f0fcf9bdb8bf888f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 27 Aug 2019 16:25:24 +0200 Subject: [PATCH 06/27] Document the bugfix. --- AUTHORS | 1 + changelog/5792.bugfix.rst | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 changelog/5792.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 88bbfe352..1dbef3d5d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -55,6 +55,7 @@ Charnjit SiNGH (CCSJ) Chris Lamb Christian Boelsen Christian Fetzer +Christian Neumüller Christian Theunert Christian Tismer Christopher Gilling diff --git a/changelog/5792.bugfix.rst b/changelog/5792.bugfix.rst new file mode 100644 index 000000000..1ee0364dd --- /dev/null +++ b/changelog/5792.bugfix.rst @@ -0,0 +1,3 @@ +Windows: Fix error that occurs in certain circumstances when loading +``conftest.py`` from a working directory that has casing other than the one stored +in the filesystem (e.g., ``c:\test`` instead of ``C:\test``). From 29bb0eda276fc3808264a1b070544339c1eee393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 28 Aug 2019 09:21:03 +0200 Subject: [PATCH 07/27] Move _uniquepath to pathlib as unique_path. Co-authored-by: Bruno Oliveira --- src/_pytest/config/__init__.py | 11 ++++------- src/_pytest/pathlib.py | 10 ++++++++++ testing/test_conftest.py | 10 +++++----- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 5d77fa983..3a0eca546 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -30,16 +30,13 @@ from _pytest._code import filter_traceback from _pytest.compat import importlib_metadata from _pytest.outcomes import fail from _pytest.outcomes import Skipped +from _pytest.pathlib import unique_path from _pytest.warning_types import PytestConfigWarning hookimpl = HookimplMarker("pytest") hookspec = HookspecMarker("pytest") -def _uniquepath(path): - return type(path)(os.path.normcase(str(path.realpath()))) - - class ConftestImportFailure(Exception): def __init__(self, path, excinfo): Exception.__init__(self, path, excinfo) @@ -370,7 +367,7 @@ class PytestPluginManager(PluginManager): """ current = py.path.local() self._confcutdir = ( - _uniquepath(current.join(namespace.confcutdir, abs=True)) + unique_path(current.join(namespace.confcutdir, abs=True)) if namespace.confcutdir else None ) @@ -409,7 +406,7 @@ class PytestPluginManager(PluginManager): else: directory = path - directory = _uniquepath(directory) + directory = unique_path(directory) # XXX these days we may rather want to use config.rootdir # and allow users to opt into looking into the rootdir parent @@ -438,7 +435,7 @@ class PytestPluginManager(PluginManager): # Use realpath to avoid loading the same conftest twice # with build systems that create build directories containing # symlinks to actual files. - conftestpath = _uniquepath(conftestpath) + conftestpath = unique_path(conftestpath) try: return self._conftestpath2mod[conftestpath] except KeyError: diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 19f9c062f..0403b6947 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -11,6 +11,7 @@ from functools import partial from os.path import expanduser from os.path import expandvars from os.path import isabs +from os.path import normcase from os.path import sep from posixpath import sep as posix_sep @@ -334,3 +335,12 @@ def fnmatch_ex(pattern, path): def parts(s): parts = s.split(sep) return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} + + +def unique_path(path): + """Returns a unique path in case-insensitive (but case-preserving) file + systems such as Windows. + + This is needed only for ``py.path.local``; ``pathlib.Path`` handles this + natively with ``resolve()``.""" + return type(path)(normcase(str(path.realpath()))) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index a9af649d0..9888f5457 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -4,9 +4,9 @@ import textwrap import py import pytest -from _pytest.config import _uniquepath from _pytest.config import PytestPluginManager from _pytest.main import ExitCode +from _pytest.pathlib import unique_path def ConftestWithSetinitial(path): @@ -143,11 +143,11 @@ def test_conftestcutdir(testdir): # but we can still import a conftest directly conftest._importconftest(conf) values = conftest._getconftestmodules(conf.dirpath()) - assert values[0].__file__.startswith(str(_uniquepath(conf))) + assert values[0].__file__.startswith(str(unique_path(conf))) # and all sub paths get updated properly values = conftest._getconftestmodules(p) assert len(values) == 1 - assert values[0].__file__.startswith(str(_uniquepath(conf))) + assert values[0].__file__.startswith(str(unique_path(conf))) def test_conftestcutdir_inplace_considered(testdir): @@ -156,7 +156,7 @@ def test_conftestcutdir_inplace_considered(testdir): conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath()) values = conftest._getconftestmodules(conf.dirpath()) assert len(values) == 1 - assert values[0].__file__.startswith(str(_uniquepath(conf))) + assert values[0].__file__.startswith(str(unique_path(conf))) @pytest.mark.parametrize("name", "test tests whatever .dotdir".split()) @@ -166,7 +166,7 @@ def test_setinitial_conftest_subdirs(testdir, name): conftest = PytestPluginManager() conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir) if name not in ("whatever", ".dotdir"): - assert _uniquepath(subconftest) in conftest._conftestpath2mod + assert unique_path(subconftest) in conftest._conftestpath2mod assert len(conftest._conftestpath2mod) == 1 else: assert subconftest not in conftest._conftestpath2mod From 487659d8b13b6adfb1db23225c71a485cd0117bb Mon Sep 17 00:00:00 2001 From: Andrzej Klajnert Date: Wed, 28 Aug 2019 19:50:13 +0200 Subject: [PATCH 08/27] Fix the scope behavior with indirect fixtures. --- AUTHORS | 1 + changelog/570.bugfix.rst | 1 + src/_pytest/fixtures.py | 7 ++++-- testing/python/fixtures.py | 45 +++++++++++++++++++++++++++++++++++++- 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 changelog/570.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 1dbef3d5d..e355f01e0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -23,6 +23,7 @@ Andras Tim Andrea Cimatoribus Andreas Zeidler Andrey Paramonov +Andrzej Klajnert Andrzej Ostrowski Andy Freeland Anthon van der Neut diff --git a/changelog/570.bugfix.rst b/changelog/570.bugfix.rst new file mode 100644 index 000000000..8936ff96a --- /dev/null +++ b/changelog/570.bugfix.rst @@ -0,0 +1 @@ +Fix the scope behavior with indirect fixtures. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d5f9ad2d3..9a904758f 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -859,7 +859,7 @@ class FixtureDef: if argname != "request": fixturedef.addfinalizer(functools.partial(self.finish, request=request)) - my_cache_key = request.param_index + my_cache_key = self.cache_key(request) cached_result = getattr(self, "cached_result", None) if cached_result is not None: result, cache_key, err = cached_result @@ -877,6 +877,9 @@ class FixtureDef: hook = self._fixturemanager.session.gethookproxy(request.node.fspath) return hook.pytest_fixture_setup(fixturedef=self, request=request) + def cache_key(self, request): + return request.param_index if not hasattr(request, "param") else request.param + def __repr__(self): return "".format( self.argname, self.scope, self.baseid @@ -913,7 +916,7 @@ def pytest_fixture_setup(fixturedef, request): kwargs[argname] = result fixturefunc = resolve_fixture_function(fixturedef, request) - my_cache_key = request.param_index + my_cache_key = fixturedef.cache_key(request) try: result = call_fixture_func(fixturefunc, request, kwargs) except TEST_OUTCOME: diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 1f383e752..a129aa582 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -449,7 +449,8 @@ class TestFillFixtures: "*ERROR at setup of test_lookup_error*", " def test_lookup_error(unknown):*", "E fixture 'unknown' not found", - "> available fixtures:*a_fixture,*b_fixture,*c_fixture,*d_fixture*monkeypatch,*", # sorted + "> available fixtures:*a_fixture,*b_fixture,*c_fixture,*d_fixture*monkeypatch,*", + # sorted "> use 'py*test --fixtures *' for help on them.", "*1 error*", ] @@ -4009,3 +4010,45 @@ def test_fixture_named_request(testdir): " *test_fixture_named_request.py:5", ] ) + + +def test_indirect_fixture(testdir): + testdir.makepyfile( + """ + from collections import Counter + + import pytest + + + @pytest.fixture(scope="session") + def fixture_1(request, count=Counter()): + count[request.param] += 1 + yield count[request.param] + + + @pytest.fixture(scope="session") + def fixture_2(request): + yield request.param + + + scenarios = [ + ("a", "a1"), + ("a", "a2"), + ("b", "b1"), + ("b", "b2"), + ("c", "c1"), + ("c", "c2"), + ] + + + @pytest.mark.parametrize( + "fixture_1,fixture_2", scenarios, indirect=["fixture_1", "fixture_2"] + ) + def test_it(fixture_1, fixture_2): + assert fixture_1 == 1 + assert fixture_2[1] in ("1", "2") + + """ + ) + result = testdir.runpytest() + result.assert_outcomes(passed=6) From a511b98da92c0fbb607818e29a854e6c610e5aa0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 26 Aug 2019 13:21:53 -0300 Subject: [PATCH 09/27] Serialize/deserialize chained exceptions Fix #5786 --- changelog/5786.bugfix.rst | 2 ++ src/_pytest/reports.py | 69 +++++++++++++++++++++++++----------- testing/test_reports.py | 74 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 122 insertions(+), 23 deletions(-) create mode 100644 changelog/5786.bugfix.rst diff --git a/changelog/5786.bugfix.rst b/changelog/5786.bugfix.rst new file mode 100644 index 000000000..70754c901 --- /dev/null +++ b/changelog/5786.bugfix.rst @@ -0,0 +1,2 @@ +Chained exceptions in test and collection reports are now correctly serialized, allowing plugins like +``pytest-xdist`` to display them properly. diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index b87277c33..56aea248d 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -3,6 +3,7 @@ from typing import Optional import py +from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprEntry from _pytest._code.code import ReprEntryNative @@ -160,7 +161,7 @@ class BaseReport: Experimental method. """ - return _test_report_to_json(self) + return _report_to_json(self) @classmethod def _from_json(cls, reportdict): @@ -172,7 +173,7 @@ class BaseReport: Experimental method. """ - kwargs = _test_report_kwargs_from_json(reportdict) + kwargs = _report_kwargs_from_json(reportdict) return cls(**kwargs) @@ -340,7 +341,7 @@ def pytest_report_from_serializable(data): ) -def _test_report_to_json(test_report): +def _report_to_json(report): """ This was originally the serialize_report() function from xdist (ca03269). @@ -366,22 +367,35 @@ def _test_report_to_json(test_report): return reprcrash.__dict__.copy() def serialize_longrepr(rep): - return { + result = { "reprcrash": serialize_repr_crash(rep.longrepr.reprcrash), "reprtraceback": serialize_repr_traceback(rep.longrepr.reprtraceback), "sections": rep.longrepr.sections, } - - d = test_report.__dict__.copy() - if hasattr(test_report.longrepr, "toterminal"): - if hasattr(test_report.longrepr, "reprtraceback") and hasattr( - test_report.longrepr, "reprcrash" - ): - d["longrepr"] = serialize_longrepr(test_report) + if isinstance(rep.longrepr, ExceptionChainRepr): + result["chain"] = [] + for repr_traceback, repr_crash, description in rep.longrepr.chain: + result["chain"].append( + ( + serialize_repr_traceback(repr_traceback), + serialize_repr_crash(repr_crash), + description, + ) + ) else: - d["longrepr"] = str(test_report.longrepr) + result["chain"] = None + return result + + d = report.__dict__.copy() + if hasattr(report.longrepr, "toterminal"): + if hasattr(report.longrepr, "reprtraceback") and hasattr( + report.longrepr, "reprcrash" + ): + d["longrepr"] = serialize_longrepr(report) + else: + d["longrepr"] = str(report.longrepr) else: - d["longrepr"] = test_report.longrepr + d["longrepr"] = report.longrepr for name in d: if isinstance(d[name], (py.path.local, Path)): d[name] = str(d[name]) @@ -390,12 +404,11 @@ def _test_report_to_json(test_report): return d -def _test_report_kwargs_from_json(reportdict): +def _report_kwargs_from_json(reportdict): """ This was originally the serialize_report() function from xdist (ca03269). - Factory method that returns either a TestReport or CollectReport, depending on the calling - class. It's the callers responsibility to know which class to pass here. + Returns **kwargs that can be used to construct a TestReport or CollectReport instance. """ def deserialize_repr_entry(entry_data): @@ -439,12 +452,26 @@ def _test_report_kwargs_from_json(reportdict): and "reprcrash" in reportdict["longrepr"] and "reprtraceback" in reportdict["longrepr"] ): - exception_info = ReprExceptionInfo( - reprtraceback=deserialize_repr_traceback( - reportdict["longrepr"]["reprtraceback"] - ), - reprcrash=deserialize_repr_crash(reportdict["longrepr"]["reprcrash"]), + + reprtraceback = deserialize_repr_traceback( + reportdict["longrepr"]["reprtraceback"] ) + reprcrash = deserialize_repr_crash(reportdict["longrepr"]["reprcrash"]) + if reportdict["longrepr"]["chain"]: + chain = [] + for repr_traceback_data, repr_crash_data, description in reportdict[ + "longrepr" + ]["chain"]: + chain.append( + ( + deserialize_repr_traceback(repr_traceback_data), + deserialize_repr_crash(repr_crash_data), + description, + ) + ) + exception_info = ExceptionChainRepr(chain) + else: + exception_info = ReprExceptionInfo(reprtraceback, reprcrash) for section in reportdict["longrepr"]["sections"]: exception_info.addsection(*section) diff --git a/testing/test_reports.py b/testing/test_reports.py index b8b1a5406..8bac0243a 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -1,4 +1,5 @@ import pytest +from _pytest._code.code import ExceptionChainRepr from _pytest.pathlib import Path from _pytest.reports import CollectReport from _pytest.reports import TestReport @@ -220,8 +221,8 @@ class TestReportSerialization: assert data["path1"] == str(testdir.tmpdir) assert data["path2"] == str(testdir.tmpdir) - def test_unserialization_failure(self, testdir): - """Check handling of failure during unserialization of report types.""" + def test_deserialization_failure(self, testdir): + """Check handling of failure during deserialization of report types.""" testdir.makepyfile( """ def test_a(): @@ -242,6 +243,75 @@ class TestReportSerialization: ): TestReport._from_json(data) + @pytest.mark.parametrize("report_class", [TestReport, CollectReport]) + def test_chained_exceptions(self, testdir, tw_mock, report_class): + """Check serialization/deserialization of report objects containing chained exceptions (#5786)""" + testdir.makepyfile( + """ + def foo(): + raise ValueError('value error') + def test_a(): + try: + foo() + except ValueError as e: + raise RuntimeError('runtime error') from e + if {error_during_import}: + test_a() + """.format( + error_during_import=report_class is CollectReport + ) + ) + + reprec = testdir.inline_run() + if report_class is TestReport: + reports = reprec.getreports("pytest_runtest_logreport") + # we have 3 reports: setup/call/teardown + assert len(reports) == 3 + # get the call report + report = reports[1] + else: + assert report_class is CollectReport + # two collection reports: session and test file + reports = reprec.getreports("pytest_collectreport") + assert len(reports) == 2 + report = reports[1] + + def check_longrepr(longrepr): + """Check the attributes of the given longrepr object according to the test file. + + We can get away with testing both CollectReport and TestReport with this function because + the longrepr objects are very similar. + """ + assert isinstance(longrepr, ExceptionChainRepr) + assert longrepr.sections == [("title", "contents", "=")] + assert len(longrepr.chain) == 2 + entry1, entry2 = longrepr.chain + tb1, fileloc1, desc1 = entry1 + tb2, fileloc2, desc2 = entry2 + + assert "ValueError('value error')" in str(tb1) + assert "RuntimeError('runtime error')" in str(tb2) + + assert ( + desc1 + == "The above exception was the direct cause of the following exception:" + ) + assert desc2 is None + + assert report.failed + assert len(report.sections) == 0 + report.longrepr.addsection("title", "contents", "=") + check_longrepr(report.longrepr) + + data = report._to_json() + loaded_report = report_class._from_json(data) + check_longrepr(loaded_report.longrepr) + + # make sure we don't blow up on ``toterminal`` call; we don't test the actual output because it is very + # brittle and hard to maintain, but we can assume it is correct because ``toterminal`` is already tested + # elsewhere and we do check the contents of the longrepr object after loading it. + loaded_report.longrepr.toterminal(tw_mock) + class TestHooks: """Test that the hooks are working correctly for plugins""" From 35b3b1097ffc576b11cd8ad205103666a90e2344 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 30 Aug 2019 10:54:07 -0300 Subject: [PATCH 10/27] Improve CHANGELOG and make test easier to understand for #570 --- changelog/570.bugfix.rst | 3 ++- testing/python/fixtures.py | 46 +++++++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/changelog/570.bugfix.rst b/changelog/570.bugfix.rst index 8936ff96a..980491e28 100644 --- a/changelog/570.bugfix.rst +++ b/changelog/570.bugfix.rst @@ -1 +1,2 @@ -Fix the scope behavior with indirect fixtures. +Fixed long standing issue where fixture scope was not respected when indirect fixtures were used during +parametrization. diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index a129aa582..d998e9726 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4012,43 +4012,53 @@ def test_fixture_named_request(testdir): ) -def test_indirect_fixture(testdir): +def test_indirect_fixture_does_not_break_scope(testdir): + """Ensure that fixture scope is respected when using indirect fixtures (#570)""" testdir.makepyfile( """ - from collections import Counter - import pytest + instantiated = [] @pytest.fixture(scope="session") - def fixture_1(request, count=Counter()): - count[request.param] += 1 - yield count[request.param] + def fixture_1(request): + instantiated.append(("fixture_1", request.param)) @pytest.fixture(scope="session") def fixture_2(request): - yield request.param + instantiated.append(("fixture_2", request.param)) scenarios = [ - ("a", "a1"), - ("a", "a2"), - ("b", "b1"), - ("b", "b2"), - ("c", "c1"), - ("c", "c2"), + ("A", "a1"), + ("A", "a2"), + ("B", "b1"), + ("B", "b2"), + ("C", "c1"), + ("C", "c2"), ] - @pytest.mark.parametrize( "fixture_1,fixture_2", scenarios, indirect=["fixture_1", "fixture_2"] ) - def test_it(fixture_1, fixture_2): - assert fixture_1 == 1 - assert fixture_2[1] in ("1", "2") + def test_create_fixtures(fixture_1, fixture_2): + pass + + def test_check_fixture_instantiations(): + assert instantiated == [ + ('fixture_1', 'A'), + ('fixture_2', 'a1'), + ('fixture_2', 'a2'), + ('fixture_1', 'B'), + ('fixture_2', 'b1'), + ('fixture_2', 'b2'), + ('fixture_1', 'C'), + ('fixture_2', 'c1'), + ('fixture_2', 'c2'), + ] """ ) result = testdir.runpytest() - result.assert_outcomes(passed=6) + result.assert_outcomes(passed=7) From bb60736a6f76276b1a0295dee5cf70c99e2997e7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 30 Aug 2019 10:25:46 -0300 Subject: [PATCH 11/27] Run py35 without xdist on Travis Due to the flaky tests in 3.5.0, drop running py35 with xdist for now in the hope we get better error messages. Ref: #5795 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c1f7ad357..41c23cfbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,7 +42,7 @@ jobs: - env: TOXENV=pypy3-xdist python: 'pypy3' - - env: TOXENV=py35-xdist + - env: TOXENV=py35 dist: trusty python: '3.5.0' From 3ddbc7fb2a63f6ca505f64368d5bd4dd939f0ea9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 30 Aug 2019 11:20:19 -0300 Subject: [PATCH 12/27] Improve CHANGELOG and add some comments Ref: #5768 --- changelog/2270.bugfix.rst | 3 ++- src/_pytest/fixtures.py | 2 ++ testing/python/fixtures.py | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/changelog/2270.bugfix.rst b/changelog/2270.bugfix.rst index 45835deb6..0da084597 100644 --- a/changelog/2270.bugfix.rst +++ b/changelog/2270.bugfix.rst @@ -1 +1,2 @@ -Fix ``self`` reference in function scoped fixtures that are in a plugin class +Fixed ``self`` reference in function-scoped fixtures defined plugin classes: previously ``self`` +would be a reference to a *test* class, not the *plugin* class. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 0640ebc90..a8da1beec 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -897,6 +897,8 @@ def resolve_fixture_function(fixturedef, request): # request.instance so that code working with "fixturedef" behaves # as expected. if request.instance is not None: + # handle the case where fixture is defined not in a test class, but some other class + # (for example a plugin class with a fixture), see #2270 if hasattr(fixturefunc, "__self__") and not isinstance( request.instance, fixturefunc.__self__.__class__ ): diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index ee050b909..4f13dd245 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3946,6 +3946,9 @@ class TestScopeOrdering: reprec.assertoutcome(passed=2) def test_class_fixture_self_instance(self, testdir): + """Check that plugin classes which implement fixtures receive the plugin instance + as self (see #2270). + """ testdir.makeconftest( """ import pytest From f9cc704b1a8cf475a02051b747b78897b13a83f7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 30 Aug 2019 12:42:14 -0300 Subject: [PATCH 13/27] Replace session duration to a fix value in regendoc --- doc/en/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/Makefile b/doc/en/Makefile index 341a5cb85..bfeb96c8c 100644 --- a/doc/en/Makefile +++ b/doc/en/Makefile @@ -16,7 +16,7 @@ REGENDOC_ARGS := \ --normalize "/[ \t]+\n/\n/" \ --normalize "~\$$REGENDOC_TMPDIR~/home/sweet/project~" \ --normalize "~/path/to/example~/home/sweet/project~" \ - --normalize "/in \d+.\d+ seconds/in 0.12 seconds/" \ + --normalize "/in \d+.\d+s ==/in 0.12s ==/" \ --normalize "@/tmp/pytest-of-.*/pytest-\d+@PYTEST_TMPDIR@" \ --normalize "@pytest-(\d+)\\.[^ ,]+@pytest-\1.x.y@" \ --normalize "@(This is pytest version )(\d+)\\.[^ ,]+@\1\2.x.y@" \ From e56544cb587e8f288bea12aff01355b31aec92f7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 30 Aug 2019 12:43:47 -0300 Subject: [PATCH 14/27] Preparing release version 5.1.2 --- CHANGELOG.rst | 26 +++++++++++++++++++++++ changelog/2270.bugfix.rst | 2 -- changelog/570.bugfix.rst | 2 -- changelog/5782.bugfix.rst | 1 - changelog/5786.bugfix.rst | 2 -- changelog/5792.bugfix.rst | 3 --- doc/en/announce/index.rst | 1 + doc/en/announce/release-5.1.2.rst | 23 +++++++++++++++++++++ doc/en/assert.rst | 4 ++-- doc/en/cache.rst | 10 ++++----- doc/en/capture.rst | 2 +- doc/en/doctest.rst | 4 ++-- doc/en/example/markers.rst | 32 ++++++++++++++--------------- doc/en/example/nonpython.rst | 6 +++--- doc/en/example/parametrize.rst | 21 ++++++++++--------- doc/en/example/pythoncollection.rst | 6 +++--- doc/en/example/reportingdemo.rst | 2 +- doc/en/example/simple.rst | 22 ++++++++++---------- doc/en/fixture.rst | 18 ++++++++-------- doc/en/getting-started.rst | 4 ++-- doc/en/index.rst | 2 +- doc/en/parametrize.rst | 4 ++-- doc/en/skipping.rst | 2 +- doc/en/tmpdir.rst | 4 ++-- doc/en/unittest.rst | 2 +- doc/en/usage.rst | 6 +++--- doc/en/warnings.rst | 2 +- 27 files changed, 127 insertions(+), 86 deletions(-) delete mode 100644 changelog/2270.bugfix.rst delete mode 100644 changelog/570.bugfix.rst delete mode 100644 changelog/5782.bugfix.rst delete mode 100644 changelog/5786.bugfix.rst delete mode 100644 changelog/5792.bugfix.rst create mode 100644 doc/en/announce/release-5.1.2.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3f9637248..4270d4df3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,32 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.1.2 (2019-08-30) +========================= + +Bug Fixes +--------- + +- `#2270 `_: Fixed ``self`` reference in function-scoped fixtures defined plugin classes: previously ``self`` + would be a reference to a *test* class, not the *plugin* class. + + +- `#570 `_: Fixed long standing issue where fixture scope was not respected when indirect fixtures were used during + parametrization. + + +- `#5782 `_: Fix decoding error when printing an error response from ``--pastebin``. + + +- `#5786 `_: Chained exceptions in test and collection reports are now correctly serialized, allowing plugins like + ``pytest-xdist`` to display them properly. + + +- `#5792 `_: Windows: Fix error that occurs in certain circumstances when loading + ``conftest.py`` from a working directory that has casing other than the one stored + in the filesystem (e.g., ``c:\test`` instead of ``C:\test``). + + pytest 5.1.1 (2019-08-20) ========================= diff --git a/changelog/2270.bugfix.rst b/changelog/2270.bugfix.rst deleted file mode 100644 index 0da084597..000000000 --- a/changelog/2270.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed ``self`` reference in function-scoped fixtures defined plugin classes: previously ``self`` -would be a reference to a *test* class, not the *plugin* class. diff --git a/changelog/570.bugfix.rst b/changelog/570.bugfix.rst deleted file mode 100644 index 980491e28..000000000 --- a/changelog/570.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed long standing issue where fixture scope was not respected when indirect fixtures were used during -parametrization. diff --git a/changelog/5782.bugfix.rst b/changelog/5782.bugfix.rst deleted file mode 100644 index e961d8fb5..000000000 --- a/changelog/5782.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix decoding error when printing an error response from ``--pastebin``. diff --git a/changelog/5786.bugfix.rst b/changelog/5786.bugfix.rst deleted file mode 100644 index 70754c901..000000000 --- a/changelog/5786.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Chained exceptions in test and collection reports are now correctly serialized, allowing plugins like -``pytest-xdist`` to display them properly. diff --git a/changelog/5792.bugfix.rst b/changelog/5792.bugfix.rst deleted file mode 100644 index 1ee0364dd..000000000 --- a/changelog/5792.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Windows: Fix error that occurs in certain circumstances when loading -``conftest.py`` from a working directory that has casing other than the one stored -in the filesystem (e.g., ``c:\test`` instead of ``C:\test``). diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 84a41d2bf..81f977ef1 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.1.2 release-5.1.1 release-5.1.0 release-5.0.1 diff --git a/doc/en/announce/release-5.1.2.rst b/doc/en/announce/release-5.1.2.rst new file mode 100644 index 000000000..ac6e00581 --- /dev/null +++ b/doc/en/announce/release-5.1.2.rst @@ -0,0 +1,23 @@ +pytest-5.1.2 +======================================= + +pytest 5.1.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Andrzej Klajnert +* Anthony Sottile +* Bruno Oliveira +* Christian Neumüller +* Robert Holt +* linchiwei123 + + +Happy testing, +The pytest Development Team diff --git a/doc/en/assert.rst b/doc/en/assert.rst index 16de77898..04ee9c0f6 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -47,7 +47,7 @@ you will see the return value of the function call: E + where 3 = f() test_assert1.py:6: AssertionError - ============================ 1 failed in 0.02s ============================= + ============================ 1 failed in 0.12s ============================= ``pytest`` has support for showing the values of the most common subexpressions including calls, attributes, comparisons, and binary and unary @@ -208,7 +208,7 @@ if you run this module: E Use -v to get the full diff test_assert2.py:6: AssertionError - ============================ 1 failed in 0.02s ============================= + ============================ 1 failed in 0.12s ============================= Special comparisons are done for a number of cases: diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 384be5daf..38e00cc0c 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -75,7 +75,7 @@ If you run this for the first time you will see two failures: E Failed: bad luck test_50.py:7: Failed - 2 failed, 48 passed in 0.08s + 2 failed, 48 passed in 0.07s If you then run it with ``--lf``: @@ -114,7 +114,7 @@ If you then run it with ``--lf``: E Failed: bad luck test_50.py:7: Failed - ===================== 2 failed, 48 deselected in 0.02s ===================== + ===================== 2 failed, 48 deselected in 0.12s ===================== You have run only the two failing tests from the last run, while the 48 passing tests have not been run ("deselected"). @@ -158,7 +158,7 @@ of ``FF`` and dots): E Failed: bad luck test_50.py:7: Failed - ======================= 2 failed, 48 passed in 0.07s ======================= + ======================= 2 failed, 48 passed in 0.12s ======================= .. _`config.cache`: @@ -283,7 +283,7 @@ You can always peek at the content of the cache using the example/value contains: 42 - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== ``--cache-show`` takes an optional argument to specify a glob pattern for filtering: @@ -300,7 +300,7 @@ filtering: example/value contains: 42 - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== Clearing Cache content ---------------------- diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 2a9de0be3..3e744e764 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -91,7 +91,7 @@ of the failing function and hide the other one: test_module.py:12: AssertionError -------------------------- Captured stdout setup --------------------------- setting up - ======================= 1 failed, 1 passed in 0.02s ======================== + ======================= 1 failed, 1 passed in 0.12s ======================== Accessing captured output from a test function --------------------------------------------------- diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 7ecfe7e56..9ffd5285a 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -36,7 +36,7 @@ then you can just invoke ``pytest`` directly: test_example.txt . [100%] - ============================ 1 passed in 0.01s ============================= + ============================ 1 passed in 0.12s ============================= By default, pytest will collect ``test*.txt`` files looking for doctest directives, but you can pass additional globs using the ``--doctest-glob`` option (multi-allowed). @@ -66,7 +66,7 @@ and functions, including from test modules: mymodule.py . [ 50%] test_example.txt . [100%] - ============================ 2 passed in 0.01s ============================= + ============================ 2 passed in 0.12s ============================= You can make these changes permanent in your project by putting them into a pytest.ini file like this: diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index f5acd296f..bb2fbb22a 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -52,7 +52,7 @@ You can then restrict a test run to only run tests marked with ``webtest``: test_server.py::test_send_http PASSED [100%] - ===================== 1 passed, 3 deselected in 0.01s ====================== + ===================== 1 passed, 3 deselected in 0.12s ====================== Or the inverse, running all tests except the webtest ones: @@ -69,7 +69,7 @@ Or the inverse, running all tests except the webtest ones: test_server.py::test_another PASSED [ 66%] test_server.py::TestClass::test_method PASSED [100%] - ===================== 3 passed, 1 deselected in 0.01s ====================== + ===================== 3 passed, 1 deselected in 0.12s ====================== Selecting tests based on their node ID -------------------------------------- @@ -89,7 +89,7 @@ tests based on their module, class, method, or function name: test_server.py::TestClass::test_method PASSED [100%] - ============================ 1 passed in 0.01s ============================= + ============================ 1 passed in 0.12s ============================= You can also select on the class: @@ -104,7 +104,7 @@ You can also select on the class: test_server.py::TestClass::test_method PASSED [100%] - ============================ 1 passed in 0.01s ============================= + ============================ 1 passed in 0.12s ============================= Or select multiple nodes: @@ -120,7 +120,7 @@ Or select multiple nodes: test_server.py::TestClass::test_method PASSED [ 50%] test_server.py::test_send_http PASSED [100%] - ============================ 2 passed in 0.01s ============================= + ============================ 2 passed in 0.12s ============================= .. _node-id: @@ -159,7 +159,7 @@ select tests based on their names: test_server.py::test_send_http PASSED [100%] - ===================== 1 passed, 3 deselected in 0.01s ====================== + ===================== 1 passed, 3 deselected in 0.12s ====================== And you can also run all tests except the ones that match the keyword: @@ -176,7 +176,7 @@ And you can also run all tests except the ones that match the keyword: test_server.py::test_another PASSED [ 66%] test_server.py::TestClass::test_method PASSED [100%] - ===================== 3 passed, 1 deselected in 0.01s ====================== + ===================== 3 passed, 1 deselected in 0.12s ====================== Or to select "http" and "quick" tests: @@ -192,7 +192,7 @@ Or to select "http" and "quick" tests: test_server.py::test_send_http PASSED [ 50%] test_server.py::test_something_quick PASSED [100%] - ===================== 2 passed, 2 deselected in 0.01s ====================== + ===================== 2 passed, 2 deselected in 0.12s ====================== .. note:: @@ -413,7 +413,7 @@ the test needs: test_someenv.py s [100%] - ============================ 1 skipped in 0.00s ============================ + ============================ 1 skipped in 0.12s ============================ and here is one that specifies exactly the environment needed: @@ -428,7 +428,7 @@ and here is one that specifies exactly the environment needed: test_someenv.py . [100%] - ============================ 1 passed in 0.01s ============================= + ============================ 1 passed in 0.12s ============================= The ``--markers`` option always gives you a list of available markers: @@ -499,7 +499,7 @@ The output is as follows: $ pytest -q -s Mark(name='my_marker', args=(,), kwargs={}) . - 1 passed in 0.00s + 1 passed in 0.01s We can see that the custom marker has its argument set extended with the function ``hello_world``. This is the key difference between creating a custom marker as a callable, which invokes ``__call__`` behind the scenes, and using ``with_args``. @@ -551,7 +551,7 @@ Let's run this without capturing output and see what we get: glob args=('class',) kwargs={'x': 2} glob args=('module',) kwargs={'x': 1} . - 1 passed in 0.01s + 1 passed in 0.02s marking platform specific tests with pytest -------------------------------------------------------------- @@ -623,7 +623,7 @@ then you will see two tests skipped and two executed tests as expected: ========================= short test summary info ========================== SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux - ======================= 2 passed, 2 skipped in 0.01s ======================= + ======================= 2 passed, 2 skipped in 0.12s ======================= Note that if you specify a platform via the marker-command line option like this: @@ -638,7 +638,7 @@ Note that if you specify a platform via the marker-command line option like this test_plat.py . [100%] - ===================== 1 passed, 3 deselected in 0.01s ====================== + ===================== 1 passed, 3 deselected in 0.12s ====================== then the unmarked-tests will not be run. It is thus a way to restrict the run to the specific tests. @@ -711,7 +711,7 @@ We can now use the ``-m option`` to select one set: test_module.py:8: in test_interface_complex assert 0 E assert 0 - ===================== 2 failed, 2 deselected in 0.02s ====================== + ===================== 2 failed, 2 deselected in 0.12s ====================== or to select both "event" and "interface" tests: @@ -739,4 +739,4 @@ or to select both "event" and "interface" tests: test_module.py:12: in test_event_simple assert 0 E assert 0 - ===================== 3 failed, 1 deselected in 0.03s ====================== + ===================== 3 failed, 1 deselected in 0.12s ====================== diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index 6699de749..28b20800e 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -41,7 +41,7 @@ now execute the test specification: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ======================= 1 failed, 1 passed in 0.02s ======================== + ======================= 1 failed, 1 passed in 0.12s ======================== .. regendoc:wipe @@ -77,7 +77,7 @@ consulted when reporting in ``verbose`` mode: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ======================= 1 failed, 1 passed in 0.02s ======================== + ======================= 1 failed, 1 passed in 0.12s ======================== .. regendoc:wipe @@ -97,4 +97,4 @@ interesting to just look at the collection tree: - ========================== no tests ran in 0.02s =========================== + ========================== no tests ran in 0.12s =========================== diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index cf99ea472..19cdcc293 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -172,7 +172,7 @@ objects, they are still using the default pytest representation: - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.12s =========================== In ``test_timedistance_v3``, we used ``pytest.param`` to specify the test IDs together with the actual data, instead of listing them separately. @@ -229,7 +229,7 @@ this is a fully self-contained example which you can run with: test_scenarios.py .... [100%] - ============================ 4 passed in 0.01s ============================= + ============================ 4 passed in 0.12s ============================= If you just collect tests you'll also nicely see 'advanced' and 'basic' as variants for the test function: @@ -248,7 +248,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.12s =========================== Note that we told ``metafunc.parametrize()`` that your scenario values should be considered class-scoped. With pytest-2.3 this leads to a @@ -323,7 +323,7 @@ Let's first see how it looks like at collection time: - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== And then when we run the test: @@ -394,7 +394,7 @@ The result of this test will be successful: - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== .. regendoc:wipe @@ -475,10 +475,11 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ssssssssssss......sss...... [100%] + ssssssssssss...ssssssssssss [100%] ========================= short test summary info ========================== - SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found - 12 passed, 15 skipped in 0.62s + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.7' not found + 3 passed, 24 skipped in 0.24s Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- @@ -547,7 +548,7 @@ If you run this with reporting for skips enabled: ========================= short test summary info ========================== SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:13: could not import 'opt2': No module named 'opt2' - ======================= 1 passed, 1 skipped in 0.01s ======================= + ======================= 1 passed, 1 skipped in 0.12s ======================= You'll see that we don't have an ``opt2`` module and thus the second test run of our ``test_func1`` was skipped. A few notes: @@ -609,7 +610,7 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker: test_pytest_param_example.py::test_eval[basic_2+4] PASSED [ 66%] test_pytest_param_example.py::test_eval[basic_6*9] XFAIL [100%] - =============== 2 passed, 15 deselected, 1 xfailed in 0.08s ================ + =============== 2 passed, 15 deselected, 1 xfailed in 0.12s ================ As the result: diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index a718de400..d8261a949 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -158,7 +158,7 @@ The test collection would look like this: - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.12s =========================== You can check for multiple glob patterns by adding a space between the patterns: @@ -221,7 +221,7 @@ You can always peek at the collection tree without running tests like this: - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== .. _customizing-test-collection: @@ -297,7 +297,7 @@ file will be left out: rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 0 items - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.12s =========================== It's also possible to ignore files based on Unix shell-style wildcards by adding patterns to ``collect_ignore_glob``. diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index c024b8616..1c06782f6 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -650,4 +650,4 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a failure_demo.py:282: AssertionError - ============================ 44 failed in 0.26s ============================ + ============================ 44 failed in 0.12s ============================ diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index fea73f4e9..913769c35 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -132,7 +132,7 @@ directory with the above conftest.py: rootdir: $REGENDOC_TMPDIR collected 0 items - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== .. _`excontrolskip`: @@ -201,7 +201,7 @@ and when running it will see a skipped "slow" test: ========================= short test summary info ========================== SKIPPED [1] test_module.py:8: need --runslow option to run - ======================= 1 passed, 1 skipped in 0.01s ======================= + ======================= 1 passed, 1 skipped in 0.12s ======================= Or run it including the ``slow`` marked test: @@ -216,7 +216,7 @@ Or run it including the ``slow`` marked test: test_module.py .. [100%] - ============================ 2 passed in 0.01s ============================= + ============================ 2 passed in 0.12s ============================= Writing well integrated assertion helpers -------------------------------------------------- @@ -358,7 +358,7 @@ which will add the string to the test header accordingly: rootdir: $REGENDOC_TMPDIR collected 0 items - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== .. regendoc:wipe @@ -388,7 +388,7 @@ which will add info only when run with "--v": rootdir: $REGENDOC_TMPDIR collecting ... collected 0 items - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== and nothing when run plainly: @@ -401,7 +401,7 @@ and nothing when run plainly: rootdir: $REGENDOC_TMPDIR collected 0 items - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== profiling test duration -------------------------- @@ -447,7 +447,7 @@ Now we can profile which test functions execute the slowest: 0.30s call test_some_are_slow.py::test_funcslow2 0.20s call test_some_are_slow.py::test_funcslow1 0.10s call test_some_are_slow.py::test_funcfast - ============================ 3 passed in 0.61s ============================= + ============================ 3 passed in 0.12s ============================= incremental testing - test steps --------------------------------------------------- @@ -531,7 +531,7 @@ If we run this: ========================= short test summary info ========================== XFAIL test_step.py::TestUserHandling::test_deletion reason: previous test failed (test_modification) - ================== 1 failed, 2 passed, 1 xfailed in 0.03s ================== + ================== 1 failed, 2 passed, 1 xfailed in 0.12s ================== We'll see that ``test_deletion`` was not executed because ``test_modification`` failed. It is reported as an "expected failure". @@ -644,7 +644,7 @@ We can run this: E assert 0 a/test_db2.py:2: AssertionError - ============= 3 failed, 2 passed, 1 xfailed, 1 error in 0.05s ============== + ============= 3 failed, 2 passed, 1 xfailed, 1 error in 0.12s ============== The two test modules in the ``a`` directory see the same ``db`` fixture instance while the one test in the sister-directory ``b`` doesn't see it. We could of course @@ -733,7 +733,7 @@ and run them: E assert 0 test_module.py:6: AssertionError - ============================ 2 failed in 0.02s ============================= + ============================ 2 failed in 0.12s ============================= you will have a "failures" file which contains the failing test ids: @@ -848,7 +848,7 @@ and run it: E assert 0 test_module.py:19: AssertionError - ======================== 2 failed, 1 error in 0.02s ======================== + ======================== 2 failed, 1 error in 0.12s ======================== You'll see that the fixture finalizers could use the precise reporting information. diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 91b5aca85..3558c79c4 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -96,7 +96,7 @@ marked ``smtp_connection`` fixture function. Running the test looks like this: E assert 0 test_smtpsimple.py:14: AssertionError - ============================ 1 failed in 0.18s ============================= + ============================ 1 failed in 0.12s ============================= In the failure traceback we see that the test function was called with a ``smtp_connection`` argument, the ``smtplib.SMTP()`` instance created by the fixture @@ -258,7 +258,7 @@ inspect what is going on and can now run the tests: E assert 0 test_module.py:13: AssertionError - ============================ 2 failed in 0.20s ============================= + ============================ 2 failed in 0.12s ============================= You see the two ``assert 0`` failing and more importantly you can also see that the same (module-scoped) ``smtp_connection`` object was passed into the @@ -361,7 +361,7 @@ Let's execute it: $ pytest -s -q --tb=no FFteardown smtp - 2 failed in 0.20s + 2 failed in 0.79s We see that the ``smtp_connection`` instance is finalized after the two tests finished execution. Note that if we decorated our fixture @@ -515,7 +515,7 @@ again, nothing much has changed: $ pytest -s -q --tb=no FFfinalizing (smtp.gmail.com) - 2 failed in 0.21s + 2 failed in 0.77s Let's quickly create another test module that actually sets the server URL in its module namespace: @@ -692,7 +692,7 @@ So let's just do another run: test_module.py:13: AssertionError ------------------------- Captured stdout teardown ------------------------- finalizing - 4 failed in 0.89s + 4 failed in 1.69s We see that our two test functions each ran twice, against the different ``smtp_connection`` instances. Note also, that with the ``mail.python.org`` @@ -771,7 +771,7 @@ Running the above tests results in the following test IDs being used: - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.12s =========================== .. _`fixture-parametrize-marks`: @@ -812,7 +812,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``: test_fixture_marks.py::test_data[1] PASSED [ 66%] test_fixture_marks.py::test_data[2] SKIPPED [100%] - ======================= 2 passed, 1 skipped in 0.01s ======================= + ======================= 2 passed, 1 skipped in 0.12s ======================= .. _`interdependent fixtures`: @@ -861,7 +861,7 @@ Here we declare an ``app`` fixture which receives the previously defined test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%] test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100%] - ============================ 2 passed in 0.44s ============================= + ============================ 2 passed in 0.12s ============================= Due to the parametrization of ``smtp_connection``, the test will run twice with two different ``App`` instances and respective smtp servers. There is no @@ -971,7 +971,7 @@ Let's run the tests in verbose mode and with looking at the print-output: TEARDOWN modarg mod2 - ============================ 8 passed in 0.01s ============================= + ============================ 8 passed in 0.12s ============================= You can see that the parametrized module-scoped ``modarg`` resource caused an ordering of test execution that lead to the fewest possible "active" resources. diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 38a361818..455e93622 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -69,7 +69,7 @@ That’s it. You can now execute the test function: E + where 4 = func(3) test_sample.py:6: AssertionError - ============================ 1 failed in 0.02s ============================= + ============================ 1 failed in 0.12s ============================= This test returns a failure report because ``func(3)`` does not return ``5``. @@ -108,7 +108,7 @@ Execute the test function with “quiet” reporting mode: $ pytest -q test_sysexit.py . [100%] - 1 passed in 0.00s + 1 passed in 0.01s Group multiple tests in a class -------------------------------------------------------------- diff --git a/doc/en/index.rst b/doc/en/index.rst index 65b4631cd..cb66f9dd3 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -44,7 +44,7 @@ To execute it: E + where 4 = inc(3) test_sample.py:6: AssertionError - ============================ 1 failed in 0.02s ============================= + ============================ 1 failed in 0.12s ============================= Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See :ref:`Getting Started ` for more examples. diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index 85f233be3..e02ff2e53 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -75,7 +75,7 @@ them in turn: E + where 54 = eval('6*9') test_expectation.py:6: AssertionError - ======================= 1 failed, 2 passed in 0.02s ======================== + ======================= 1 failed, 2 passed in 0.12s ======================== .. note:: @@ -128,7 +128,7 @@ Let's run this: test_expectation.py ..x [100%] - ======================= 2 passed, 1 xfailed in 0.02s ======================= + ======================= 2 passed, 1 xfailed in 0.12s ======================= The one parameter set which caused a failure previously now shows up as an "xfailed (expected to fail)" test. diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index d271b0b2a..9ef1e8a36 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -371,7 +371,7 @@ Running it with the report-on-xfail option gives this output: XFAIL xfail_demo.py::test_hello6 reason: reason XFAIL xfail_demo.py::test_hello7 - ============================ 7 xfailed in 0.05s ============================ + ============================ 7 xfailed in 0.12s ============================ .. _`skip/xfail with parametrize`: diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index c231e76a1..b9faef4dc 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -64,7 +64,7 @@ Running this would result in a passed test except for the last E assert 0 test_tmp_path.py:13: AssertionError - ============================ 1 failed in 0.02s ============================= + ============================ 1 failed in 0.12s ============================= .. _`tmp_path_factory example`: @@ -133,7 +133,7 @@ Running this would result in a passed test except for the last E assert 0 test_tmpdir.py:9: AssertionError - ============================ 1 failed in 0.02s ============================= + ============================ 1 failed in 0.12s ============================= .. _`tmpdir factory example`: diff --git a/doc/en/unittest.rst b/doc/en/unittest.rst index 4f0a279a2..dc55cc0ed 100644 --- a/doc/en/unittest.rst +++ b/doc/en/unittest.rst @@ -166,7 +166,7 @@ the ``self.db`` values in the traceback: E assert 0 test_unittest_db.py:13: AssertionError - ============================ 2 failed in 0.02s ============================= + ============================ 2 failed in 0.12s ============================= This default pytest traceback shows that the two test methods share the same ``self.db`` instance which was our intention diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 78702ea86..777c14be2 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -247,7 +247,7 @@ Example: XPASS test_example.py::test_xpass always xfail ERROR test_example.py::test_error - assert 0 FAILED test_example.py::test_fail - assert 0 - == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.03s === + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s === The ``-r`` options accepts a number of characters after it, with ``a`` used above meaning "all except passes". @@ -297,7 +297,7 @@ More than one character can be used, so for example to only see failed and skipp ========================= short test summary info ========================== FAILED test_example.py::test_fail - assert 0 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.03s === + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s === Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had captured output: @@ -336,7 +336,7 @@ captured output: ok ========================= short test summary info ========================== PASSED test_example.py::test_ok - == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.03s === + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s === .. _pdb-option: diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index b8a2df270..90245a0d4 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -41,7 +41,7 @@ Running pytest now produces this output: warnings.warn(UserWarning("api v1, should use functions from v2")) -- Docs: https://docs.pytest.org/en/latest/warnings.html - ====================== 1 passed, 1 warnings in 0.00s ======================= + ====================== 1 passed, 1 warnings in 0.12s ======================= The ``-W`` flag can be passed to control which warnings will be displayed or even turn them into errors: From f8dd6349c13d47223f6c280f8c755cc0e1196d41 Mon Sep 17 00:00:00 2001 From: Michael Goerz Date: Fri, 30 Aug 2019 15:34:03 -0400 Subject: [PATCH 15/27] Fix "lexer" being used when uploading to bpaste.net Closes #5806. --- changelog/5806.bugfix.rst | 1 + src/_pytest/pastebin.py | 2 +- testing/test_pastebin.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog/5806.bugfix.rst diff --git a/changelog/5806.bugfix.rst b/changelog/5806.bugfix.rst new file mode 100644 index 000000000..ec887768d --- /dev/null +++ b/changelog/5806.bugfix.rst @@ -0,0 +1 @@ +Fix "lexer" being used when uploading to bpaste.net from ``--pastebin`` to "text". diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 38ff97f2d..77b4e2621 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -65,7 +65,7 @@ def create_new_paste(contents): from urllib.request import urlopen from urllib.parse import urlencode - params = {"code": contents, "lexer": "python3", "expiry": "1week"} + params = {"code": contents, "lexer": "text", "expiry": "1week"} url = "https://bpaste.net" try: response = ( diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index a1bc0622e..86a42f9e8 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -165,7 +165,7 @@ class TestPaste: assert len(mocked_urlopen) == 1 url, data = mocked_urlopen[0] assert type(data) is bytes - lexer = "python3" + lexer = "text" assert url == "https://bpaste.net" assert "lexer=%s" % lexer in data.decode() assert "code=full-paste-contents" in data.decode() From bc163605abf4d89cf2c9213fd3291707c3b67453 Mon Sep 17 00:00:00 2001 From: Gene Wood Date: Wed, 4 Sep 2019 09:18:10 -0700 Subject: [PATCH 16/27] Fix anchor link from Good Practices to Pythonpath doc --- doc/en/goodpractices.rst | 2 +- doc/en/pythonpath.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/en/goodpractices.rst b/doc/en/goodpractices.rst index 0aa0dc97c..4da9d1bca 100644 --- a/doc/en/goodpractices.rst +++ b/doc/en/goodpractices.rst @@ -88,7 +88,7 @@ This has the following benefits: .. note:: - See :ref:`pythonpath` for more information about the difference between calling ``pytest`` and + See :ref:`pytest vs python -m pytest` for more information about the difference between calling ``pytest`` and ``python -m pytest``. Note that using this scheme your test files must have **unique names**, because diff --git a/doc/en/pythonpath.rst b/doc/en/pythonpath.rst index 113c95c30..0054acc59 100644 --- a/doc/en/pythonpath.rst +++ b/doc/en/pythonpath.rst @@ -72,6 +72,8 @@ imported in the global import namespace. This is also discussed in details in :ref:`test discovery`. +.. _`pytest vs python -m pytest`: + Invoking ``pytest`` versus ``python -m pytest`` ----------------------------------------------- From ca3884d9bbf97cd0382d427be1c018051a976b74 Mon Sep 17 00:00:00 2001 From: Gene Wood Date: Wed, 4 Sep 2019 09:21:10 -0700 Subject: [PATCH 17/27] Add Gene Wood to authors --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 4ea26481c..a64c95acb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -98,6 +98,7 @@ Feng Ma Florian Bruhin Floris Bruynooghe Gabriel Reis +Gene Wood George Kussumoto Georgy Dyuldin Graham Horler From d049b353974a8f091939775ef34106df7d9ef733 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 5 Sep 2019 18:06:47 +0300 Subject: [PATCH 18/27] Fix for Python 4: replace unsafe PY3 with PY2 --- testing/test_pdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 8d327cbb3..3c56f40e1 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -853,7 +853,7 @@ class TestDebuggingBreakpoints: Test that supports breakpoint global marks on Python 3.7+ and not on CPython 3.5, 2.7 """ - if sys.version_info.major == 3 and sys.version_info.minor >= 7: + if sys.version_info >= (3, 7): assert SUPPORTS_BREAKPOINT_BUILTIN is True if sys.version_info.major == 3 and sys.version_info.minor == 5: assert SUPPORTS_BREAKPOINT_BUILTIN is False From f1b605c95e498689b74eefe81f1f561c20a63659 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 6 Sep 2019 12:29:02 +0200 Subject: [PATCH 19/27] ci: Travis: do not test with 3.5.0 This causes flaky test failures (crashes). Closes https://github.com/pytest-dev/pytest/issues/5795. --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 41c23cfbc..5de40f3a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,9 +42,8 @@ jobs: - env: TOXENV=pypy3-xdist python: 'pypy3' - - env: TOXENV=py35 - dist: trusty - python: '3.5.0' + - env: TOXENV=py35-xdist + python: '3.5' # Coverage for: # - pytester's LsofFdLeakChecker From 9d7b919c7de45ff81ea52bb289a8e4bd192b1c27 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 7 Sep 2019 16:48:27 -0700 Subject: [PATCH 20/27] Fix pypy3.6 on windows --- changelog/5807.bugfix.rst | 1 + src/_pytest/capture.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog/5807.bugfix.rst diff --git a/changelog/5807.bugfix.rst b/changelog/5807.bugfix.rst new file mode 100644 index 000000000..bfa0ffbcf --- /dev/null +++ b/changelog/5807.bugfix.rst @@ -0,0 +1 @@ +Fix pypy3.6 (nightly) on windows. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index f89aaefba..e4e58b32c 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -789,7 +789,11 @@ def _py36_windowsconsoleio_workaround(stream): See https://github.com/pytest-dev/py/issues/103 """ - if not sys.platform.startswith("win32") or sys.version_info[:2] < (3, 6): + if ( + not sys.platform.startswith("win32") + or sys.version_info[:2] < (3, 6) + or hasattr(sys, "pypy_version_info") + ): return # bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666) From f0d538329ce977b1e280ae3bf6451fec530189de Mon Sep 17 00:00:00 2001 From: Gene Wood Date: Mon, 9 Sep 2019 12:14:09 -0700 Subject: [PATCH 21/27] Update doc regarding pytest.raises Remove reference to the `message` argument in the docs as it was deprecated in #4539 --- doc/en/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 991050c23..b6df27f9c 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -59,7 +59,7 @@ pytest.raises **Tutorial**: :ref:`assertraises`. -.. autofunction:: pytest.raises(expected_exception: Exception, [match], [message]) +.. autofunction:: pytest.raises(expected_exception: Exception, [match]) :with: excinfo pytest.deprecated_call From 8f2f51be6d03740436501524dc7d2e9ae1503e69 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 11 Sep 2019 14:07:06 -0700 Subject: [PATCH 22/27] Clarify docs by showing tox.ini considered before setup.cfg --- doc/en/customize.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/customize.rst b/doc/en/customize.rst index 77217e9d2..3471463a3 100644 --- a/doc/en/customize.rst +++ b/doc/en/customize.rst @@ -107,8 +107,8 @@ check for ini-files as follows: # first look for pytest.ini files path/pytest.ini - path/setup.cfg # must also contain [tool:pytest] section to match path/tox.ini # must also contain [pytest] section to match + path/setup.cfg # must also contain [tool:pytest] section to match pytest.ini ... # all the way down to the root From cf5b544db35478afe540eb78d75ef97ed798e1f1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 11 Sep 2019 19:37:42 -0300 Subject: [PATCH 23/27] Revert "Merge pull request #5792 from dynatrace-oss-contrib/bugfix/badcase" This reverts commit 955e54221008aba577ecbaefa15679f6777d3bf8, reversing changes made to 0215bcd84e900d9271558df98bed89f4b96187f8. Will attempt a simpler approach --- AUTHORS | 1 - src/_pytest/config/__init__.py | 16 ++++++---------- src/_pytest/pathlib.py | 10 ---------- testing/test_conftest.py | 25 ++++--------------------- 4 files changed, 10 insertions(+), 42 deletions(-) diff --git a/AUTHORS b/AUTHORS index a64c95acb..ff47f9d7c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -56,7 +56,6 @@ Charnjit SiNGH (CCSJ) Chris Lamb Christian Boelsen Christian Fetzer -Christian Neumüller Christian Theunert Christian Tismer Christopher Gilling diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 3a0eca546..b861563e9 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -30,7 +30,6 @@ from _pytest._code import filter_traceback from _pytest.compat import importlib_metadata from _pytest.outcomes import fail from _pytest.outcomes import Skipped -from _pytest.pathlib import unique_path from _pytest.warning_types import PytestConfigWarning hookimpl = HookimplMarker("pytest") @@ -367,7 +366,7 @@ class PytestPluginManager(PluginManager): """ current = py.path.local() self._confcutdir = ( - unique_path(current.join(namespace.confcutdir, abs=True)) + current.join(namespace.confcutdir, abs=True) if namespace.confcutdir else None ) @@ -406,18 +405,19 @@ class PytestPluginManager(PluginManager): else: directory = path - directory = unique_path(directory) - # XXX these days we may rather want to use config.rootdir # and allow users to opt into looking into the rootdir parent # directories instead of requiring to specify confcutdir clist = [] - for parent in directory.parts(): + for parent in directory.realpath().parts(): if self._confcutdir and self._confcutdir.relto(parent): continue conftestpath = parent.join("conftest.py") if conftestpath.isfile(): - mod = self._importconftest(conftestpath) + # Use realpath to avoid loading the same conftest twice + # with build systems that create build directories containing + # symlinks to actual files. + mod = self._importconftest(conftestpath.realpath()) clist.append(mod) self._dirpath2confmods[directory] = clist return clist @@ -432,10 +432,6 @@ class PytestPluginManager(PluginManager): raise KeyError(name) def _importconftest(self, conftestpath): - # Use realpath to avoid loading the same conftest twice - # with build systems that create build directories containing - # symlinks to actual files. - conftestpath = unique_path(conftestpath) try: return self._conftestpath2mod[conftestpath] except KeyError: diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 0403b6947..19f9c062f 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -11,7 +11,6 @@ from functools import partial from os.path import expanduser from os.path import expandvars from os.path import isabs -from os.path import normcase from os.path import sep from posixpath import sep as posix_sep @@ -335,12 +334,3 @@ def fnmatch_ex(pattern, path): def parts(s): parts = s.split(sep) return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} - - -def unique_path(path): - """Returns a unique path in case-insensitive (but case-preserving) file - systems such as Windows. - - This is needed only for ``py.path.local``; ``pathlib.Path`` handles this - natively with ``resolve()``.""" - return type(path)(normcase(str(path.realpath()))) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 9888f5457..447416f10 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,4 +1,3 @@ -import os.path import textwrap import py @@ -6,7 +5,6 @@ import py import pytest from _pytest.config import PytestPluginManager from _pytest.main import ExitCode -from _pytest.pathlib import unique_path def ConftestWithSetinitial(path): @@ -143,11 +141,11 @@ def test_conftestcutdir(testdir): # but we can still import a conftest directly conftest._importconftest(conf) values = conftest._getconftestmodules(conf.dirpath()) - assert values[0].__file__.startswith(str(unique_path(conf))) + assert values[0].__file__.startswith(str(conf)) # and all sub paths get updated properly values = conftest._getconftestmodules(p) assert len(values) == 1 - assert values[0].__file__.startswith(str(unique_path(conf))) + assert values[0].__file__.startswith(str(conf)) def test_conftestcutdir_inplace_considered(testdir): @@ -156,7 +154,7 @@ def test_conftestcutdir_inplace_considered(testdir): conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath()) values = conftest._getconftestmodules(conf.dirpath()) assert len(values) == 1 - assert values[0].__file__.startswith(str(unique_path(conf))) + assert values[0].__file__.startswith(str(conf)) @pytest.mark.parametrize("name", "test tests whatever .dotdir".split()) @@ -166,7 +164,7 @@ def test_setinitial_conftest_subdirs(testdir, name): conftest = PytestPluginManager() conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir) if name not in ("whatever", ".dotdir"): - assert unique_path(subconftest) in conftest._conftestpath2mod + assert subconftest in conftest._conftestpath2mod assert len(conftest._conftestpath2mod) == 1 else: assert subconftest not in conftest._conftestpath2mod @@ -277,21 +275,6 @@ def test_conftest_symlink_files(testdir): assert result.ret == ExitCode.OK -@pytest.mark.skipif( - os.path.normcase("x") != os.path.normcase("X"), - reason="only relevant for case insensitive file systems", -) -def test_conftest_badcase(testdir): - """Check conftest.py loading when directory casing is wrong.""" - testdir.tmpdir.mkdir("JenkinsRoot").mkdir("test") - source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""} - testdir.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()}) - - testdir.tmpdir.join("jenkinsroot/test").chdir() - result = testdir.runpytest() - assert result.ret == ExitCode.NO_TESTS_COLLECTED - - def test_no_conftest(testdir): testdir.makeconftest("assert 0") result = testdir.runpytest("--noconftest") From b48f51eb031f1b35b7fd4cd5d9da774541e10ec1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 11 Sep 2019 20:09:08 -0300 Subject: [PATCH 24/27] Use Path() objects to store conftest files Using Path().resolve() is better than py.path.realpath because it resolves to the correct path/drive in case-insensitive file systems (#5792): >>> from py.path import local >>> from pathlib import Path >>> >>> local('d:\\projects').realpath() local('d:\\projects') >>> Path('d:\\projects').resolve() WindowsPath('D:/projects') Fix #5819 --- src/_pytest/config/__init__.py | 15 +++++++++------ testing/test_conftest.py | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index b861563e9..e39c63c4b 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -414,10 +414,7 @@ class PytestPluginManager(PluginManager): continue conftestpath = parent.join("conftest.py") if conftestpath.isfile(): - # Use realpath to avoid loading the same conftest twice - # with build systems that create build directories containing - # symlinks to actual files. - mod = self._importconftest(conftestpath.realpath()) + mod = self._importconftest(conftestpath) clist.append(mod) self._dirpath2confmods[directory] = clist return clist @@ -432,8 +429,14 @@ class PytestPluginManager(PluginManager): raise KeyError(name) def _importconftest(self, conftestpath): + # Use a resolved Path object as key to avoid loading the same conftest twice + # with build systems that create build directories containing + # symlinks to actual files. + # Using Path().resolve() is better than py.path.realpath because + # it resolves to the correct path/drive in case-insensitive file systems (#5792) + key = Path(str(conftestpath)).resolve() try: - return self._conftestpath2mod[conftestpath] + return self._conftestpath2mod[key] except KeyError: pkgpath = conftestpath.pypkgpath() if pkgpath is None: @@ -450,7 +453,7 @@ class PytestPluginManager(PluginManager): raise ConftestImportFailure(conftestpath, sys.exc_info()) self._conftest_plugins.add(mod) - self._conftestpath2mod[conftestpath] = mod + self._conftestpath2mod[key] = mod dirpath = conftestpath.dirpath() if dirpath in self._dirpath2confmods: for path, mods in self._dirpath2confmods.items(): diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 447416f10..3f08ee381 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,4 +1,6 @@ +import os import textwrap +from pathlib import Path import py @@ -163,11 +165,12 @@ def test_setinitial_conftest_subdirs(testdir, name): subconftest = sub.ensure("conftest.py") conftest = PytestPluginManager() conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir) + key = Path(str(subconftest)).resolve() if name not in ("whatever", ".dotdir"): - assert subconftest in conftest._conftestpath2mod + assert key in conftest._conftestpath2mod assert len(conftest._conftestpath2mod) == 1 else: - assert subconftest not in conftest._conftestpath2mod + assert key not in conftest._conftestpath2mod assert len(conftest._conftestpath2mod) == 0 @@ -275,6 +278,31 @@ def test_conftest_symlink_files(testdir): assert result.ret == ExitCode.OK +@pytest.mark.skipif( + os.path.normcase("x") != os.path.normcase("X"), + reason="only relevant for case insensitive file systems", +) +def test_conftest_badcase(testdir): + """Check conftest.py loading when directory casing is wrong (#5792).""" + testdir.tmpdir.mkdir("JenkinsRoot").mkdir("test") + source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""} + testdir.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()}) + + testdir.tmpdir.join("jenkinsroot/test").chdir() + result = testdir.runpytest() + assert result.ret == ExitCode.NO_TESTS_COLLECTED + + +def test_conftest_uppercase(testdir): + """Check conftest.py whose qualified name contains uppercase characters (#5819)""" + source = {"__init__.py": "", "Foo/conftest.py": "", "Foo/__init__.py": ""} + testdir.makepyfile(**source) + + testdir.tmpdir.chdir() + result = testdir.runpytest() + assert result.ret == ExitCode.NO_TESTS_COLLECTED + + def test_no_conftest(testdir): testdir.makeconftest("assert 0") result = testdir.runpytest("--noconftest") From 05850d73bd9dc77ad2c90cea9767523a2e9d18e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 27 Aug 2019 16:25:24 +0200 Subject: [PATCH 25/27] =?UTF-8?q?Re-introduce=20Christian=20Neum=C3=BCller?= =?UTF-8?q?=20to=20AUTHORS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The introduction was reverted by cd29d56 --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index ff47f9d7c..a64c95acb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -56,6 +56,7 @@ Charnjit SiNGH (CCSJ) Chris Lamb Christian Boelsen Christian Fetzer +Christian Neumüller Christian Theunert Christian Tismer Christopher Gilling From 5c3b4a6f528f206da449d3c379e33f549f3f66e8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 11 Sep 2019 21:57:48 -0300 Subject: [PATCH 26/27] Add CHANGELOG entry for #5792 --- changelog/5819.bugfix.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog/5819.bugfix.rst diff --git a/changelog/5819.bugfix.rst b/changelog/5819.bugfix.rst new file mode 100644 index 000000000..aa953429a --- /dev/null +++ b/changelog/5819.bugfix.rst @@ -0,0 +1,2 @@ +Windows: Fix regression with conftest whose qualified name contains uppercase +characters (introduced by #5792). From f832ac3316e9f733990f619af8c0f0f4abdbdb69 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 1 Sep 2019 19:17:02 +0200 Subject: [PATCH 27/27] Handle --fulltrace with pytest.raises This changes `_repr_failure_py` to use `tbfilter=False` always. --- changelog/5811.bugfix.rst | 1 + src/_pytest/nodes.py | 4 +--- testing/code/test_excinfo.py | 7 +++++++ 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 changelog/5811.bugfix.rst diff --git a/changelog/5811.bugfix.rst b/changelog/5811.bugfix.rst new file mode 100644 index 000000000..a052eb03a --- /dev/null +++ b/changelog/5811.bugfix.rst @@ -0,0 +1 @@ +Handle ``--fulltrace`` correctly with ``pytest.raises``. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 9b78dca38..c0a88fbbc 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -245,7 +245,6 @@ class Node: fm = self.session._fixturemanager if excinfo.errisinstance(fm.FixtureLookupError): return excinfo.value.formatrepr() - tbfilter = True if self.config.getoption("fulltrace", False): style = "long" else: @@ -253,7 +252,6 @@ class Node: self._prunetraceback(excinfo) if len(excinfo.traceback) == 0: excinfo.traceback = tb - tbfilter = False # prunetraceback already does it if style == "auto": style = "long" # XXX should excinfo.getrepr record all data and toterminal() process it? @@ -279,7 +277,7 @@ class Node: abspath=abspath, showlocals=self.config.getoption("showlocals", False), style=style, - tbfilter=tbfilter, + tbfilter=False, # pruned already, or in --fulltrace mode. truncate_locals=truncate_locals, ) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index bdd7a5a6f..5673b811b 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -399,6 +399,13 @@ def test_match_raises_error(testdir): result = testdir.runpytest() assert result.ret != 0 result.stdout.fnmatch_lines(["*AssertionError*Pattern*[123]*not found*"]) + assert "__tracebackhide__ = True" not in result.stdout.str() + + result = testdir.runpytest("--fulltrace") + assert result.ret != 0 + result.stdout.fnmatch_lines( + ["*__tracebackhide__ = True*", "*AssertionError*Pattern*[123]*not found*"] + ) class TestFormattedExcinfo: