Merge pull request #6521 from blueyed/harden-nose-raises

tests: improve test for `nose.raises`
This commit is contained in:
Daniel Hahler 2020-01-23 13:42:14 +01:00 committed by GitHub
commit 6b13379f37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 77 additions and 27 deletions

View File

@ -307,7 +307,7 @@ def get_real_method(obj, holder):
return obj return obj
def getfslineno(obj): def getfslineno(obj) -> Tuple[Union[str, py.path.local], int]:
# xxx let decorators etc specify a sane ordering # xxx let decorators etc specify a sane ordering
obj = get_real_func(obj) obj = get_real_func(obj)
if hasattr(obj, "place_as"): if hasattr(obj, "place_as"):

View File

@ -82,8 +82,8 @@ class Parser:
self.optparser = self._getparser() self.optparser = self._getparser()
try_argcomplete(self.optparser) try_argcomplete(self.optparser)
args = [str(x) if isinstance(x, py.path.local) else x for x in args] strargs = [str(x) if isinstance(x, py.path.local) else x for x in args]
return self.optparser.parse_args(args, namespace=namespace) return self.optparser.parse_args(strargs, namespace=namespace)
def _getparser(self) -> "MyOptionParser": def _getparser(self) -> "MyOptionParser":
from _pytest._argcomplete import filescompleter from _pytest._argcomplete import filescompleter
@ -124,8 +124,8 @@ class Parser:
the remaining arguments unknown at this point. the remaining arguments unknown at this point.
""" """
optparser = self._getparser() optparser = self._getparser()
args = [str(x) if isinstance(x, py.path.local) else x for x in args] strargs = [str(x) if isinstance(x, py.path.local) else x for x in args]
return optparser.parse_known_args(args, namespace=namespace) return optparser.parse_known_args(strargs, namespace=namespace)
def addini(self, name, help, type=None, default=None): def addini(self, name, help, type=None, default=None):
""" register an ini-file option. """ register an ini-file option.

View File

@ -1,6 +1,9 @@
import os import os
from typing import Any
from typing import Iterable
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Tuple
import py import py
@ -60,7 +63,7 @@ def getcfg(args, config=None):
return None, None, None return None, None, None
def get_common_ancestor(paths): def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local:
common_ancestor = None common_ancestor = None
for path in paths: for path in paths:
if not path.exists(): if not path.exists():
@ -113,7 +116,7 @@ def determine_setup(
args: List[str], args: List[str],
rootdir_cmd_arg: Optional[str] = None, rootdir_cmd_arg: Optional[str] = None,
config: Optional["Config"] = None, config: Optional["Config"] = None,
): ) -> Tuple[py.path.local, Optional[str], Any]:
dirs = get_dirs_from_args(args) dirs = get_dirs_from_args(args)
if inifile: if inifile:
iniconfig = py.iniconfig.IniConfig(inifile) iniconfig = py.iniconfig.IniConfig(inifile)

View File

@ -308,7 +308,7 @@ class DoctestItem(pytest.Item):
else: else:
return super().repr_failure(excinfo) return super().repr_failure(excinfo)
def reportinfo(self) -> Tuple[str, int, str]: def reportinfo(self) -> Tuple[py.path.local, int, str]:
return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name

View File

@ -351,7 +351,7 @@ class FixtureRequest:
self.fixturename = None self.fixturename = None
#: Scope string, one of "function", "class", "module", "session" #: Scope string, one of "function", "class", "module", "session"
self.scope = "function" self.scope = "function"
self._fixture_defs = {} # argname -> FixtureDef self._fixture_defs = {} # type: Dict[str, FixtureDef]
fixtureinfo = pyfuncitem._fixtureinfo fixtureinfo = pyfuncitem._fixtureinfo
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
self._arg2index = {} self._arg2index = {}
@ -426,7 +426,8 @@ class FixtureRequest:
@scopeproperty() @scopeproperty()
def fspath(self) -> py.path.local: def fspath(self) -> py.path.local:
""" the file system path of the test module which collected this test. """ """ the file system path of the test module which collected this test. """
return self._pyfuncitem.fspath # TODO: Remove ignore once _pyfuncitem is properly typed.
return self._pyfuncitem.fspath # type: ignore
@property @property
def keywords(self): def keywords(self):
@ -549,7 +550,9 @@ class FixtureRequest:
source_lineno = frameinfo.lineno source_lineno = frameinfo.lineno
source_path = py.path.local(source_path) source_path = py.path.local(source_path)
if source_path.relto(funcitem.config.rootdir): if source_path.relto(funcitem.config.rootdir):
source_path = source_path.relto(funcitem.config.rootdir) source_path_str = source_path.relto(funcitem.config.rootdir)
else:
source_path_str = str(source_path)
msg = ( msg = (
"The requested fixture has no parameter defined for test:\n" "The requested fixture has no parameter defined for test:\n"
" {}\n\n" " {}\n\n"
@ -558,7 +561,7 @@ class FixtureRequest:
funcitem.nodeid, funcitem.nodeid,
fixturedef.argname, fixturedef.argname,
getlocation(fixturedef.func, funcitem.config.rootdir), getlocation(fixturedef.func, funcitem.config.rootdir),
source_path, source_path_str,
source_lineno, source_lineno,
) )
) )

