diff --git a/py/__init__.py b/py/__init__.py index d16105739..5ff8de317 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -133,6 +133,7 @@ initpkg(__name__, 'code.Frame' : ('./code/frame.py', 'Frame'), 'code.ExceptionInfo' : ('./code/excinfo.py', 'ExceptionInfo'), 'code.Traceback' : ('./code/traceback2.py', 'Traceback'), + 'code.getfslineno' : ('./code/source.py', 'getfslineno'), # backports and additions of builtins 'builtin.__doc__' : ('./builtin/__init__.py', '__doc__'), diff --git a/py/code/source.py b/py/code/source.py index 6923012fe..515464a05 100644 --- a/py/code/source.py +++ b/py/code/source.py @@ -217,6 +217,26 @@ def compile_(source, filename=None, mode='exec', flags= return co +def getfslineno(obj): + try: + code = py.code.Code(obj) + except TypeError: + # fallback to + fn = (py.std.inspect.getsourcefile(obj) or + py.std.inspect.getfile(obj)) + fspath = fn and py.path.local(fn) or None + if fspath: + try: + _, lineno = findsource(obj) + except IOError: + lineno = None + else: + lineno = None + else: + fspath = code.path + lineno = code.firstlineno + return fspath, lineno + # # helper functions # diff --git a/py/code/testing/test_source.py b/py/code/testing/test_source.py index 0c2045478..641b26fd4 100644 --- a/py/code/testing/test_source.py +++ b/py/code/testing/test_source.py @@ -355,3 +355,27 @@ def test_findsource___source__(): assert 'if 1:' in str(src) assert src[lineno] == " def x():" + +def test_getfslineno(): + from py.code import getfslineno + + def f(x): + pass + + fspath, lineno = getfslineno(f) + + fname = __file__ + if fname.lower().endswith('.pyc'): + fname = fname[:-1] + + assert fspath == py.path.local(fname) + assert lineno == f.func_code.co_firstlineno-1 # see findsource + + class A(object): + pass + + fspath, lineno = getfslineno(A) + + _, A_lineno = py.std.inspect.findsource(A) + assert fspath == py.path.local(fname) + assert lineno == A_lineno diff --git a/py/test/collect.py b/py/test/collect.py index f3442467e..6a76e3b66 100644 --- a/py/test/collect.py +++ b/py/test/collect.py @@ -439,7 +439,7 @@ class Item(Node): """ deprecated, here because subclasses might call it. """ return obj(*args) - def metainfo(self): + def reportinfo(self): return self.fspath, None, "" def warnoldcollect(function=None): diff --git a/py/test/funcargs.py b/py/test/funcargs.py index 016f764e9..e7b2008ed 100644 --- a/py/test/funcargs.py +++ b/py/test/funcargs.py @@ -111,7 +111,7 @@ class FuncargRequest: name = name[len(self._argprefix):] if name not in available: available.append(name) - fspath, lineno, msg = self._pyfuncitem.metainfo() + fspath, lineno, msg = self._pyfuncitem.reportinfo() line = "%s:%s" %(fspath, lineno) msg = "funcargument %r not found for: %s" %(self.argname, line) msg += "\n available funcargs: %s" %(", ".join(available),) diff --git a/py/test/plugin/api.py b/py/test/plugin/api.py index 7f057b3fb..3b46fc5b8 100644 --- a/py/test/plugin/api.py +++ b/py/test/plugin/api.py @@ -99,6 +99,12 @@ class PluginHooks: def pytest_terminal_summary(self, terminalreporter): """ add additional section in terminal summary reporting. """ + def pytest_report_iteminfo(self, item): + """ return (fspath, lineno, name) for the item. + the information is used for result display and to sort tests + """ + pytest_report_iteminfo.firstresult = True + # ------------------------------------------------------------------------------ # doctest hooks # ------------------------------------------------------------------------------ diff --git a/py/test/plugin/pytest_default.py b/py/test/plugin/pytest_default.py index aa559f4b0..b3a50f5de 100644 --- a/py/test/plugin/pytest_default.py +++ b/py/test/plugin/pytest_default.py @@ -54,6 +54,9 @@ class DefaultPlugin: Directory = parent.config.getvalue('Directory', path) return Directory(path, parent=parent) + def pytest_report_iteminfo(self, item): + return item.reportinfo() + def pytest_addoption(self, parser): group = parser.addgroup("general", "test collection and failure interaction options") group._addoption('-v', '--verbose', action="count", @@ -248,3 +251,16 @@ def test_dist_options(testdir): config = testdir.parseconfigure("-d") assert config.option.dist == "load" + +def test_pytest_report_iteminfo(): + plugin = DefaultPlugin() + + class FakeItem(object): + + def reportinfo(self): + return "-reportinfo-" + + res = plugin.pytest_report_iteminfo(FakeItem()) + + assert res == "-reportinfo-" + diff --git a/py/test/plugin/pytest_restdoc.py b/py/test/plugin/pytest_restdoc.py index 33bc1dd40..cb04ce238 100644 --- a/py/test/plugin/pytest_restdoc.py +++ b/py/test/plugin/pytest_restdoc.py @@ -64,7 +64,7 @@ class ReSTSyntaxTest(py.test.collect.Item): super(ReSTSyntaxTest, self).__init__(*args, **kwargs) self.project = project - def metainfo(self): + def reportinfo(self): return self.fspath, None, "syntax check" def runtest(self): @@ -196,7 +196,7 @@ class ReSTSyntaxTest(py.test.collect.Item): #return [] # no need to rebuild class DoctestText(py.test.collect.Item): - def metainfo(self): + def reportinfo(self): return self.fspath, None, "doctest" def runtest(self): @@ -282,7 +282,7 @@ class LinkCheckerMaker(py.test.collect.Collector): args=(tryfn, path, lineno), callobj=localrefcheck) class CheckLink(py.test.collect.Function): - def metainfo(self, basedir=None): + def reportinfo(self, basedir=None): return (self.fspath, self._args[2], "checklink: %s" % self._args[0]) def setup(self): diff --git a/py/test/plugin/pytest_terminal.py b/py/test/plugin/pytest_terminal.py index d73e5260e..e003055f0 100644 --- a/py/test/plugin/pytest_terminal.py +++ b/py/test/plugin/pytest_terminal.py @@ -149,19 +149,19 @@ class TerminalReporter: # for dist-testing situations itemstart means we # queued the item for sending, not interesting (unless debugging) if self.config.option.debug: - line = self._metainfoline(item) + line = self._reportinfoline(item) extra = "" if node: extra = "-> " + str(node.gateway.id) self.write_ensure_prefix(line, extra) else: if self.config.option.verbose: - line = self._metainfoline(item) + line = self._reportinfoline(item) self.write_ensure_prefix(line, "") else: # ensure that the path is printed before the # 1st test of a module starts running - fspath, lineno, msg = item.metainfo() + fspath, lineno, msg = self._getreportinfo(item) self.write_fspath_result(fspath, "") def pytest_itemtestreport(self, rep): @@ -173,10 +173,10 @@ class TerminalReporter: markup = {} self.stats.setdefault(cat, []).append(rep) if not self.config.option.verbose: - fspath, lineno, msg = rep.colitem.metainfo() + fspath, lineno, msg = self._getreportinfo(rep.colitem) self.write_fspath_result(fspath, letter) else: - line = self._metainfoline(rep.colitem) + line = self._reportinfoline(rep.colitem) if not hasattr(rep, 'node'): self.write_ensure_prefix(line, word, **markup) else: @@ -254,8 +254,8 @@ class TerminalReporter: for rootdir in rootdirs: self.write_line("### Watching: %s" %(rootdir,), bold=True) - def _metainfoline(self, item): - fspath, lineno, msg = item.metainfo() + def _reportinfoline(self, item): + fspath, lineno, msg = self._getreportinfo(item) if fspath: fspath = self.curdir.bestrelpath(fspath) if lineno is not None: @@ -267,16 +267,26 @@ class TerminalReporter: elif fspath and lineno: line = "%(fspath)s:%(lineno)s" else: - line = "[nometainfo]" + line = "[noreportinfo]" return line % locals() + " " - + def _getfailureheadline(self, rep): if isinstance(rep.colitem, py.test.collect.Collector): return str(rep.colitem.fspath) else: - fspath, lineno, msg = rep.colitem.metainfo() + fspath, lineno, msg = self._getreportinfo(rep.colitem) return msg + def _getreportinfo(self, item): + try: + return item.__reportinfo + except AttributeError: + pass + reportinfo = item.config.hook.pytest_report_iteminfo(item=item) + # cache on item + item.__reportinfo = reportinfo + return reportinfo + # # summaries for testrunfinish # @@ -579,11 +589,11 @@ class TestTerminal: "*test_show_path_before_running_test.py*" ]) - def test_itemreport_metainfo(self, testdir, linecomp): + def test_itemreport_reportinfo(self, testdir, linecomp): testdir.makeconftest(""" import py class Function(py.test.collect.Function): - def metainfo(self): + def reportinfo(self): return "ABCDE", 42, "custom" """) item = testdir.getitem("def test_func(): pass") @@ -599,6 +609,25 @@ class TestTerminal: "*ABCDE:43: custom*" ]) + def test_itemreport_pytest_report_iteminfo(self, testdir, linecomp): + item = testdir.getitem("def test_func(): pass") + class Plugin: + def pytest_report_iteminfo(self, item): + return "FGHJ", 42, "custom" + item.config.pluginmanager.register(Plugin()) + tr = TerminalReporter(item.config, file=linecomp.stringio) + item.config.pluginmanager.register(tr) + tr.config.hook.pytest_itemstart(item=item) + linecomp.assert_contains_lines([ + "*FGHJ " + ]) + tr.config.option.verbose = True + tr.config.hook.pytest_itemstart(item=item) + linecomp.assert_contains_lines([ + "*FGHJ:43: custom*" + ]) + + def pseudo_keyboard_interrupt(self, testdir, linecomp, verbose=False): modcol = testdir.getmodulecol(""" def test_foobar(): diff --git a/py/test/pycollect.py b/py/test/pycollect.py index ab2740c8a..7a7c91732 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -18,7 +18,6 @@ a tree of collectors and test items that this modules provides:: """ import py from py.__.test.collect import configproperty, warnoldcollect -from py.__.code.source import findsource pydir = py.path.local(py.__file__).dirpath() from py.__.test import funcargs @@ -65,37 +64,21 @@ class PyobjMixin(object): s = ".".join(parts) return s.replace(".[", "[") - def getfslineno(self): + def _getfslineno(self): try: return self._fslineno except AttributeError: pass obj = self.obj - # let decorators etc specify a sane ordering + # xxx let decorators etc specify a sane ordering if hasattr(obj, 'place_as'): obj = obj.place_as - try: - code = py.code.Code(obj) - except TypeError: - # fallback to - fn = (py.std.inspect.getsourcefile(obj) or - py.std.inspect.getfile(obj)) - fspath = fn and py.path.local(fn) or None - if fspath: - try: - _, lineno = findsource(obj) - except IOError: - lineno = None - else: - lineno = None - else: - fspath = code.path - lineno = code.firstlineno - self._fslineno = fspath, lineno - return fspath, lineno - def metainfo(self): - fspath, lineno = self.getfslineno() + self._fslineno = py.code.getfslineno(obj) + return self._fslineno + + def reportinfo(self): + fspath, lineno = self._getfslineno() modpath = self.getmodpath() return fspath, lineno, modpath @@ -229,7 +212,7 @@ class Class(PyCollectorMixin, py.test.collect.Collector): teardown_class(self.obj) def _getsortvalue(self): - return self.getfslineno() + return self._getfslineno() class Instance(PyCollectorMixin, py.test.collect.Collector): def _getobj(self): @@ -254,7 +237,7 @@ class FunctionMixin(PyobjMixin): """ def _getsortvalue(self): - return self.getfslineno() + return self._getfslineno() def setup(self): """ perform setup for this test function. """ diff --git a/py/test/runner.py b/py/test/runner.py index 384dc644b..9c3c5fa83 100644 --- a/py/test/runner.py +++ b/py/test/runner.py @@ -76,7 +76,7 @@ def forked_run_report(item, pdb=None): return report_process_crash(item, result) def report_process_crash(item, result): - path, lineno = item.getfslineno() + path, lineno = item._getfslineno() longrepr = [ ("X", "CRASHED"), ("%s:%s: CRASHED with signal %d" %(path, lineno, result.signal)), diff --git a/py/test/testing/test_pycollect.py b/py/test/testing/test_pycollect.py index 4a7d3e6e0..807a750c7 100644 --- a/py/test/testing/test_pycollect.py +++ b/py/test/testing/test_pycollect.py @@ -217,7 +217,7 @@ class TestGenerator: class TestFunction: def test_getmodulecollector(self, testdir): item = testdir.getitem("def test_func(): pass") - modcol = item.getmodulecollector() + modcol = item._getparent(py.test.collect.Module) assert isinstance(modcol, py.test.collect.Module) assert hasattr(modcol.obj, 'test_func') @@ -346,28 +346,28 @@ class TestConftestCustomization: assert colitems[0].name == "check_method" -class TestMetaInfo: +class TestReportinfo: - def test_func_metainfo(self, testdir): + def test_func_reportinfo(self, testdir): item = testdir.getitem("def test_func(): pass") - fspath, lineno, modpath = item.metainfo() + fspath, lineno, modpath = item.reportinfo() assert fspath == item.fspath assert lineno == 0 assert modpath == "test_func" - def test_class_metainfo(self, testdir): + def test_class_reportinfo(self, testdir): modcol = testdir.getmodulecol(""" # lineno 0 class TestClass: def test_hello(self): pass """) classcol = modcol.collect_by_name("TestClass") - fspath, lineno, msg = classcol.metainfo() + fspath, lineno, msg = classcol.reportinfo() assert fspath == modcol.fspath assert lineno == 1 assert msg == "TestClass" - def test_generator_metainfo(self, testdir): + def test_generator_reportinfo(self, testdir): modcol = testdir.getmodulecol(""" # lineno 0 def test_gen(): @@ -376,13 +376,13 @@ class TestMetaInfo: yield check, 3 """) gencol = modcol.collect_by_name("test_gen") - fspath, lineno, modpath = gencol.metainfo() + fspath, lineno, modpath = gencol.reportinfo() assert fspath == modcol.fspath assert lineno == 1 assert modpath == "test_gen" genitem = gencol.collect()[0] - fspath, lineno, modpath = genitem.metainfo() + fspath, lineno, modpath = genitem.reportinfo() assert fspath == modcol.fspath assert lineno == 2 assert modpath == "test_gen[0]"