Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP/blocked] Factor out runner._report_for_call, using item._current_callinfo #107

Open
wants to merge 8 commits into
base: my-master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/_pytest/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
if TYPE_CHECKING:
# Imported here due to circular import.
from _pytest.main import Session # noqa: F401
from _pytest.runner import CallInfo # noqa: F401

from _pytest._code.code import _TracebackStyle # noqa: F401

Expand Down Expand Up @@ -148,6 +149,8 @@ def __init__(
if self.name != "()":
self._nodeid += "::" + self.name

self._current_callinfo = None # type: Optional[CallInfo]

@classmethod
def from_parent(cls, parent: "Node", **kw):
"""
Expand Down
5 changes: 4 additions & 1 deletion src/_pytest/reports.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from io import StringIO
from pprint import pprint
from typing import Any
from time import time
from typing import List
from typing import Optional
from typing import Tuple
Expand Down Expand Up @@ -222,7 +223,7 @@ def __init__(
longrepr,
when,
sections=(),
duration=0,
duration: float = 0,
user_properties=None,
**extra
) -> None:
Expand Down Expand Up @@ -273,6 +274,8 @@ def from_item_and_call(cls, item, call) -> "TestReport":
Factory method to create and fill a TestReport with standard item and call info.
"""
when = call.when
if not call.stop:
blueyed marked this conversation as resolved.
Show resolved Hide resolved
call.stop = time()
duration = call.stop - call.start
keywords = {x: 1 for x in item.keywords}
excinfo = call.excinfo
Expand Down
48 changes: 35 additions & 13 deletions src/_pytest/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,27 @@ def pytest_report_teststatus(report):

def call_and_report(
item, when: "Literal['setup', 'call', 'teardown']", log=True, **kwds
):
) -> TestReport:
call = call_runtest_hook(item, when, **kwds)
if not call._report:
call._report = _report_for_call(item, call, log)
return call._report


def _report_for_call(
item, call: "Optional[CallInfo]" = None, log: bool = True
) -> TestReport:
if not call:
call = item._current_callinfo
assert call
assert not call._report, call._report
hook = item.ihook
report = hook.pytest_runtest_makereport(item=item, call=call)
report = hook.pytest_runtest_makereport(item=item, call=call) # type: TestReport
if log:
hook.pytest_runtest_logreport(report=report)
if check_interactive_exception(call, report):
hook.pytest_exception_interact(node=item, call=call, report=report)
call._report = report
return report


Expand All @@ -203,7 +216,7 @@ def check_interactive_exception(call, report):
)


def call_runtest_hook(item, when: "Literal['setup', 'call', 'teardown']", **kwds):
def call_runtest_hook(item, when: "Literal['setup', 'call', 'teardown']", **kwds) -> "CallInfo":
if when == "setup":
ihook = item.ihook.pytest_runtest_setup
elif when == "call":
Expand All @@ -216,7 +229,7 @@ def call_runtest_hook(item, when: "Literal['setup', 'call', 'teardown']", **kwds
if not item.config.getoption("usepdb", False):
reraise += (KeyboardInterrupt,)
return CallInfo.from_call(
lambda: ihook(item=item, **kwds), when=when, reraise=reraise
lambda: ihook(item=item, **kwds), when=when, reraise=reraise, item=item
)


Expand All @@ -229,6 +242,8 @@ class CallInfo:
start = attr.ib()
stop = attr.ib()
when = attr.ib()
item = attr.ib(type=Optional[Node])
_report = attr.ib(type=Optional[TestReport], default=None)
blueyed marked this conversation as resolved.
Show resolved Hide resolved

@property
def result(self):
Expand All @@ -237,20 +252,27 @@ def result(self):
return self._result

@classmethod
def from_call(cls, func, when, reraise=None) -> "CallInfo":
def from_call(cls, func, when, item=None, reraise=None) -> "CallInfo":
#: context of invocation: one of "setup", "call",
#: "teardown", "memocollect"
start = time()
excinfo = None
call = cls(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this structure changes a CallInfo from something concrete and state-free to a state-machine tracking the progress if a call

i believe a class name change is warranted to make this clear, but off hand no good name describing it comes to mind

start=time(), stop=None, when=when, item=item, result=None, excinfo=None
)
if item:
item._current_callinfo = call
try:
result = func()
except: # noqa
excinfo = ExceptionInfo.from_current()
call._result = func()
except BaseException:
call.stop = time()
excinfo = call.excinfo = ExceptionInfo.from_current()
if reraise is not None and excinfo.errisinstance(reraise):
raise
result = None
stop = time()
return cls(start=start, stop=stop, when=when, result=result, excinfo=excinfo)
call._result = None
finally:
if item:
del item._current_callinfo
call.stop = time()
return call

def __repr__(self):
if self.excinfo is None:
Expand Down
29 changes: 12 additions & 17 deletions src/_pytest/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from _pytest.config import hookimpl
from _pytest.outcomes import exit
from _pytest.outcomes import fail
from _pytest.outcomes import OutcomeException
from _pytest.outcomes import skip
from _pytest.outcomes import xfail
from _pytest.python import Class
Expand Down Expand Up @@ -207,31 +208,25 @@ def runtest(self):

testMethod = getattr(self._testcase, self._testcase._testMethodName)

class _GetOutOf_testPartExecutor(KeyboardInterrupt):
"""Helper exception to get out of unittests's testPartExecutor (see TestCase.run)."""

@functools.wraps(testMethod)
def wrapped_testMethod(*args, **kwargs):
"""Wrap the original method to call into pytest's machinery, so other pytest
features can have a chance to kick in (notably --pdb)"""
try:
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
except unittest.SkipTest:
except (unittest.SkipTest, OutcomeException, exit.Exception):
raise
except Exception as exc:
expecting_failure = self._expecting_failure(testMethod)
if expecting_failure:
raise
self._needs_explicit_tearDown = True
raise _GetOutOf_testPartExecutor(exc)
except Exception:
from _pytest.runner import _report_for_call

setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod)
try:
self._testcase(result=self)
except _GetOutOf_testPartExecutor as exc:
raise exc.args[0] from exc.args[0]
finally:
delattr(self._testcase, self._testcase._testMethodName)
self._addexcinfo(sys.exc_info())

assert self._current_callinfo
_report_for_call(self)

self._testcase._wrapped_testMethod = wrapped_testMethod
self._testcase._testMethodName = "_wrapped_testMethod"
self._testcase(result=self)

def _prunetraceback(self, excinfo):
Function._prunetraceback(self, excinfo)
Expand Down
2 changes: 1 addition & 1 deletion testing/test_skipping.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ def test_this_false():

from_call_code = CallInfo.from_call.__func__.__code__
loc = "{}:{}".format(
from_call_code.co_filename, from_call_code.co_firstlineno + 7
from_call_code.co_filename, from_call_code.co_firstlineno + 10
)

result = testdir.runpytest(p, "-rx")
Expand Down