Use pytest.fail(..., pytrace=False) when treating user errors

This prevents an enormous and often useless stack trace from showing
to end users.

Fix #3867
Fix #2293
This commit is contained in:
Bruno Oliveira 2018-10-03 20:07:59 -03:00
parent 67f40e18a7
commit 5436e42990
11 changed files with 124 additions and 99 deletions

View File

@ -0,0 +1,4 @@
Improve usage errors messages by hiding internal details which can be distracting and noisy.
This has the side effect that some error conditions that previously raised generic errors (such as
``ValueError`` for unregistered marks) are now raising ``Failed`` exceptions.

View File

@ -0,0 +1 @@
The internal ``MarkerError`` exception has been removed.

View File

@ -579,7 +579,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
nodeid=funcitem.nodeid, nodeid=funcitem.nodeid,
typename=type(funcitem).__name__, typename=type(funcitem).__name__,
) )
fail(msg) fail(msg, pytrace=False)
if has_params: if has_params:
frame = inspect.stack()[3] frame = inspect.stack()[3]
frameinfo = inspect.getframeinfo(frame[0]) frameinfo = inspect.getframeinfo(frame[0])
@ -600,7 +600,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
source_lineno, source_lineno,
) )
) )
fail(msg) fail(msg, pytrace=False)
else: else:
# indices might not be set if old-style metafunc.addcall() was used # indices might not be set if old-style metafunc.addcall() was used
param_index = funcitem.callspec.indices.get(argname, 0) param_index = funcitem.callspec.indices.get(argname, 0)
@ -718,10 +718,11 @@ def scope2index(scope, descr, where=None):
try: try:
return scopes.index(scope) return scopes.index(scope)
except ValueError: except ValueError:
raise ValueError( fail(
"{} {}has an unsupported scope value '{}'".format( "{} {}got an unexpected scope value '{}'".format(
descr, "from {} ".format(where) if where else "", scope descr, "from {} ".format(where) if where else "", scope
) ),
pytrace=False,
) )
@ -854,7 +855,9 @@ class FixtureDef(object):
self.argname = argname self.argname = argname
self.scope = scope self.scope = scope
self.scopenum = scope2index( self.scopenum = scope2index(
scope or "function", descr="fixture {}".format(func.__name__), where=baseid scope or "function",
descr="Fixture '{}'".format(func.__name__),
where=baseid,
) )
self.params = params self.params = params
self.argnames = getfuncargnames(func, is_method=unittest) self.argnames = getfuncargnames(func, is_method=unittest)

View File

@ -24,11 +24,6 @@ __all__ = [
] ]
class MarkerError(Exception):
"""Error in use of a pytest marker/attribute."""
def param(*values, **kw): def param(*values, **kw):
"""Specify a parameter in `pytest.mark.parametrize`_ calls or """Specify a parameter in `pytest.mark.parametrize`_ calls or
:ref:`parametrized fixtures <fixture-parametrize-marks>`. :ref:`parametrized fixtures <fixture-parametrize-marks>`.

View File

@ -6,6 +6,7 @@ from operator import attrgetter
import attr import attr
from _pytest.outcomes import fail
from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE
from ..compat import NOTSET, getfslineno, MappingMixin from ..compat import NOTSET, getfslineno, MappingMixin
from six.moves import map from six.moves import map
@ -393,7 +394,7 @@ class MarkGenerator(object):
x = marker.split("(", 1)[0] x = marker.split("(", 1)[0]
values.add(x) values.add(x)
if name not in self._markers: if name not in self._markers:
raise AttributeError("%r not a registered marker" % (name,)) fail("{!r} not a registered marker".format(name), pytrace=False)
MARK_GEN = MarkGenerator() MARK_GEN = MarkGenerator()

View File

@ -9,6 +9,7 @@ import attr
import _pytest import _pytest
import _pytest._code import _pytest._code
from _pytest.compat import getfslineno from _pytest.compat import getfslineno
from _pytest.outcomes import fail
from _pytest.mark.structures import NodeKeywords, MarkInfo from _pytest.mark.structures import NodeKeywords, MarkInfo
@ -346,6 +347,9 @@ class Node(object):
pass pass
def _repr_failure_py(self, excinfo, style=None): def _repr_failure_py(self, excinfo, style=None):
if excinfo.errisinstance(fail.Exception):
if not excinfo.value.pytrace:
return six.text_type(excinfo.value)
fm = self.session._fixturemanager fm = self.session._fixturemanager
if excinfo.errisinstance(fm.FixtureLookupError): if excinfo.errisinstance(fm.FixtureLookupError):
return excinfo.value.formatrepr() return excinfo.value.formatrepr()

