diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py new file mode 100644 index 000000000..3f199cb8b --- /dev/null +++ b/src/_pytest/reports.py @@ -0,0 +1,192 @@ +import py +from _pytest._code.code import TerminalRepr + + +def getslaveinfoline(node): + try: + return node._slaveinfocache + except AttributeError: + d = node.slaveinfo + ver = "%s.%s.%s" % d["version_info"][:3] + node._slaveinfocache = s = "[%s] %s -- Python %s %s" % ( + d["id"], d["sysplatform"], ver, d["executable"] + ) + return s + + +class BaseReport(object): + + def __init__(self, **kw): + self.__dict__.update(kw) + + def toterminal(self, out): + if hasattr(self, "node"): + out.line(getslaveinfoline(self.node)) + + longrepr = self.longrepr + if longrepr is None: + return + + if hasattr(longrepr, "toterminal"): + longrepr.toterminal(out) + else: + try: + out.line(longrepr) + except UnicodeEncodeError: + out.line("") + + def get_sections(self, prefix): + for name, content in self.sections: + if name.startswith(prefix): + yield prefix, content + + @property + def longreprtext(self): + """ + Read-only property that returns the full string representation + of ``longrepr``. + + .. versionadded:: 3.0 + """ + tw = py.io.TerminalWriter(stringio=True) + tw.hasmarkup = False + self.toterminal(tw) + exc = tw.stringio.getvalue() + return exc.strip() + + @property + def caplog(self): + """Return captured log lines, if log capturing is enabled + + .. versionadded:: 3.5 + """ + return "\n".join( + content for (prefix, content) in self.get_sections("Captured log") + ) + + @property + def capstdout(self): + """Return captured text from stdout, if capturing is enabled + + .. versionadded:: 3.0 + """ + return "".join( + content for (prefix, content) in self.get_sections("Captured stdout") + ) + + @property + def capstderr(self): + """Return captured text from stderr, if capturing is enabled + + .. versionadded:: 3.0 + """ + return "".join( + content for (prefix, content) in self.get_sections("Captured stderr") + ) + + passed = property(lambda x: x.outcome == "passed") + failed = property(lambda x: x.outcome == "failed") + skipped = property(lambda x: x.outcome == "skipped") + + @property + def fspath(self): + return self.nodeid.split("::")[0] + + +class TestReport(BaseReport): + """ Basic test report object (also used for setup and teardown calls if + they fail). + """ + + def __init__( + self, + nodeid, + location, + keywords, + outcome, + longrepr, + when, + sections=(), + duration=0, + user_properties=(), + **extra + ): + #: normalized collection node id + self.nodeid = nodeid + + #: a (filesystempath, lineno, domaininfo) tuple indicating the + #: actual location of a test item - it might be different from the + #: collected one e.g. if a method is inherited from a different module. + self.location = location + + #: a name -> value dictionary containing all keywords and + #: markers associated with a test invocation. + self.keywords = keywords + + #: test outcome, always one of "passed", "failed", "skipped". + self.outcome = outcome + + #: None or a failure representation. + self.longrepr = longrepr + + #: one of 'setup', 'call', 'teardown' to indicate runtest phase. + self.when = when + + #: user properties is a list of tuples (name, value) that holds user + #: defined properties of the test + self.user_properties = user_properties + + #: list of pairs ``(str, str)`` of extra information which needs to + #: marshallable. Used by pytest to add captured text + #: from ``stdout`` and ``stderr``, but may be used by other plugins + #: to add arbitrary information to reports. + self.sections = list(sections) + + #: time it took to run just the test + self.duration = duration + + self.__dict__.update(extra) + + def __repr__(self): + return "" % ( + self.nodeid, self.when, self.outcome + ) + + +class TeardownErrorReport(BaseReport): + outcome = "failed" + when = "teardown" + + def __init__(self, longrepr, **extra): + self.longrepr = longrepr + self.sections = [] + self.__dict__.update(extra) + + +class CollectReport(BaseReport): + + def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra): + self.nodeid = nodeid + self.outcome = outcome + self.longrepr = longrepr + self.result = result or [] + self.sections = list(sections) + self.__dict__.update(extra) + + @property + def location(self): + return (self.fspath, None, self.fspath) + + def __repr__(self): + return "" % ( + self.nodeid, len(self.result), self.outcome + ) + + +class CollectErrorRepr(TerminalRepr): + + def __init__(self, msg): + self.longrepr = msg + + def toterminal(self, out): + out.line(self.longrepr, red=True) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index ee53642c7..dea1115e7 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -7,9 +7,11 @@ import sys from time import time import py -from _pytest._code.code import TerminalRepr, ExceptionInfo +from _pytest._code.code import ExceptionInfo from _pytest.outcomes import skip, Skipped, TEST_OUTCOME +from .reports import TestReport, CollectReport, CollectErrorRepr + # # pytest plugin hooks @@ -215,97 +217,44 @@ class CallInfo(object): return "" % (self.when, status) -def getslaveinfoline(node): - try: - return node._slaveinfocache - except AttributeError: - d = node.slaveinfo - ver = "%s.%s.%s" % d["version_info"][:3] - node._slaveinfocache = s = "[%s] %s -- Python %s %s" % ( - d["id"], - d["sysplatform"], - ver, - d["executable"], - ) - return s - - -class BaseReport(object): - def __init__(self, **kw): - self.__dict__.update(kw) - - def toterminal(self, out): - if hasattr(self, "node"): - out.line(getslaveinfoline(self.node)) - - longrepr = self.longrepr - if longrepr is None: - return - - if hasattr(longrepr, "toterminal"): - longrepr.toterminal(out) +def pytest_runtest_makereport(item, call): + when = call.when + duration = call.stop - call.start + keywords = {x: 1 for x in item.keywords} + excinfo = call.excinfo + sections = [] + if not call.excinfo: + outcome = "passed" + longrepr = None + else: + if not isinstance(excinfo, ExceptionInfo): + outcome = "failed" + longrepr = excinfo + elif excinfo.errisinstance(skip.Exception): + outcome = "skipped" + r = excinfo._getreprcrash() + longrepr = (str(r.path), r.lineno, r.message) else: - try: - out.line(longrepr) - except UnicodeEncodeError: - out.line("") - - def get_sections(self, prefix): - for name, content in self.sections: - if name.startswith(prefix): - yield prefix, content - - @property - def longreprtext(self): - """ - Read-only property that returns the full string representation - of ``longrepr``. - - .. versionadded:: 3.0 - """ - tw = py.io.TerminalWriter(stringio=True) - tw.hasmarkup = False - self.toterminal(tw) - exc = tw.stringio.getvalue() - return exc.strip() - - @property - def caplog(self): - """Return captured log lines, if log capturing is enabled - - .. versionadded:: 3.5 - """ - return "\n".join( - content for (prefix, content) in self.get_sections("Captured log") - ) - - @property - def capstdout(self): - """Return captured text from stdout, if capturing is enabled - - .. versionadded:: 3.0 - """ - return "".join( - content for (prefix, content) in self.get_sections("Captured stdout") - ) - - @property - def capstderr(self): - """Return captured text from stderr, if capturing is enabled - - .. versionadded:: 3.0 - """ - return "".join( - content for (prefix, content) in self.get_sections("Captured stderr") - ) - - passed = property(lambda x: x.outcome == "passed") - failed = property(lambda x: x.outcome == "failed") - skipped = property(lambda x: x.outcome == "skipped") - - @property - def fspath(self): - return self.nodeid.split("::")[0] + outcome = "failed" + if call.when == "call": + longrepr = item.repr_failure(excinfo) + else: # exception in setup or teardown + longrepr = item._repr_failure_py( + excinfo, style=item.config.option.tbstyle + ) + for rwhen, key, content in item._report_sections: + sections.append(("Captured %s %s" % (key, rwhen), content)) + return TestReport( + item.nodeid, + item.location, + keywords, + outcome, + longrepr, + when, + sections, + duration, + user_properties=item.user_properties, + ) def pytest_runtest_makereport(item, call): @@ -348,78 +297,6 @@ def pytest_runtest_makereport(item, call): ) -class TestReport(BaseReport): - """ Basic test report object (also used for setup and teardown calls if - they fail). - """ - - def __init__( - self, - nodeid, - location, - keywords, - outcome, - longrepr, - when, - sections=(), - duration=0, - user_properties=(), - **extra - ): - #: normalized collection node id - self.nodeid = nodeid - - #: a (filesystempath, lineno, domaininfo) tuple indicating the - #: actual location of a test item - it might be different from the - #: collected one e.g. if a method is inherited from a different module. - self.location = location - - #: a name -> value dictionary containing all keywords and - #: markers associated with a test invocation. - self.keywords = keywords - - #: test outcome, always one of "passed", "failed", "skipped". - self.outcome = outcome - - #: None or a failure representation. - self.longrepr = longrepr - - #: one of 'setup', 'call', 'teardown' to indicate runtest phase. - self.when = when - - #: user properties is a list of tuples (name, value) that holds user - #: defined properties of the test - self.user_properties = user_properties - - #: list of pairs ``(str, str)`` of extra information which needs to - #: marshallable. Used by pytest to add captured text - #: from ``stdout`` and ``stderr``, but may be used by other plugins - #: to add arbitrary information to reports. - self.sections = list(sections) - - #: time it took to run just the test - self.duration = duration - - self.__dict__.update(extra) - - def __repr__(self): - return "" % ( - self.nodeid, - self.when, - self.outcome, - ) - - -class TeardownErrorReport(BaseReport): - outcome = "failed" - when = "teardown" - - def __init__(self, longrepr, **extra): - self.longrepr = longrepr - self.sections = [] - self.__dict__.update(extra) - - def pytest_make_collect_report(collector): call = CallInfo(lambda: list(collector.collect()), "collect") longrepr = None @@ -446,35 +323,6 @@ def pytest_make_collect_report(collector): return rep -class CollectReport(BaseReport): - def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra): - self.nodeid = nodeid - self.outcome = outcome - self.longrepr = longrepr - self.result = result or [] - self.sections = list(sections) - self.__dict__.update(extra) - - @property - def location(self): - return (self.fspath, None, self.fspath) - - def __repr__(self): - return "" % ( - self.nodeid, - len(self.result), - self.outcome, - ) - - -class CollectErrorRepr(TerminalRepr): - def __init__(self, msg): - self.longrepr = msg - - def toterminal(self, out): - out.line(self.longrepr, red=True) - - class SetupState(object): """ shared state for setting up/tearing down test items or collectors. """ diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index aa6b9a120..ae2b4ea76 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -7,6 +7,8 @@ import os from _pytest.junitxml import LogXML import pytest +from _pytest.reports import BaseReport + def runandparse(testdir, *args): resultpath = testdir.tmpdir.join("junit.xml") @@ -940,7 +942,6 @@ def test_unicode_issue368(testdir): path = testdir.tmpdir.join("test.xml") log = LogXML(str(path), None) ustr = py.builtin._totext("ВНИ!", "utf-8") - from _pytest.runner import BaseReport class Report(BaseReport): longrepr = ustr @@ -1137,7 +1138,6 @@ def test_fancy_items_regression(testdir): def test_global_properties(testdir): path = testdir.tmpdir.join("test_global_properties.xml") log = LogXML(str(path), None) - from _pytest.runner import BaseReport class Report(BaseReport): sections = [] @@ -1173,7 +1173,6 @@ def test_url_property(testdir): test_url = "http://www.github.com/pytest-dev" path = testdir.tmpdir.join("test_url_property.xml") log = LogXML(str(path), None) - from _pytest.runner import BaseReport class Report(BaseReport): longrepr = "FooBarBaz" diff --git a/testing/test_runner.py b/testing/test_runner.py index a78d4de31..741180692 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -8,7 +8,7 @@ import py import pytest import sys import types -from _pytest import runner, main, outcomes +from _pytest import runner, main, outcomes, reports class TestSetupState(object): @@ -459,10 +459,10 @@ class TestSessionReports(object): reporttypes = [ - runner.BaseReport, - runner.TestReport, - runner.TeardownErrorReport, - runner.CollectReport, + reports.BaseReport, + reports.TestReport, + reports.TeardownErrorReport, + reports.CollectReport, ]