From 9f3bfe82cf1200f7a4249a0fbc1e7db2c8369e63 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 16 Aug 2019 10:08:18 +0300 Subject: [PATCH 01/19] Fix TypeError when importing pytest on Python 3.5.0 and 3.5.1 The typing module on these versions have these issues: - `typing.Pattern` cannot appear in a Union since it is not considered a class. - `@overload` is not supported in runtime. (On the other hand, mypy doesn't support putting it under `if False`, so we need some runtime hack). Refs #5751. --- changelog/5751.bugfix.rst | 1 + src/_pytest/_code/code.py | 2 +- src/_pytest/python_api.py | 17 ++++++++++++----- src/_pytest/recwarn.py | 18 ++++++++++++------ 4 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 changelog/5751.bugfix.rst diff --git a/changelog/5751.bugfix.rst b/changelog/5751.bugfix.rst new file mode 100644 index 000000000..879909c8b --- /dev/null +++ b/changelog/5751.bugfix.rst @@ -0,0 +1 @@ +Fixed ``TypeError`` when importing pytest on Python 3.5.0 and 3.5.1. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 7d72234e7..a0f4d15ce 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -591,7 +591,7 @@ class ExceptionInfo(Generic[_E]): ) return fmt.repr_excinfo(self) - def match(self, regexp: Union[str, Pattern]) -> bool: + def match(self, regexp: "Union[str, Pattern]") -> bool: """ Check whether the regular expression 'regexp' is found in the string representation of the exception using ``re.search``. If it matches diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index fbc3d914e..c5e06d491 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -1,6 +1,7 @@ import inspect import math import pprint +import sys from collections.abc import Iterable from collections.abc import Mapping from collections.abc import Sized @@ -28,6 +29,12 @@ from _pytest.outcomes import fail if False: # TYPE_CHECKING from typing import Type # noqa: F401 (used in type string) +if sys.version_info <= (3, 5, 1): + + def overload(f): # noqa: F811 + return f + + BASE_TYPE = (type, STRING_TYPES) @@ -547,12 +554,12 @@ _E = TypeVar("_E", bound=BaseException) def raises( expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], *, - match: Optional[Union[str, Pattern]] = ... + match: "Optional[Union[str, Pattern]]" = ... ) -> "RaisesContext[_E]": ... # pragma: no cover -@overload +@overload # noqa: F811 def raises( expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], func: Callable, @@ -563,10 +570,10 @@ def raises( ... # pragma: no cover -def raises( +def raises( # noqa: F811 expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], *args: Any, - match: Optional[Union[str, Pattern]] = None, + match: Optional[Union[str, "Pattern"]] = None, **kwargs: Any ) -> Union["RaisesContext[_E]", Optional[_pytest._code.ExceptionInfo[_E]]]: r""" @@ -724,7 +731,7 @@ class RaisesContext(Generic[_E]): self, expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], message: str, - match_expr: Optional[Union[str, Pattern]] = None, + match_expr: Optional[Union[str, "Pattern"]] = None, ) -> None: self.expected_exception = expected_exception self.message = message diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 19e3938c3..27519fd46 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -1,5 +1,6 @@ """ recording warnings during test function execution. """ import re +import sys import warnings from types import TracebackType from typing import Any @@ -18,6 +19,11 @@ from _pytest.outcomes import fail if False: # TYPE_CHECKING from typing import Type +if sys.version_info <= (3, 5, 1): + + def overload(f): # noqa: F811 + return f + @yield_fixture def recwarn(): @@ -58,26 +64,26 @@ def deprecated_call(func=None, *args, **kwargs): def warns( expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]], *, - match: Optional[Union[str, Pattern]] = ... + match: "Optional[Union[str, Pattern]]" = ... ) -> "WarningsChecker": ... # pragma: no cover -@overload +@overload # noqa: F811 def warns( expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]], func: Callable, *args: Any, - match: Optional[Union[str, Pattern]] = ..., + match: Optional[Union[str, "Pattern"]] = ..., **kwargs: Any ) -> Union[Any]: ... # pragma: no cover -def warns( +def warns( # noqa: F811 expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]], *args: Any, - match: Optional[Union[str, Pattern]] = None, + match: Optional[Union[str, "Pattern"]] = None, **kwargs: Any ) -> Union["WarningsChecker", Any]: r"""Assert that code raises a particular class of warning. @@ -207,7 +213,7 @@ class WarningsChecker(WarningsRecorder): expected_warning: Optional[ Union["Type[Warning]", Tuple["Type[Warning]", ...]] ] = None, - match_expr: Optional[Union[str, Pattern]] = None, + match_expr: Optional[Union[str, "Pattern"]] = None, ) -> None: super().__init__() From 1e3205e7cfff63ab87fce4adfd4118d8fe1543d6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 17 Aug 2019 22:07:48 +0200 Subject: [PATCH 02/19] ci: Travis: use 3.5.0 Ref: https://github.com/pytest-dev/pytest/pull/5752#issuecomment-522241225 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5de40f3a4..c1f7ad357 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,7 +43,8 @@ jobs: python: 'pypy3' - env: TOXENV=py35-xdist - python: '3.5' + dist: trusty + python: '3.5.0' # Coverage for: # - pytester's LsofFdLeakChecker From a7ede64f4262d1acbc4d50442d980f54631a14c7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 18 Aug 2019 14:54:52 -0700 Subject: [PATCH 03/19] Move `@overload` to compat --- .pre-commit-config.yaml | 2 +- src/_pytest/compat.py | 7 +++++++ src/_pytest/python_api.py | 8 +------- src/_pytest/recwarn.py | 8 +------- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4d127d3c5..e9a970ca7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: hooks: - id: flake8 language_version: python3 - additional_dependencies: [flake8-typing-imports] + additional_dependencies: [flake8-typing-imports==1.3.0] - repo: https://github.com/asottile/reorder_python_imports rev: v1.4.0 hooks: diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 2d11231a4..596a8fd0f 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -9,6 +9,7 @@ import sys from contextlib import contextmanager from inspect import Parameter from inspect import signature +from typing import overload import attr import py @@ -347,3 +348,9 @@ class FuncargnamesCompatAttr: warnings.warn(FUNCARGNAMES, stacklevel=2) return self.fixturenames + + +if sys.version_info < (3, 5, 2): + + def overload(f): # noqa: F811 + return f diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index c5e06d491..f03d45ab7 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -1,7 +1,6 @@ import inspect import math import pprint -import sys from collections.abc import Iterable from collections.abc import Mapping from collections.abc import Sized @@ -14,7 +13,6 @@ from typing import Callable from typing import cast from typing import Generic from typing import Optional -from typing import overload from typing import Pattern from typing import Tuple from typing import TypeVar @@ -23,17 +21,13 @@ from typing import Union from more_itertools.more import always_iterable import _pytest._code +from _pytest.compat import overload from _pytest.compat import STRING_TYPES from _pytest.outcomes import fail if False: # TYPE_CHECKING from typing import Type # noqa: F401 (used in type string) -if sys.version_info <= (3, 5, 1): - - def overload(f): # noqa: F811 - return f - BASE_TYPE = (type, STRING_TYPES) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 27519fd46..58076d66b 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -1,6 +1,5 @@ """ recording warnings during test function execution. """ import re -import sys import warnings from types import TracebackType from typing import Any @@ -8,22 +7,17 @@ from typing import Callable from typing import Iterator from typing import List from typing import Optional -from typing import overload from typing import Pattern from typing import Tuple from typing import Union +from _pytest.compat import overload from _pytest.fixtures import yield_fixture from _pytest.outcomes import fail if False: # TYPE_CHECKING from typing import Type -if sys.version_info <= (3, 5, 1): - - def overload(f): # noqa: F811 - return f - @yield_fixture def recwarn(): From c049fd85abae2c03a83e4cddad824fb6a12c3b40 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 19 Aug 2019 22:07:53 +0200 Subject: [PATCH 04/19] Remove cancelled training --- doc/en/talks.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/en/talks.rst b/doc/en/talks.rst index f66192817..eb1eadbe1 100644 --- a/doc/en/talks.rst +++ b/doc/en/talks.rst @@ -4,7 +4,6 @@ Talks and Tutorials .. sidebar:: Next Open Trainings - - `Training at Workshoptage 2019 `_ (German), 10th September 2019, Rapperswil, Switzerland. - `3 day hands-on workshop covering pytest, tox and devpi: "Professional Testing with Python" `_ (English), October 21 - 23, 2019, Leipzig, Germany. .. _`funcargs`: funcargs.html From cec2183aebe8106f740b2891422d5818dd01a399 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 20 Aug 2019 11:19:25 +0300 Subject: [PATCH 05/19] Add workaround for test_raises_cyclic_reference in Python 3.5.{0,1} --- testing/python/raises.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/testing/python/raises.py b/testing/python/raises.py index 668be57fc..2b7e92615 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -159,13 +159,19 @@ class TestRaises: """ Ensure pytest.raises does not leave a reference cycle (#1965). """ - import gc class T: def __call__(self): + # Early versions of Python 3.5 have some bug causing the + # __call__ frame to still refer to t even after everything + # is done. This makes the test pass for them. + if sys.version_info < (3, 5, 2): + del self raise ValueError t = T() + refcount = sys.getrefcount(t) + if method == "function": pytest.raises(ValueError, t) else: @@ -175,14 +181,7 @@ class TestRaises: # ensure both forms of pytest.raises don't leave exceptions in sys.exc_info() assert sys.exc_info() == (None, None, None) - del t - # Make sure this does get updated in locals dict - # otherwise it could keep a reference - locals() - - # ensure the t instance is not stuck in a cyclic reference - for o in gc.get_objects(): - assert type(o) is not T + assert sys.getrefcount(t) == refcount def test_raises_match(self): msg = r"with base \d+" From a7c235732a10ac94f5c0881de49419d234fe2caf Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 20 Aug 2019 11:43:29 +0300 Subject: [PATCH 06/19] Pypy doesn't have sys.getrefcount(), so go back to gc --- testing/python/raises.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testing/python/raises.py b/testing/python/raises.py index 2b7e92615..4607ef327 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -159,6 +159,7 @@ class TestRaises: """ Ensure pytest.raises does not leave a reference cycle (#1965). """ + import gc class T: def __call__(self): @@ -170,7 +171,7 @@ class TestRaises: raise ValueError t = T() - refcount = sys.getrefcount(t) + refcount = len(gc.get_referrers(t)) if method == "function": pytest.raises(ValueError, t) @@ -181,7 +182,7 @@ class TestRaises: # ensure both forms of pytest.raises don't leave exceptions in sys.exc_info() assert sys.exc_info() == (None, None, None) - assert sys.getrefcount(t) == refcount + assert refcount == len(gc.get_referrers(t)) def test_raises_match(self): msg = r"with base \d+" From 43eab917a1d174808f1975364f64214f98b094a8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 20 Aug 2019 15:41:32 +0300 Subject: [PATCH 07/19] Fix coverage --- src/_pytest/compat.py | 2 +- testing/python/raises.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 596a8fd0f..2d6e8eb6b 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -350,7 +350,7 @@ class FuncargnamesCompatAttr: return self.fixturenames -if sys.version_info < (3, 5, 2): +if sys.version_info < (3, 5, 2): # pragma: no cover def overload(f): # noqa: F811 return f diff --git a/testing/python/raises.py b/testing/python/raises.py index 4607ef327..28b0715c0 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -166,7 +166,7 @@ class TestRaises: # Early versions of Python 3.5 have some bug causing the # __call__ frame to still refer to t even after everything # is done. This makes the test pass for them. - if sys.version_info < (3, 5, 2): + if sys.version_info < (3, 5, 2): # pragma: no cover del self raise ValueError From b135f5af8d9b3a2a3904a55ec51b55db737c97d1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 20 Aug 2019 11:25:51 -0700 Subject: [PATCH 08/19] Preparing release version 5.1.1 --- CHANGELOG.rst | 9 +++++++++ changelog/5751.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-5.1.1.rst | 24 ++++++++++++++++++++++++ doc/en/assert.rst | 6 +++--- doc/en/builtin.rst | 2 +- doc/en/cache.rst | 12 ++++++------ doc/en/capture.rst | 2 +- doc/en/doctest.rst | 4 ++-- doc/en/example/markers.rst | 18 +++++++++--------- doc/en/example/nonpython.rst | 6 +++--- doc/en/example/parametrize.rst | 27 +++++++++++++-------------- doc/en/example/pythoncollection.rst | 4 ++-- doc/en/example/reportingdemo.rst | 2 +- doc/en/example/simple.rst | 20 ++++++++++---------- doc/en/example/special.rst | 2 +- doc/en/fixture.rst | 20 ++++++++++---------- doc/en/getting-started.rst | 8 ++++---- doc/en/index.rst | 2 +- doc/en/parametrize.rst | 8 ++++---- doc/en/skipping.rst | 2 +- doc/en/tmpdir.rst | 4 ++-- doc/en/unittest.rst | 4 ++-- doc/en/usage.rst | 6 +++--- doc/en/warnings.rst | 6 +++--- doc/en/writing_plugins.rst | 2 +- 26 files changed, 117 insertions(+), 85 deletions(-) delete mode 100644 changelog/5751.bugfix.rst create mode 100644 doc/en/announce/release-5.1.1.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6c4479897..3f9637248 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,15 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.1.1 (2019-08-20) +========================= + +Bug Fixes +--------- + +- `#5751 `_: Fixed ``TypeError`` when importing pytest on Python 3.5.0 and 3.5.1. + + pytest 5.1.0 (2019-08-15) ========================= diff --git a/changelog/5751.bugfix.rst b/changelog/5751.bugfix.rst deleted file mode 100644 index 879909c8b..000000000 --- a/changelog/5751.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed ``TypeError`` when importing pytest on Python 3.5.0 and 3.5.1. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 7c6220b24..84a41d2bf 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.1.1 release-5.1.0 release-5.0.1 release-5.0.0 diff --git a/doc/en/announce/release-5.1.1.rst b/doc/en/announce/release-5.1.1.rst new file mode 100644 index 000000000..9cb731ebb --- /dev/null +++ b/doc/en/announce/release-5.1.1.rst @@ -0,0 +1,24 @@ +pytest-5.1.1 +======================================= + +pytest 5.1.1 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: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* Florian Bruhin +* Hugo van Kemenade +* Ran Benita +* Ronny Pfannschmidt + + +Happy testing, +The pytest Development Team diff --git a/doc/en/assert.rst b/doc/en/assert.rst index bd6e9b3b3..16de77898 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.05s ============================= + ============================ 1 failed in 0.02s ============================= ``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.05s ============================= + ============================ 1 failed in 0.02s ============================= Special comparisons are done for a number of cases: @@ -279,7 +279,7 @@ the conftest file: E vals: 1 != 2 test_foocompare.py:12: AssertionError - 1 failed in 0.05s + 1 failed in 0.02s .. _assert-details: .. _`assert introspection`: diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 4638cf784..fc8b3f40f 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -160,7 +160,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a in python < 3.6 this is a pathlib2.Path - no tests ran in 0.01s + no tests ran in 0.00s You can also interactively ask for help, e.g. by typing on the Python interactive prompt something like: diff --git a/doc/en/cache.rst b/doc/en/cache.rst index c6b3e3c47..384be5daf 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.16s + 2 failed, 48 passed in 0.08s 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.07s ===================== + ===================== 2 failed, 48 deselected in 0.02s ===================== 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.15s ======================= + ======================= 2 failed, 48 passed in 0.07s ======================= .. _`config.cache`: @@ -230,7 +230,7 @@ If you run this command for the first time, you can see the print statement: test_caching.py:20: AssertionError -------------------------- Captured stdout setup --------------------------- running expensive computation... - 1 failed in 0.05s + 1 failed in 0.02s If you run it a second time, the value will be retrieved from the cache and nothing will be printed: @@ -249,7 +249,7 @@ the cache and nothing will be printed: E assert 42 == 23 test_caching.py:20: AssertionError - 1 failed in 0.05s + 1 failed in 0.02s See the :ref:`cache-api` for more details. @@ -300,7 +300,7 @@ filtering: example/value contains: 42 - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.00s =========================== Clearing Cache content ---------------------- diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 72bdea983..2a9de0be3 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.05s ======================== + ======================= 1 failed, 1 passed in 0.02s ======================== Accessing captured output from a test function --------------------------------------------------- diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 2718e1e63..7ecfe7e56 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.02s ============================= + ============================ 1 passed in 0.01s ============================= 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.03s ============================= + ============================ 2 passed in 0.01s ============================= 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 38d02ed0c..f5acd296f 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -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.02s ====================== + ===================== 3 passed, 1 deselected in 0.01s ====================== Selecting tests based on their node ID -------------------------------------- @@ -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.02s ============================= + ============================ 2 passed in 0.01s ============================= .. _node-id: @@ -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.02s ====================== + ===================== 3 passed, 1 deselected in 0.01s ====================== 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.02s ====================== + ===================== 2 passed, 2 deselected in 0.01s ====================== .. note:: @@ -413,7 +413,7 @@ the test needs: test_someenv.py s [100%] - ============================ 1 skipped in 0.01s ============================ + ============================ 1 skipped in 0.00s ============================ and here is one that specifies exactly the environment needed: @@ -499,7 +499,7 @@ The output is as follows: $ pytest -q -s Mark(name='my_marker', args=(,), kwargs={}) . - 1 passed in 0.01s + 1 passed in 0.00s 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``. @@ -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.02s ======================= + ======================= 2 passed, 2 skipped in 0.01s ======================= Note that if you specify a platform via the marker-command line option like this: @@ -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.07s ====================== + ===================== 2 failed, 2 deselected in 0.02s ====================== 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.07s ====================== + ===================== 3 failed, 1 deselected in 0.03s ====================== diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index 9c9e462f6..6699de749 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.06s ======================== + ======================= 1 failed, 1 passed in 0.02s ======================== .. 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.07s ======================== + ======================= 1 failed, 1 passed in 0.02s ======================== .. regendoc:wipe @@ -97,4 +97,4 @@ interesting to just look at the collection tree: - ========================== no tests ran in 0.05s =========================== + ========================== no tests ran in 0.02s =========================== diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 08b414880..cf99ea472 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -73,7 +73,7 @@ let's run the full monty: E assert 4 < 4 test_compute.py:4: AssertionError - 1 failed, 4 passed in 0.06s + 1 failed, 4 passed in 0.02s As expected when running the full range of ``param1`` values we'll get an error on the last one. @@ -172,7 +172,7 @@ objects, they are still using the default pytest representation: - ========================== no tests ran in 0.02s =========================== + ========================== no tests ran in 0.01s =========================== 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.02s ============================= + ============================ 4 passed in 0.01s ============================= 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.02s =========================== + ========================== no tests ran in 0.01s =========================== 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.01s =========================== + ========================== no tests ran in 0.00s =========================== And then when we run the test: @@ -343,7 +343,7 @@ And then when we run the test: E Failed: deliberately failing for demo purposes test_backends.py:8: Failed - 1 failed, 1 passed in 0.05s + 1 failed, 1 passed in 0.02s The first invocation with ``db == "DB1"`` passed while the second with ``db == "DB2"`` failed. Our ``db`` fixture function has instantiated each of the DB values during the setup phase while the ``pytest_generate_tests`` generated two according calls to the ``test_db_initialized`` during the collection phase. @@ -394,7 +394,7 @@ The result of this test will be successful: - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.00s =========================== .. regendoc:wipe @@ -454,7 +454,7 @@ argument sets to use for each test function. Let's run it: E assert 1 == 2 test_parametrize.py:21: AssertionError - 1 failed, 2 passed in 0.07s + 1 failed, 2 passed in 0.03s Indirect parametrization with multiple fixtures -------------------------------------------------------------- @@ -475,11 +475,10 @@ 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...ssssssssssss [100%] + ssssssssssss......sss...... [100%] ========================= short test summary info ========================== - 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.43s + SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found + 12 passed, 15 skipped in 0.62s Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- @@ -548,7 +547,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.02s ======================= + ======================= 1 passed, 1 skipped in 0.01s ======================= 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: @@ -610,7 +609,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.23s ================ + =============== 2 passed, 15 deselected, 1 xfailed in 0.08s ================ As the result: diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 95faae34b..a718de400 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -221,7 +221,7 @@ You can always peek at the collection tree without running tests like this: - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.00s =========================== .. _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.04s =========================== + ========================== no tests ran in 0.01s =========================== 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 1ad7c6966..c024b8616 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.82s ============================ + ============================ 44 failed in 0.26s ============================ diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index ce2fbff54..fea73f4e9 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -65,7 +65,7 @@ Let's run this without supplying our new option: test_sample.py:6: AssertionError --------------------------- Captured stdout call --------------------------- first - 1 failed in 0.06s + 1 failed in 0.02s And now with supplying a command line option: @@ -89,7 +89,7 @@ And now with supplying a command line option: test_sample.py:6: AssertionError --------------------------- Captured stdout call --------------------------- second - 1 failed in 0.06s + 1 failed in 0.02s You can see that the command line option arrived in our test. This completes the basic pattern. However, one often rather wants to process @@ -132,7 +132,7 @@ directory with the above conftest.py: rootdir: $REGENDOC_TMPDIR collected 0 items - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.00s =========================== .. _`excontrolskip`: @@ -261,7 +261,7 @@ Let's run our little function: E Failed: not configured: 42 test_checkconfig.py:11: Failed - 1 failed in 0.05s + 1 failed in 0.02s If you only want to hide certain exceptions, you can set ``__tracebackhide__`` to a callable which gets the ``ExceptionInfo`` object. You can for example use @@ -445,9 +445,9 @@ Now we can profile which test functions execute the slowest: ========================= slowest 3 test durations ========================= 0.30s call test_some_are_slow.py::test_funcslow2 - 0.25s call test_some_are_slow.py::test_funcslow1 + 0.20s call test_some_are_slow.py::test_funcslow1 0.10s call test_some_are_slow.py::test_funcfast - ============================ 3 passed in 0.68s ============================= + ============================ 3 passed in 0.61s ============================= 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.07s ================== + ================== 1 failed, 2 passed, 1 xfailed in 0.03s ================== 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.10s ============== + ============= 3 failed, 2 passed, 1 xfailed, 1 error in 0.05s ============== 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.07s ============================= + ============================ 2 failed in 0.02s ============================= 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.07s ======================== + ======================== 2 failed, 1 error in 0.02s ======================== You'll see that the fixture finalizers could use the precise reporting information. diff --git a/doc/en/example/special.rst b/doc/en/example/special.rst index 5142d08b9..9ad55f3d7 100644 --- a/doc/en/example/special.rst +++ b/doc/en/example/special.rst @@ -81,4 +81,4 @@ If you run this without output capturing: .test other .test_unit1 method called . - 4 passed in 0.02s + 4 passed in 0.01s diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index b8469ad46..333383d65 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.57s ============================= + ============================ 1 failed in 0.18s ============================= 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.76s ============================= + ============================ 2 failed in 0.20s ============================= 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.76s + 2 failed in 0.20s 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.76s + 2 failed in 0.21s 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 1.77s + 4 failed in 0.89s 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.04s =========================== + ========================== no tests ran in 0.01s =========================== .. _`fixture-parametrize-marks`: @@ -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.79s ============================= + ============================ 2 passed in 0.44s ============================= 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.02s ============================= + ============================ 8 passed in 0.01s ============================= You can see that the parametrized module-scoped ``modarg`` resource caused an ordering of test execution that lead to the fewest possible "active" resources. @@ -1043,7 +1043,7 @@ to verify our fixture is activated and the tests pass: $ pytest -q .. [100%] - 2 passed in 0.02s + 2 passed in 0.01s You can specify multiple fixtures like this: @@ -1151,7 +1151,7 @@ If we run it, we get two passing tests: $ pytest -q .. [100%] - 2 passed in 0.02s + 2 passed in 0.01s Here is how autouse fixtures work in other scopes: diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index bf1f6ac3d..38a361818 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.05s ============================= + ============================ 1 failed in 0.02s ============================= 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.01s + 1 passed in 0.00s Group multiple tests in a class -------------------------------------------------------------- @@ -145,7 +145,7 @@ Once you develop multiple tests, you may want to group them into a class. pytest E + where False = hasattr('hello', 'check') test_class.py:8: AssertionError - 1 failed, 1 passed in 0.05s + 1 failed, 1 passed in 0.02s The first test passed and the second failed. You can easily see the intermediate values in the assertion to help you understand the reason for the failure. @@ -180,7 +180,7 @@ List the name ``tmpdir`` in the test function signature and ``pytest`` will look test_tmpdir.py:3: AssertionError --------------------------- Captured stdout call --------------------------- PYTEST_TMPDIR/test_needsfiles0 - 1 failed in 0.05s + 1 failed in 0.02s More info on tmpdir handling is available at :ref:`Temporary directories and files `. diff --git a/doc/en/index.rst b/doc/en/index.rst index 8b8e8b337..65b4631cd 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.06s ============================= + ============================ 1 failed in 0.02s ============================= 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 79716b379..85f233be3 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.05s ======================== + ======================= 1 failed, 2 passed in 0.02s ======================== .. note:: @@ -128,7 +128,7 @@ Let's run this: test_expectation.py ..x [100%] - ======================= 2 passed, 1 xfailed in 0.06s ======================= + ======================= 2 passed, 1 xfailed in 0.02s ======================= The one parameter set which caused a failure previously now shows up as an "xfailed (expected to fail)" test. @@ -225,7 +225,7 @@ Let's also run with a stringinput that will lead to a failing test: E + where = '!'.isalpha test_strings.py:4: AssertionError - 1 failed in 0.05s + 1 failed in 0.02s As expected our test function fails. @@ -239,7 +239,7 @@ list: s [100%] ========================= short test summary info ========================== SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:2 - 1 skipped in 0.01s + 1 skipped in 0.00s Note that when calling ``metafunc.parametrize`` multiple times with different parameter sets, all parameter names across those sets cannot be duplicated, otherwise an error will be raised. diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 7be1eb364..d271b0b2a 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.17s ============================ + ============================ 7 xfailed in 0.05s ============================ .. _`skip/xfail with parametrize`: diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index 4084fc015..c231e76a1 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.06s ============================= + ============================ 1 failed in 0.02s ============================= .. _`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.05s ============================= + ============================ 1 failed in 0.02s ============================= .. _`tmpdir factory example`: diff --git a/doc/en/unittest.rst b/doc/en/unittest.rst index b1d58071a..4f0a279a2 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.07s ============================= + ============================ 2 failed in 0.02s ============================= This default pytest traceback shows that the two test methods share the same ``self.db`` instance which was our intention @@ -219,7 +219,7 @@ Running this test module ...: $ pytest -q test_unittest_cleandir.py . [100%] - 1 passed in 0.02s + 1 passed in 0.01s ... gives us one passed test because the ``initdir`` fixture function was executed ahead of the ``test_method``. diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 0ad70ff27..78702ea86 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.08s === + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.03s === 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.08s === + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.03s === 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.08s === + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.03s === .. _pdb-option: diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index e54d9f027..b8a2df270 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.01s ======================= + ====================== 1 passed, 1 warnings in 0.00s ======================= The ``-W`` flag can be passed to control which warnings will be displayed or even turn them into errors: @@ -64,7 +64,7 @@ them into errors: E UserWarning: api v1, should use functions from v2 test_show_warnings.py:5: UserWarning - 1 failed in 0.05s + 1 failed in 0.02s The same option can be set in the ``pytest.ini`` file using the ``filterwarnings`` ini option. For example, the configuration below will ignore all user warnings, but will transform @@ -407,7 +407,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta class Test: -- Docs: https://docs.pytest.org/en/latest/warnings.html - 1 warnings in 0.01s + 1 warnings in 0.00s These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings. diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 9f3d3115e..5f429c219 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -442,7 +442,7 @@ additionally it is possible to copy examples for an example folder before runnin testdir.copy_example("test_example.py") -- Docs: https://docs.pytest.org/en/latest/warnings.html - ====================== 2 passed, 1 warnings in 0.28s ======================= + ====================== 2 passed, 1 warnings in 0.12s ======================= For more information about the result object that ``runpytest()`` returns, and the methods that it provides please check out the :py:class:`RunResult From a4adf511fc1e275880c7c4bd68c376eab3435437 Mon Sep 17 00:00:00 2001 From: linchiwei123 <40888469+linchiwei123@users.noreply.github.com> Date: Sat, 24 Aug 2019 20:49:00 +0800 Subject: [PATCH 09/19] Fix TypeError can only concatenate str (not "bytes") to str --- changelog/5782.bugfix.rst | 1 + src/_pytest/pastebin.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog/5782.bugfix.rst diff --git a/changelog/5782.bugfix.rst b/changelog/5782.bugfix.rst new file mode 100644 index 000000000..d1f144b21 --- /dev/null +++ b/changelog/5782.bugfix.rst @@ -0,0 +1 @@ +Fix decoding error when printing an error response from ``--pastebin``. \ No newline at end of file diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index ce0e73acc..91aa5f1fd 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -72,7 +72,7 @@ def create_new_paste(contents): if m: return "{}/show/{}".format(url, m.group(1)) else: - return "bad response: " + response + return "bad response: " + response.decode("utf-8") def pytest_terminal_summary(terminalreporter): From 691c706fcc27e4765bdb49b3e3bdf9a95e2bf283 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 24 Aug 2019 14:41:06 -0300 Subject: [PATCH 10/19] Add test for #5782 --- testing/test_pastebin.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index 9afa1e23f..4e8bac56c 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -116,3 +116,15 @@ class TestPaste: assert "lexer=%s" % lexer in data.decode() assert "code=full-paste-contents" in data.decode() assert "expiry=1week" in data.decode() + + def test_create_new_paste_failure(self, pastebin, monkeypatch): + import io + import urllib.request + + def response(url, data): + stream = io.BytesIO(b"something bad occurred") + return stream + + monkeypatch.setattr(urllib.request, "urlopen", response) + result = pastebin.create_new_paste(b"full-paste-contents") + assert result == "bad response: something bad occurred" From 1c7aeb670af879f0b50d991da15e61d69ac17dab Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 24 Aug 2019 15:01:48 -0300 Subject: [PATCH 11/19] Fix linting --- changelog/5782.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/5782.bugfix.rst b/changelog/5782.bugfix.rst index d1f144b21..e961d8fb5 100644 --- a/changelog/5782.bugfix.rst +++ b/changelog/5782.bugfix.rst @@ -1 +1 @@ -Fix decoding error when printing an error response from ``--pastebin``. \ No newline at end of file +Fix decoding error when printing an error response from ``--pastebin``. From 01b9774e3b0571cc907074a50cc37b0d0b0119c4 Mon Sep 17 00:00:00 2001 From: linchiwei123 <306741005@qq.com> Date: Sun, 25 Aug 2019 12:44:30 +0800 Subject: [PATCH 12/19] update doc --- doc/en/fixture.rst | 6 +++--- doc/en/monkeypatch.rst | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 333383d65..91b5aca85 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -315,15 +315,15 @@ Consider the code below: .. literalinclude:: example/fixtures/test_fixtures_order.py -The fixtures requested by ``test_foo`` will be instantiated in the following order: +The fixtures requested by ``test_order`` will be instantiated in the following order: 1. ``s1``: is the highest-scoped fixture (``session``). 2. ``m1``: is the second highest-scoped fixture (``module``). 3. ``a1``: is a ``function``-scoped ``autouse`` fixture: it will be instantiated before other fixtures within the same scope. 4. ``f3``: is a ``function``-scoped fixture, required by ``f1``: it needs to be instantiated at this point -5. ``f1``: is the first ``function``-scoped fixture in ``test_foo`` parameter list. -6. ``f2``: is the last ``function``-scoped fixture in ``test_foo`` parameter list. +5. ``f1``: is the first ``function``-scoped fixture in ``test_order`` parameter list. +6. ``f2``: is the last ``function``-scoped fixture in ``test_order`` parameter list. .. _`finalization`: diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst index a38f07e79..1d1bd68c0 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -50,7 +50,7 @@ these patches. :py:meth:`monkeypatch.chdir` to change the context of the current working directory during a test. -5. Use py:meth:`monkeypatch.syspath_prepend` to modify ``sys.path`` which will also +5. Use :py:meth:`monkeypatch.syspath_prepend` to modify ``sys.path`` which will also call :py:meth:`pkg_resources.fixup_namespace_packages` and :py:meth:`importlib.invalidate_caches`. See the `monkeypatch blog post`_ for some introduction material From 3c82b1cb976d77870f80f05f980be8a1d6f77162 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 26 Aug 2019 10:47:21 -0300 Subject: [PATCH 13/19] 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 14/19] 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 15/19] 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 16/19] 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 17/19] 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 18/19] 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 a511b98da92c0fbb607818e29a854e6c610e5aa0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 26 Aug 2019 13:21:53 -0300 Subject: [PATCH 19/19] 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"""