View File

@ -13,7 +13,6 @@ from textwrap import dedent
import py import py
import six import six
from _pytest.main import FSHookProxy from _pytest.main import FSHookProxy
from _pytest.mark import MarkerError
from _pytest.config import hookimpl from _pytest.config import hookimpl
import _pytest import _pytest
@ -159,8 +158,8 @@ def pytest_generate_tests(metafunc):
alt_spellings = ["parameterize", "parametrise", "parameterise"] alt_spellings = ["parameterize", "parametrise", "parameterise"]
for attr in alt_spellings: for attr in alt_spellings:
if hasattr(metafunc.function, attr): if hasattr(metafunc.function, attr):
msg = "{0} has '{1}', spelling should be 'parametrize'" msg = "{0} has '{1}' mark, spelling should be 'parametrize'"
raise MarkerError(msg.format(metafunc.function.__name__, attr)) fail(msg.format(metafunc.function.__name__, attr), pytrace=False)
for marker in metafunc.definition.iter_markers(name="parametrize"): for marker in metafunc.definition.iter_markers(name="parametrize"):
metafunc.parametrize(*marker.args, **marker.kwargs) metafunc.parametrize(*marker.args, **marker.kwargs)
@ -760,12 +759,6 @@ class FunctionMixin(PyobjMixin):
for entry in excinfo.traceback[1:-1]: for entry in excinfo.traceback[1:-1]:
entry.set_repr_style("short") entry.set_repr_style("short")
def _repr_failure_py(self, excinfo, style="long"):
if excinfo.errisinstance(fail.Exception):
if not excinfo.value.pytrace:
return six.text_type(excinfo.value)
return super(FunctionMixin, self)._repr_failure_py(excinfo, style=style)
def repr_failure(self, excinfo, outerr=None): def repr_failure(self, excinfo, outerr=None):
assert outerr is None, "XXX outerr usage is deprecated" assert outerr is None, "XXX outerr usage is deprecated"
style = self.config.option.tbstyle style = self.config.option.tbstyle
@ -987,7 +980,9 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition) ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition)
scopenum = scope2index(scope, descr="call to {}".format(self.parametrize)) scopenum = scope2index(
scope, descr="parametrize() call in {}".format(self.function.__name__)
)
# create the new calls: if we are parametrize() multiple times (by applying the decorator # create the new calls: if we are parametrize() multiple times (by applying the decorator
# more than once) then we accumulate those calls generating the cartesian product # more than once) then we accumulate those calls generating the cartesian product
@ -1026,15 +1021,16 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
idfn = ids idfn = ids
ids = None ids = None
if ids: if ids:
func_name = self.function.__name__
if len(ids) != len(parameters): if len(ids) != len(parameters):
raise ValueError( msg = "In {}: {} parameter sets specified, with different number of ids: {}"
"%d tests specified with %d ids" % (len(parameters), len(ids)) fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False)
)
for id_value in ids: for id_value in ids:
if id_value is not None and not isinstance(id_value, six.string_types): if id_value is not None and not isinstance(id_value, six.string_types):
msg = "ids must be list of strings, found: %s (type: %s)" msg = "In {}: ids must be list of strings, found: {} (type: {!r})"
raise ValueError( fail(
msg % (saferepr(id_value), type(id_value).__name__) msg.format(func_name, saferepr(id_value), type(id_value)),
pytrace=False,
) )
ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item) ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item)
return ids return ids
@ -1059,9 +1055,11 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
valtypes = dict.fromkeys(argnames, "funcargs") valtypes = dict.fromkeys(argnames, "funcargs")
for arg in indirect: for arg in indirect:
if arg not in argnames: if arg not in argnames:
raise ValueError( fail(
"indirect given to %r: fixture %r doesn't exist" "In {}: indirect fixture '{}' doesn't exist".format(
% (self.function, arg) self.function.__name__, arg
),
pytrace=False,
) )
valtypes[arg] = "params" valtypes[arg] = "params"
return valtypes return valtypes
@ -1075,19 +1073,25 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
:raise ValueError: if validation fails. :raise ValueError: if validation fails.
""" """
default_arg_names = set(get_default_arg_names(self.function)) default_arg_names = set(get_default_arg_names(self.function))
func_name = self.function.__name__
for arg in argnames: for arg in argnames:
if arg not in self.fixturenames: if arg not in self.fixturenames:
if arg in default_arg_names: if arg in default_arg_names:
raise ValueError( fail(
"%r already takes an argument %r with a default value" "In {}: function already takes an argument '{}' with a default value".format(
% (self.function, arg) func_name, arg
),
pytrace=False,
) )
else: else:
if isinstance(indirect, (tuple, list)): if isinstance(indirect, (tuple, list)):
name = "fixture" if arg in indirect else "argument" name = "fixture" if arg in indirect else "argument"
else: else:
name = "fixture" if indirect else "argument" name = "fixture" if indirect else "argument"
raise ValueError("%r uses no %s %r" % (self.function, name, arg)) fail(
"In {}: function uses no {} '{}'".format(func_name, name, arg),
pytrace=False,
)
def addcall(self, funcargs=None, id=NOTSET, param=NOTSET): def addcall(self, funcargs=None, id=NOTSET, param=NOTSET):
""" Add a new call to the underlying test function during the collection phase of a test run. """ Add a new call to the underlying test function during the collection phase of a test run.

