diff --git a/CHANGELOG b/CHANGELOG index aa239d67f..8038fffcd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -37,7 +37,9 @@ New features Fixes / Maintenance ++++++++++++++++++++++ -- improve tracebacks presentation: +- improved traceback presentation: + - improved and unified reporting for "--tb=short" option + - Errors during test module imports are much shorter, (using --tb=short style) - raises shows shorter more relevant tracebacks - improve support for raises and other dynamically compiled code by diff --git a/py/_code/code.py b/py/_code/code.py index efe422a53..dc765bb36 100644 --- a/py/_code/code.py +++ b/py/_code/code.py @@ -416,7 +416,7 @@ class FormattedExcinfo(object): args.append((argname, self._saferepr(argvalue))) return ReprFuncArgs(args) - def get_source(self, source, line_index=-1, excinfo=None): + def get_source(self, source, line_index=-1, excinfo=None, short=False): """ return formatted and marked up source lines. """ lines = [] if source is None: @@ -428,6 +428,8 @@ class FormattedExcinfo(object): if i == line_index: prefix = self.flow_marker + " " else: + if short: + continue prefix = " " line = prefix + source[i] lines.append(line) @@ -482,24 +484,26 @@ class FormattedExcinfo(object): line_index = entry.lineno - max(entry.getfirstlinesource(), 0) lines = [] - if self.style == "long": - reprargs = self.repr_args(entry) - lines.extend(self.get_source(source, line_index, excinfo)) - message = excinfo and excinfo.typename or "" + if self.style in ("short", "long"): + short = self.style == "short" + reprargs = None + if not short: + reprargs = self.repr_args(entry) + s = self.get_source(source, line_index, excinfo, short=short) + lines.extend(s) + if short: + message = "in %s" %(entry.name) + else: + message = excinfo and excinfo.typename or "" path = self._makepath(entry.path) filelocrepr = ReprFileLocation(path, entry.lineno+1, message) - localsrepr = self.repr_locals(entry.locals) - return ReprEntry(lines, reprargs, localsrepr, filelocrepr) - else: - if self.style == "short": - line = source[line_index].lstrip() - basename = os.path.basename(entry.frame.code.filename) - lines.append(' File "%s", line %d, in %s' % ( - basename, entry.lineno+1, entry.name)) - lines.append(" " + line) - if excinfo: - lines.extend(self.get_exconly(excinfo, indent=4)) - return ReprEntry(lines, None, None, None) + localsrepr = None + if not short: + localsrepr = self.repr_locals(entry.locals) + return ReprEntry(lines, reprargs, localsrepr, filelocrepr, short) + if excinfo: + lines.extend(self.get_exconly(excinfo, indent=4)) + return ReprEntry(lines, None, None, None, False) def _makepath(self, path): if not self.abspath: @@ -595,13 +599,21 @@ class ReprTraceback(TerminalRepr): class ReprEntry(TerminalRepr): localssep = "_ " - def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr): + def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, short): self.lines = lines self.reprfuncargs = reprfuncargs self.reprlocals = reprlocals self.reprfileloc = filelocrepr + self.short = short def toterminal(self, tw): + if self.short: + self.reprfileloc.toterminal(tw) + for line in self.lines: + red = line.startswith("E ") + tw.line(line, bold=True, red=red) + #tw.line("") + return if self.reprfuncargs: self.reprfuncargs.toterminal(tw) for line in self.lines: diff --git a/py/_plugin/pytest_runner.py b/py/_plugin/pytest_runner.py index b791daf94..0c39ba775 100644 --- a/py/_plugin/pytest_runner.py +++ b/py/_plugin/pytest_runner.py @@ -188,7 +188,11 @@ class CollectReport(BaseReport): self.passed = True self.result = result else: - self.longrepr = self.collector._repr_failure_py(excinfo) + style = "short" + if collector.config.getvalue("fulltrace"): + style = "long" + self.longrepr = self.collector._repr_failure_py(excinfo, + style=style) if excinfo.errisinstance(py.test.skip.Exception): self.skipped = True self.reason = str(excinfo.value) diff --git a/py/_test/collect.py b/py/_test/collect.py index 7242b7dd8..60d9a1fa1 100644 --- a/py/_test/collect.py +++ b/py/_test/collect.py @@ -172,14 +172,15 @@ class Node(object): def _prunetraceback(self, traceback): return traceback - def _repr_failure_py(self, excinfo): + def _repr_failure_py(self, excinfo, style=None): excinfo.traceback = self._prunetraceback(excinfo.traceback) # XXX should excinfo.getrepr record all data and toterminal() # process it? - if self.config.option.tbstyle == "short": - style = "short" - else: - style = "long" + if style is None: + if self.config.option.tbstyle == "short": + style = "short" + else: + style = "long" return excinfo.getrepr(funcargs=True, showlocals=self.config.option.showlocals, style=style) diff --git a/py/_test/pycollect.py b/py/_test/pycollect.py index 038a6fb2b..eb60f9e85 100644 --- a/py/_test/pycollect.py +++ b/py/_test/pycollect.py @@ -253,7 +253,7 @@ class FunctionMixin(PyobjMixin): traceback = ntraceback.filter() return traceback - def _repr_failure_py(self, excinfo): + def _repr_failure_py(self, excinfo, style="long"): if excinfo.errisinstance(funcargs.FuncargRequest.LookupError): fspath, lineno, msg = self.reportinfo() lines, _ = inspect.getsourcelines(self.obj) @@ -261,11 +261,13 @@ class FunctionMixin(PyobjMixin): if line.strip().startswith('def'): return FuncargLookupErrorRepr(fspath, lineno, lines[:i+1], str(excinfo.value)) - return super(FunctionMixin, self)._repr_failure_py(excinfo) + return super(FunctionMixin, self)._repr_failure_py(excinfo, + style=style) def repr_failure(self, excinfo, outerr=None): assert outerr is None, "XXX outerr usage is deprecated" - return self._repr_failure_py(excinfo) + return self._repr_failure_py(excinfo, + style=self.config.getvalue("tbstyle")) shortfailurerepr = "F" diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index b4538fa38..7c142fe52 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -463,16 +463,18 @@ raise ValueError() reprtb = p.repr_traceback_entry(excinfo.traceback[-2]) lines = reprtb.lines basename = py.path.local(mod.__file__).basename - assert lines[0] == ' File "%s", line 5, in entry' % basename - assert lines[1] == ' func1()' + assert lines[0] == '> func1()' + assert basename in str(reprtb.reprfileloc.path) + assert reprtb.reprfileloc.lineno == 5 # test last entry p = FormattedExcinfo(style="short") reprtb = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) lines = reprtb.lines - assert lines[0] == ' File "%s", line 3, in func1' % basename - assert lines[1] == ' raise ValueError("hello")' - assert lines[2] == 'E ValueError: hello' + assert lines[0] == '> raise ValueError("hello")' + assert lines[1] == 'E ValueError: hello' + assert basename in str(reprtb.reprfileloc.path) + assert reprtb.reprfileloc.lineno == 3 def test_repr_tracebackentry_no(self, importasmod): mod = importasmod(""" @@ -525,12 +527,10 @@ raise ValueError() last_lines = last_reprtb.lines monkeypatch.undo() basename = py.path.local(mod.__file__).basename - assert lines[0] == ' File "%s", line 5, in entry' % basename - assert lines[1] == ' func1()' + assert lines[0] == '> func1()' - assert last_lines[0] == ' File "%s", line 3, in func1' % basename - assert last_lines[1] == ' raise ValueError("hello")' - assert last_lines[2] == 'E ValueError: hello' + assert last_lines[0] == '> raise ValueError("hello")' + assert last_lines[1] == 'E ValueError: hello' def test_repr_traceback_and_excinfo(self, importasmod): mod = importasmod(""" diff --git a/testing/plugin/test_pytest_terminal.py b/testing/plugin/test_pytest_terminal.py index 2f76a8043..2e54f0b7b 100644 --- a/testing/plugin/test_pytest_terminal.py +++ b/testing/plugin/test_pytest_terminal.py @@ -626,6 +626,35 @@ def test_terminalreporter_reportopt_conftestsetting(testdir): "*1 passed*" ]) +def test_tbstyle_short(testdir): + p = testdir.makepyfile(""" + def pytest_funcarg__arg(request): + return 42 + def test_opt(arg): + x = 0 + assert x + """) + result = testdir.runpytest("--tb=short") + s = result.stdout.str() + assert 'arg = 42' not in s + assert 'x = 0' not in s + result.stdout.fnmatch_lines([ + "*%s:5*" % p.basename, + ">*assert x", + "E*assert*", + ]) + result = testdir.runpytest() + s = result.stdout.str() + assert 'x = 0' in s + assert 'assert x' in s + +def test_trace_reporting(testdir): + result = testdir.runpytest("--traceconfig") + result.stdout.fnmatch_lines([ + "*active plugins*" + ]) + assert result.ret == 0 + def test_trace_reporting(testdir): result = testdir.runpytest("--traceconfig") result.stdout.fnmatch_lines([ diff --git a/testing/test_pycollect.py b/testing/test_pycollect.py index af70b2b66..a6fbc344a 100644 --- a/testing/test_pycollect.py +++ b/testing/test_pycollect.py @@ -484,3 +484,28 @@ class TestTracebackCutting: assert out.find("conftest.py:2: ValueError") != -1 numentries = out.count("_ _ _ _") # separator for traceback entries assert numentries >3 + + def test_traceback_error_during_import(self, testdir): + testdir.makepyfile(""" + x = 1 + x = 2 + x = 17 + asd + """) + result = testdir.runpytest() + assert result.ret != 0 + out = result.stdout.str() + assert "x = 1" not in out + assert "x = 2" not in out + result.stdout.fnmatch_lines([ + ">*asd*", + "E*NameError*", + ]) + result = testdir.runpytest("--fulltrace") + out = result.stdout.str() + assert "x = 1" in out + assert "x = 2" in out + result.stdout.fnmatch_lines([ + ">*asd*", + "E*NameError*", + ])