View File

@ -376,9 +376,9 @@ class Failed(Exception):
@attr.s @attr.s
class _bestrelpath_cache(dict): class _bestrelpath_cache(dict):
path = attr.ib() path = attr.ib(type=py.path.local)
def __missing__(self, path: str) -> str: def __missing__(self, path: py.path.local) -> str:
r = self.path.bestrelpath(path) # type: str r = self.path.bestrelpath(path) # type: str
self[path] = r self[path] = r
return r return r
@ -412,7 +412,7 @@ class Session(nodes.FSCollector):
self._bestrelpathcache = _bestrelpath_cache( self._bestrelpathcache = _bestrelpath_cache(
config.rootdir config.rootdir
) # type: Dict[str, str] ) # type: Dict[py.path.local, str]
self.config.pluginmanager.register(self, name="session") self.config.pluginmanager.register(self, name="session")
@ -425,7 +425,7 @@ class Session(nodes.FSCollector):
self.testscollected, self.testscollected,
) )
def _node_location_to_relpath(self, node_path: str) -> str: def _node_location_to_relpath(self, node_path: py.path.local) -> str:
# bestrelpath is a quite slow function # bestrelpath is a quite slow function
return self._bestrelpathcache[node_path] return self._bestrelpathcache[node_path]

View File

@ -462,6 +462,10 @@ class Item(Node):
@cached_property @cached_property
def location(self) -> Tuple[str, Optional[int], str]: def location(self) -> Tuple[str, Optional[int], str]:
location = self.reportinfo() location = self.reportinfo()
fspath = self.session._node_location_to_relpath(location[0]) if isinstance(location[0], py.path.local):
fspath = location[0]
else:
fspath = py.path.local(location[0])
relfspath = self.session._node_location_to_relpath(fspath)
assert type(location[2]) is str assert type(location[2]) is str
return (fspath, location[1], location[2]) return (relfspath, location[1], location[2])

View File

@ -12,6 +12,7 @@ from functools import partial
from textwrap import dedent from textwrap import dedent
from typing import List from typing import List
from typing import Tuple from typing import Tuple
from typing import Union
import py import py
@ -280,15 +281,16 @@ class PyobjMixin(PyobjContext):
parts.reverse() parts.reverse()
return ".".join(parts) return ".".join(parts)
def reportinfo(self) -> Tuple[str, int, str]: def reportinfo(self) -> Tuple[Union[py.path.local, str], int, str]:
# XXX caching? # XXX caching?
obj = self.obj obj = self.obj
compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None)
if isinstance(compat_co_firstlineno, int): if isinstance(compat_co_firstlineno, int):
# nose compatibility # nose compatibility
fspath = sys.modules[obj.__module__].__file__ file_path = sys.modules[obj.__module__].__file__
if fspath.endswith(".pyc"): if file_path.endswith(".pyc"):
fspath = fspath[:-1] file_path = file_path[:-1]
fspath = file_path # type: Union[py.path.local, str]
lineno = compat_co_firstlineno lineno = compat_co_firstlineno
else: else:
fspath, lineno = getfslineno(obj) fspath, lineno = getfslineno(obj)
@ -367,7 +369,12 @@ class PyCollector(PyobjMixin, nodes.Collector):
if not isinstance(res, list): if not isinstance(res, list):
res = [res] res = [res]
values.extend(res) values.extend(res)
values.sort(key=lambda item: item.reportinfo()[:2])
def sort_key(item):
fspath, lineno, _ = item.reportinfo()
return (str(fspath), lineno)
values.sort(key=sort_key)
return values return values
def _makeitem(self, name, obj): def _makeitem(self, name, obj):

View File

@ -377,15 +377,48 @@ def test_skip_test_with_unicode(testdir):
result.stdout.fnmatch_lines(["* 1 skipped *"]) result.stdout.fnmatch_lines(["* 1 skipped *"])
def test_issue_6517(testdir): def test_raises(testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
from nose.tools import raises from nose.tools import raises
@raises(RuntimeError) @raises(RuntimeError)
def test_fail_without_tcp(): def test_raises_runtimeerror():
raise RuntimeError raise RuntimeError
@raises(Exception)
def test_raises_baseexception_not_caught():
raise BaseException
@raises(BaseException)
def test_raises_baseexception_caught():
raise BaseException
""" """
) )
result = testdir.runpytest() result = testdir.runpytest("-vv")
result.stdout.fnmatch_lines(["* 1 passed *"]) result.stdout.fnmatch_lines(
[
"test_raises.py::test_raises_runtimeerror PASSED*",
"test_raises.py::test_raises_baseexception_not_caught FAILED*",
"test_raises.py::test_raises_baseexception_caught PASSED*",
"*= FAILURES =*",
"*_ test_raises_baseexception_not_caught _*",
"",
"arg = (), kw = {}",
"",
" def newfunc(*arg, **kw):",
" try:",
"> func(*arg, **kw)",
"",
"*/nose/*: ",
"_ _ *",
"",
" @raises(Exception)",
" def test_raises_baseexception_not_caught():",
"> raise BaseException",
"E BaseException",
"",
"test_raises.py:9: BaseException",
"* 1 failed, 2 passed *",
]
)