View File

@ -1217,8 +1217,7 @@ class TestFixtureUsages(object):
result = testdir.runpytest_inprocess() result = testdir.runpytest_inprocess()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
( (
"*ValueError: fixture badscope from test_invalid_scope.py has an unsupported" "*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'"
" scope value 'functions'"
) )
) )
@ -3607,16 +3606,15 @@ class TestParameterizedSubRequest(object):
) )
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
""" [
E*Failed: The requested fixture has no parameter defined for test: "The requested fixture has no parameter defined for test:",
E* test_call_from_fixture.py::test_foo " test_call_from_fixture.py::test_foo",
E* "Requested fixture 'fix_with_param' defined in:",
E*Requested fixture 'fix_with_param' defined in: "test_call_from_fixture.py:4",
E*test_call_from_fixture.py:4 "Requested here:",
E*Requested here: "test_call_from_fixture.py:9",
E*test_call_from_fixture.py:9 "*1 error in*",
*1 error* ]
"""
) )
def test_call_from_test(self, testdir): def test_call_from_test(self, testdir):
@ -3634,16 +3632,15 @@ class TestParameterizedSubRequest(object):
) )
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
""" [
E*Failed: The requested fixture has no parameter defined for test: "The requested fixture has no parameter defined for test:",
E* test_call_from_test.py::test_foo " test_call_from_test.py::test_foo",
E* "Requested fixture 'fix_with_param' defined in:",
E*Requested fixture 'fix_with_param' defined in: "test_call_from_test.py:4",
E*test_call_from_test.py:4 "Requested here:",
E*Requested here: "test_call_from_test.py:8",
E*test_call_from_test.py:8 "*1 failed*",
*1 failed* ]
"""
) )
def test_external_fixture(self, testdir): def test_external_fixture(self, testdir):
@ -3665,16 +3662,16 @@ class TestParameterizedSubRequest(object):
) )
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
""" [
E*Failed: The requested fixture has no parameter defined for test: "The requested fixture has no parameter defined for test:",
E* test_external_fixture.py::test_foo " test_external_fixture.py::test_foo",
E* "",
E*Requested fixture 'fix_with_param' defined in: "Requested fixture 'fix_with_param' defined in:",
E*conftest.py:4 "conftest.py:4",
E*Requested here: "Requested here:",
E*test_external_fixture.py:2 "test_external_fixture.py:2",
*1 failed* "*1 failed*",
""" ]
) )
def test_non_relative_path(self, testdir): def test_non_relative_path(self, testdir):
@ -3709,16 +3706,16 @@ class TestParameterizedSubRequest(object):
testdir.syspathinsert(fixdir) testdir.syspathinsert(fixdir)
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
""" [
E*Failed: The requested fixture has no parameter defined for test: "The requested fixture has no parameter defined for test:",
E* test_foos.py::test_foo " test_foos.py::test_foo",
E* "",
E*Requested fixture 'fix_with_param' defined in: "Requested fixture 'fix_with_param' defined in:",
E*fix.py:4 "*fix.py:4",
E*Requested here: "Requested here:",
E*test_foos.py:4 "test_foos.py:4",
*1 failed* "*1 failed*",
""" ]
) )

View File

