diff --git a/_pytest/junitxml.py b/_pytest/junitxml.py index 704e62a1b..de9a4d54b 100644 --- a/_pytest/junitxml.py +++ b/_pytest/junitxml.py @@ -118,13 +118,10 @@ class _NodeReporter(object): def _write_captured_output(self, report): for capname in ('out', 'err'): - allcontent = "" - for name, content in report.get_sections("Captured std%s" % - capname): - allcontent += content - if allcontent: + content = getattr(report, 'capstd' + capname) + if content: tag = getattr(Junit, 'system-' + capname) - self.append(tag(bin_xml_escape(allcontent))) + self.append(tag(bin_xml_escape(content))) def append_pass(self, report): self.add_stats('passed') diff --git a/_pytest/python.py b/_pytest/python.py index d3789109e..4d2155aee 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -357,11 +357,13 @@ class PyCollector(PyobjMixin, pytest.Collector): fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm) for callspec in metafunc._calls: - subname = "%s[%s]" %(name, callspec.id) + subname = "%s[%s]" % (name, callspec.id) yield Function(name=subname, parent=self, callspec=callspec, callobj=funcobj, fixtureinfo=fixtureinfo, - keywords={callspec.id:True}) + keywords={callspec.id:True}, + originalname=name, + ) def _marked(func, mark): @@ -1471,7 +1473,7 @@ class Function(FunctionMixin, pytest.Item, fixtures.FuncargnamesCompatAttr): _genid = None def __init__(self, name, parent, args=None, config=None, callspec=None, callobj=NOTSET, keywords=None, session=None, - fixtureinfo=None): + fixtureinfo=None, originalname=None): super(Function, self).__init__(name, parent, config=config, session=session) self._args = args @@ -1493,6 +1495,12 @@ class Function(FunctionMixin, pytest.Item, fixtures.FuncargnamesCompatAttr): self.fixturenames = fixtureinfo.names_closure self._initrequest() + #: original function name, without any decorations (for example + #: parametrization adds a ``"[...]"`` suffix to function names). + #: + #: .. versionadded:: 3.0 + self.originalname = originalname + def _initrequest(self): self.funcargs = {} if self._isyieldedfunction(): diff --git a/_pytest/runner.py b/_pytest/runner.py index 2ec24cd53..525a2b00f 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -211,6 +211,36 @@ class BaseReport(object): 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 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") @@ -276,8 +306,10 @@ class TestReport(BaseReport): #: one of 'setup', 'call', 'teardown' to indicate runtest phase. self.when = when - #: list of (secname, data) extra information which needs to - #: marshallable + #: 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 diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index af47bad09..74533e273 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -632,6 +632,7 @@ Reference of objects involved in hooks .. autoclass:: _pytest.runner.TestReport() :members: + :inherited-members: .. autoclass:: _pytest.vendored_packages.pluggy._CallOutcome() :members: diff --git a/testing/python/collect.py b/testing/python/collect.py index 692854ce6..843f26a73 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -634,6 +634,15 @@ class TestFunction: result = testdir.runpytest() result.stdout.fnmatch_lines('* 3 passed in *') + def test_function_original_name(self, testdir): + items = testdir.getitems(""" + import pytest + @pytest.mark.parametrize('arg', [1,2]) + def test_func(arg): + pass + """) + assert [x.originalname for x in items] == ['test_func', 'test_func'] + class TestSorting: def test_check_equality(self, testdir): diff --git a/testing/test_runner.py b/testing/test_runner.py index 49379e439..a4fe80912 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -668,3 +668,68 @@ def test_store_except_info_on_eror(): assert sys.last_type is IndexError assert sys.last_value.args[0] == 'TEST' assert sys.last_traceback + + +class TestReportContents: + """ + Test user-level API of ``TestReport`` objects. + """ + + def getrunner(self): + return lambda item: runner.runtestprotocol(item, log=False) + + def test_longreprtext_pass(self, testdir): + reports = testdir.runitem(""" + def test_func(): + pass + """) + rep = reports[1] + assert rep.longreprtext == '' + + def test_longreprtext_failure(self, testdir): + reports = testdir.runitem(""" + def test_func(): + x = 1 + assert x == 4 + """) + rep = reports[1] + assert 'assert 1 == 4' in rep.longreprtext + + def test_captured_text(self, testdir): + reports = testdir.runitem(""" + import pytest + import sys + + @pytest.fixture + def fix(): + sys.stdout.write('setup: stdout\\n') + sys.stderr.write('setup: stderr\\n') + yield + sys.stdout.write('teardown: stdout\\n') + sys.stderr.write('teardown: stderr\\n') + assert 0 + + def test_func(fix): + sys.stdout.write('call: stdout\\n') + sys.stderr.write('call: stderr\\n') + assert 0 + """) + setup, call, teardown = reports + assert setup.capstdout == 'setup: stdout\n' + assert call.capstdout == 'setup: stdout\ncall: stdout\n' + assert teardown.capstdout == 'setup: stdout\ncall: stdout\nteardown: stdout\n' + + assert setup.capstderr == 'setup: stderr\n' + assert call.capstderr == 'setup: stderr\ncall: stderr\n' + assert teardown.capstderr == 'setup: stderr\ncall: stderr\nteardown: stderr\n' + + def test_no_captured_text(self, testdir): + reports = testdir.runitem(""" + def test_func(): + pass + """) + rep = reports[1] + assert rep.capstdout == '' + assert rep.capstderr == '' + +