Merge remote-tracking branch 'origin/master' into mm
This commit is contained in:
commit
fb3ae5eaa9
|
@ -0,0 +1 @@
|
||||||
|
Fix issue where fixtures dependent on other parametrized fixtures would be erroneously parametrized.
|
|
@ -0,0 +1,2 @@
|
||||||
|
Show the test module being collected when emitting ``PytestCollectionWarning`` messages for
|
||||||
|
test classes with ``__init__`` and ``__new__`` methods to make it easier to pin down the problem.
|
|
@ -1127,18 +1127,40 @@ class FixtureManager(object):
|
||||||
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
|
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
|
||||||
session.config.pluginmanager.register(self, "funcmanage")
|
session.config.pluginmanager.register(self, "funcmanage")
|
||||||
|
|
||||||
|
def _get_direct_parametrize_args(self, node):
|
||||||
|
"""This function returns all the direct parametrization
|
||||||
|
arguments of a node, so we don't mistake them for fixtures
|
||||||
|
|
||||||
|
Check https://github.com/pytest-dev/pytest/issues/5036
|
||||||
|
|
||||||
|
This things are done later as well when dealing with parametrization
|
||||||
|
so this could be improved
|
||||||
|
"""
|
||||||
|
from _pytest.mark import ParameterSet
|
||||||
|
|
||||||
|
parametrize_argnames = []
|
||||||
|
for marker in node.iter_markers(name="parametrize"):
|
||||||
|
if not marker.kwargs.get("indirect", False):
|
||||||
|
p_argnames, _ = ParameterSet._parse_parametrize_args(
|
||||||
|
*marker.args, **marker.kwargs
|
||||||
|
)
|
||||||
|
parametrize_argnames.extend(p_argnames)
|
||||||
|
|
||||||
|
return parametrize_argnames
|
||||||
|
|
||||||
def getfixtureinfo(self, node, func, cls, funcargs=True):
|
def getfixtureinfo(self, node, func, cls, funcargs=True):
|
||||||
if funcargs and not getattr(node, "nofuncargs", False):
|
if funcargs and not getattr(node, "nofuncargs", False):
|
||||||
argnames = getfuncargnames(func, cls=cls)
|
argnames = getfuncargnames(func, cls=cls)
|
||||||
else:
|
else:
|
||||||
argnames = ()
|
argnames = ()
|
||||||
|
|
||||||
usefixtures = itertools.chain.from_iterable(
|
usefixtures = itertools.chain.from_iterable(
|
||||||
mark.args for mark in node.iter_markers(name="usefixtures")
|
mark.args for mark in node.iter_markers(name="usefixtures")
|
||||||
)
|
)
|
||||||
initialnames = tuple(usefixtures) + argnames
|
initialnames = tuple(usefixtures) + argnames
|
||||||
fm = node.session._fixturemanager
|
fm = node.session._fixturemanager
|
||||||
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
|
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
|
||||||
initialnames, node
|
initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
|
||||||
)
|
)
|
||||||
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
||||||
|
|
||||||
|
@ -1172,7 +1194,7 @@ class FixtureManager(object):
|
||||||
autousenames.extend(basenames)
|
autousenames.extend(basenames)
|
||||||
return autousenames
|
return autousenames
|
||||||
|
|
||||||
def getfixtureclosure(self, fixturenames, parentnode):
|
def getfixtureclosure(self, fixturenames, parentnode, ignore_args=()):
|
||||||
# collect the closure of all fixtures , starting with the given
|
# collect the closure of all fixtures , starting with the given
|
||||||
# fixturenames as the initial set. As we have to visit all
|
# fixturenames as the initial set. As we have to visit all
|
||||||
# factory definitions anyway, we also return an arg2fixturedefs
|
# factory definitions anyway, we also return an arg2fixturedefs
|
||||||
|
@ -1200,6 +1222,8 @@ class FixtureManager(object):
|
||||||
while lastlen != len(fixturenames_closure):
|
while lastlen != len(fixturenames_closure):
|
||||||
lastlen = len(fixturenames_closure)
|
lastlen = len(fixturenames_closure)
|
||||||
for argname in fixturenames_closure:
|
for argname in fixturenames_closure:
|
||||||
|
if argname in ignore_args:
|
||||||
|
continue
|
||||||
if argname in arg2fixturedefs:
|
if argname in arg2fixturedefs:
|
||||||
continue
|
continue
|
||||||
fixturedefs = self.getfixturedefs(argname, parentid)
|
fixturedefs = self.getfixturedefs(argname, parentid)
|
||||||
|
|
|
@ -103,8 +103,11 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
|
||||||
else:
|
else:
|
||||||
return cls(parameterset, marks=[], id=None)
|
return cls(parameterset, marks=[], id=None)
|
||||||
|
|
||||||
@classmethod
|
@staticmethod
|
||||||
def _for_parametrize(cls, argnames, argvalues, func, config, function_definition):
|
def _parse_parametrize_args(argnames, argvalues, **_):
|
||||||
|
"""It receives an ignored _ (kwargs) argument so this function can
|
||||||
|
take also calls from parametrize ignoring scope, indirect, and other
|
||||||
|
arguments..."""
|
||||||
if not isinstance(argnames, (tuple, list)):
|
if not isinstance(argnames, (tuple, list)):
|
||||||
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
||||||
force_tuple = len(argnames) == 1
|
force_tuple = len(argnames) == 1
|
||||||
|
@ -113,6 +116,11 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
|
||||||
parameters = [
|
parameters = [
|
||||||
ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
|
ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
|
||||||
]
|
]
|
||||||
|
return argnames, parameters
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _for_parametrize(cls, argnames, argvalues, func, config, function_definition):
|
||||||
|
argnames, parameters = cls._parse_parametrize_args(argnames, argvalues)
|
||||||
del argvalues
|
del argvalues
|
||||||
|
|
||||||
if parameters:
|
if parameters:
|
||||||
|
|
|
@ -719,7 +719,8 @@ class Class(PyCollector):
|
||||||
self.warn(
|
self.warn(
|
||||||
PytestCollectionWarning(
|
PytestCollectionWarning(
|
||||||
"cannot collect test class %r because it has a "
|
"cannot collect test class %r because it has a "
|
||||||
"__init__ constructor" % self.obj.__name__
|
"__init__ constructor (from: %s)"
|
||||||
|
% (self.obj.__name__, self.parent.nodeid)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
|
@ -727,7 +728,8 @@ class Class(PyCollector):
|
||||||
self.warn(
|
self.warn(
|
||||||
PytestCollectionWarning(
|
PytestCollectionWarning(
|
||||||
"cannot collect test class %r because it has a "
|
"cannot collect test class %r because it has a "
|
||||||
"__new__ constructor" % self.obj.__name__
|
"__new__ constructor (from: %s)"
|
||||||
|
% (self.obj.__name__, self.parent.nodeid)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
|
|
|
@ -146,7 +146,24 @@ class TestClass(object):
|
||||||
result = testdir.runpytest("-rw")
|
result = testdir.runpytest("-rw")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"*cannot collect test class 'TestClass1' because it has a __init__ constructor"
|
"*cannot collect test class 'TestClass1' because it has "
|
||||||
|
"a __init__ constructor (from: test_class_with_init_warning.py)"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_class_with_new_warning(self, testdir):
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
class TestClass1(object):
|
||||||
|
def __new__(self):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest("-rw")
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"*cannot collect test class 'TestClass1' because it has "
|
||||||
|
"a __new__ constructor (from: test_class_with_new_warning.py)"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3950,3 +3950,46 @@ def test_call_fixture_function_error():
|
||||||
|
|
||||||
with pytest.raises(pytest.fail.Exception):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
assert fix() == 1
|
assert fix() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_fixture_param_shadowing(testdir):
|
||||||
|
"""Parametrized arguments would be shadowed if a fixture with the same name also exists (#5036)"""
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.fixture(params=['a', 'b'])
|
||||||
|
def argroot(request):
|
||||||
|
return request.param
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def arg(argroot):
|
||||||
|
return argroot
|
||||||
|
|
||||||
|
# This should only be parametrized directly
|
||||||
|
@pytest.mark.parametrize("arg", [1])
|
||||||
|
def test_direct(arg):
|
||||||
|
assert arg == 1
|
||||||
|
|
||||||
|
# This should be parametrized based on the fixtures
|
||||||
|
def test_normal_fixture(arg):
|
||||||
|
assert isinstance(arg, str)
|
||||||
|
|
||||||
|
# Indirect should still work:
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def arg2(request):
|
||||||
|
return 2*request.param
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg2", [1], indirect=True)
|
||||||
|
def test_indirect(arg2):
|
||||||
|
assert arg2 == 2
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
# Only one test should have run
|
||||||
|
result = testdir.runpytest("-v")
|
||||||
|
result.assert_outcomes(passed=4)
|
||||||
|
result.stdout.fnmatch_lines(["*::test_direct[[]1[]]*"])
|
||||||
|
result.stdout.fnmatch_lines(["*::test_normal_fixture[[]a[]]*"])
|
||||||
|
result.stdout.fnmatch_lines(["*::test_normal_fixture[[]b[]]*"])
|
||||||
|
result.stdout.fnmatch_lines(["*::test_indirect[[]1[]]*"])
|
||||||
|
|
Loading…
Reference in New Issue