@ -127,10 +127,11 @@ class TestMetafunc(object):
pass pass
metafunc = self.Metafunc(func) metafunc = self.Metafunc(func)
try: with pytest.raises(
pytest.fail.Exception,
match=r"parametrize\(\) call in func got an unexpected scope value 'doggy'",
):
metafunc.parametrize("x", [1], scope="doggy") metafunc.parametrize("x", [1], scope="doggy")
except ValueError as ve:
assert "has an unsupported scope value 'doggy'" in str(ve)
def test_find_parametrized_scope(self): def test_find_parametrized_scope(self):
"""unittest for _find_parametrized_scope (#3941)""" """unittest for _find_parametrized_scope (#3941)"""
@ -206,16 +207,13 @@ class TestMetafunc(object):
metafunc = self.Metafunc(func) metafunc = self.Metafunc(func)
pytest.raises( with pytest.raises(pytest.fail.Exception):
ValueError, lambda: metafunc.parametrize("x", [1, 2], ids=["basic"]) metafunc.parametrize("x", [1, 2], ids=["basic"])
)
pytest.raises( with pytest.raises(pytest.fail.Exception):
ValueError, metafunc.parametrize(
lambda: metafunc.parametrize(
("x", "y"), [("abc", "def"), ("ghi", "jkl")], ids=["one"] ("x", "y"), [("abc", "def"), ("ghi", "jkl")], ids=["one"]
), )
)
@pytest.mark.issue510 @pytest.mark.issue510
def test_parametrize_empty_list(self): def test_parametrize_empty_list(self):
@ -573,7 +571,7 @@ class TestMetafunc(object):
pass pass
metafunc = self.Metafunc(func) metafunc = self.Metafunc(func)
with pytest.raises(ValueError): with pytest.raises(pytest.fail.Exception):
metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "z"]) metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "z"])
@pytest.mark.issue714 @pytest.mark.issue714
@ -1189,7 +1187,9 @@ class TestMetafuncFunctional(object):
) )
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
["*ids must be list of strings, found: 2 (type: int)*"] [
"*In test_ids_numbers: ids must be list of strings, found: 2 (type: *'int'>)*"
]
) )
def test_parametrize_with_identical_ids_get_unique_names(self, testdir): def test_parametrize_with_identical_ids_get_unique_names(self, testdir):
@ -1326,13 +1326,13 @@ class TestMetafuncFunctional(object):
attr attr
) )
) )
reprec = testdir.inline_run("--collectonly") result = testdir.runpytest("--collectonly")
failures = reprec.getfailures() result.stdout.fnmatch_lines(
assert len(failures) == 1 [
expectederror = "MarkerError: test_foo has '{}', spelling should be 'parametrize'".format( "test_foo has '{}' mark, spelling should be 'parametrize'".format(attr),
attr "*1 error in*",
]
) )
assert expectederror in failures[0].longrepr.reprcrash.message
class TestMetafuncFunctionalAuto(object): class TestMetafuncFunctionalAuto(object):

View File

@ -247,7 +247,7 @@ def test_marker_without_description(testdir):
) )
ftdir = testdir.mkdir("ft1_dummy") ftdir = testdir.mkdir("ft1_dummy")
testdir.tmpdir.join("conftest.py").move(ftdir.join("conftest.py")) testdir.tmpdir.join("conftest.py").move(ftdir.join("conftest.py"))
rec = testdir.runpytest_subprocess("--strict") rec = testdir.runpytest("--strict")
rec.assert_outcomes() rec.assert_outcomes()
@ -302,7 +302,7 @@ def test_strict_prohibits_unregistered_markers(testdir):
) )
result = testdir.runpytest("--strict") result = testdir.runpytest("--strict")
assert result.ret != 0 assert result.ret != 0
result.stdout.fnmatch_lines(["*unregisteredmark*not*registered*"]) result.stdout.fnmatch_lines(["'unregisteredmark' not a registered marker"])
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -570,7 +570,8 @@ def test_pytest_exit_msg(testdir):
result.stderr.fnmatch_lines(["Exit: oh noes"]) result.stderr.fnmatch_lines(["Exit: oh noes"])
def test_pytest_fail_notrace(testdir): def test_pytest_fail_notrace_runtest(testdir):
"""Test pytest.fail(..., pytrace=False) does not show tracebacks during test run."""
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
@ -585,6 +586,21 @@ def test_pytest_fail_notrace(testdir):
assert "def teardown_function" not in result.stdout.str() assert "def teardown_function" not in result.stdout.str()
def test_pytest_fail_notrace_collection(testdir):
"""Test pytest.fail(..., pytrace=False) does not show tracebacks during collection."""
testdir.makepyfile(
"""
import pytest
def some_internal_function():
pytest.fail("hello", pytrace=False)
some_internal_function()
"""
)
result = testdir.runpytest()
result.stdout.fnmatch_lines(["hello"])
assert "def some_internal_function()" not in result.stdout.str()
@pytest.mark.parametrize("str_prefix", ["u", ""]) @pytest.mark.parametrize("str_prefix", ["u", ""])
def test_pytest_fail_notrace_non_ascii(testdir, str_prefix): def test_pytest_fail_notrace_non_ascii(testdir, str_prefix):
"""Fix pytest.fail with pytrace=False with non-ascii characters (#1178). """Fix pytest.fail with pytrace=False with non-ascii characters (#1178).