Merged in w00t0r/pytest-fixes (pull request #35)
Fixed issue #306: Keywords and markers are now matched in a defined way. Also applied some pep8 formatting while fixing.
This commit is contained in:
commit
655afba17d
|
@ -216,6 +216,9 @@ class Node(object):
|
||||||
#: keywords/markers collected from all scopes
|
#: keywords/markers collected from all scopes
|
||||||
self.keywords = NodeKeywords(self)
|
self.keywords = NodeKeywords(self)
|
||||||
|
|
||||||
|
#: allow adding of extra keywords to use for matching
|
||||||
|
self.extra_keyword_matches = set()
|
||||||
|
|
||||||
#self.extrainit()
|
#self.extrainit()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -307,6 +310,14 @@ class Node(object):
|
||||||
chain.reverse()
|
chain.reverse()
|
||||||
return chain
|
return chain
|
||||||
|
|
||||||
|
def listextrakeywords(self):
|
||||||
|
""" Return a set of all extra keywords in self and any parents."""
|
||||||
|
extra_keywords = set()
|
||||||
|
item = self
|
||||||
|
for item in self.listchain():
|
||||||
|
extra_keywords.update(item.extra_keyword_matches)
|
||||||
|
return extra_keywords
|
||||||
|
|
||||||
def listnames(self):
|
def listnames(self):
|
||||||
return [x.name for x in self.listchain()]
|
return [x.name for x in self.listchain()]
|
||||||
|
|
||||||
|
|
120
_pytest/mark.py
120
_pytest/mark.py
|
@ -1,44 +1,56 @@
|
||||||
""" generic mechanism for marking and selecting python functions. """
|
""" generic mechanism for marking and selecting python functions. """
|
||||||
import pytest, py
|
import pytest, py
|
||||||
|
|
||||||
|
|
||||||
def pytest_namespace():
|
def pytest_namespace():
|
||||||
return {'mark': MarkGenerator()}
|
return {'mark': MarkGenerator()}
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
group = parser.getgroup("general")
|
group = parser.getgroup("general")
|
||||||
group._addoption('-k',
|
group._addoption(
|
||||||
|
'-k',
|
||||||
action="store", dest="keyword", default='', metavar="EXPRESSION",
|
action="store", dest="keyword", default='', metavar="EXPRESSION",
|
||||||
help="only run tests which match the given substring expression. "
|
help="only run tests which match the given substring expression. "
|
||||||
"An expression is a python evaluatable expression "
|
"An expression is a python evaluatable expression "
|
||||||
"where all names are substring-matched against test names "
|
"where all names are substring-matched against test names "
|
||||||
"and keywords. Example: -k 'test_method or test_other' "
|
"and their parent classes. Example: -k 'test_method or test "
|
||||||
"matches all test functions whose name contains "
|
"other' matches all test functions and classes whose name "
|
||||||
"'test_method' or 'test_other'.")
|
"contains 'test_method' or 'test_other'. "
|
||||||
|
"Additionally keywords are matched to classes and functions "
|
||||||
|
"containing extra names in their 'extra_keyword_matches' set, "
|
||||||
|
"as well as functions which have names assigned directly to them."
|
||||||
|
)
|
||||||
|
|
||||||
group._addoption("-m",
|
group._addoption(
|
||||||
|
"-m",
|
||||||
action="store", dest="markexpr", default="", metavar="MARKEXPR",
|
action="store", dest="markexpr", default="", metavar="MARKEXPR",
|
||||||
help="only run tests matching given mark expression. "
|
help="only run tests matching given mark expression. "
|
||||||
"example: -m 'mark1 and not mark2'."
|
"example: -m 'mark1 and not mark2'."
|
||||||
)
|
)
|
||||||
|
|
||||||
group.addoption("--markers", action="store_true", help=
|
group.addoption(
|
||||||
"show markers (builtin, plugin and per-project ones).")
|
"--markers", action="store_true",
|
||||||
|
help="show markers (builtin, plugin and per-project ones)."
|
||||||
|
)
|
||||||
|
|
||||||
parser.addini("markers", "markers for test functions", 'linelist')
|
parser.addini("markers", "markers for test functions", 'linelist')
|
||||||
|
|
||||||
|
|
||||||
def pytest_cmdline_main(config):
|
def pytest_cmdline_main(config):
|
||||||
if config.option.markers:
|
if config.option.markers:
|
||||||
config.pluginmanager.do_configure(config)
|
config.pluginmanager.do_configure(config)
|
||||||
tw = py.io.TerminalWriter()
|
tw = py.io.TerminalWriter()
|
||||||
for line in config.getini("markers"):
|
for line in config.getini("markers"):
|
||||||
name, rest = line.split(":", 1)
|
name, rest = line.split(":", 1)
|
||||||
tw.write("@pytest.mark.%s:" % name, bold=True)
|
tw.write("@pytest.mark.%s:" % name, bold=True)
|
||||||
tw.line(rest)
|
tw.line(rest)
|
||||||
tw.line()
|
tw.line()
|
||||||
config.pluginmanager.do_unconfigure(config)
|
config.pluginmanager.do_unconfigure(config)
|
||||||
return 0
|
return 0
|
||||||
pytest_cmdline_main.tryfirst = True
|
pytest_cmdline_main.tryfirst = True
|
||||||
|
|
||||||
|
|
||||||
def pytest_collection_modifyitems(items, config):
|
def pytest_collection_modifyitems(items, config):
|
||||||
keywordexpr = config.option.keyword
|
keywordexpr = config.option.keyword
|
||||||
matchexpr = config.option.markexpr
|
matchexpr = config.option.markexpr
|
||||||
|
@ -67,32 +79,76 @@ def pytest_collection_modifyitems(items, config):
|
||||||
config.hook.pytest_deselected(items=deselected)
|
config.hook.pytest_deselected(items=deselected)
|
||||||
items[:] = remaining
|
items[:] = remaining
|
||||||
|
|
||||||
class BoolDict:
|
|
||||||
def __init__(self, mydict):
|
|
||||||
self._mydict = mydict
|
|
||||||
def __getitem__(self, name):
|
|
||||||
return name in self._mydict
|
|
||||||
|
|
||||||
class SubstringDict:
|
class MarkMapping:
|
||||||
def __init__(self, mydict):
|
"""Provides a local mapping for markers.
|
||||||
self._mydict = mydict
|
Only the marker names from the given :class:`NodeKeywords` will be mapped,
|
||||||
def __getitem__(self, name):
|
so the names are taken only from :class:`MarkInfo` or
|
||||||
for key in self._mydict:
|
:class:`MarkDecorator` items.
|
||||||
if name in key:
|
"""
|
||||||
|
def __init__(self, keywords):
|
||||||
|
mymarks = set()
|
||||||
|
for key, value in keywords.items():
|
||||||
|
if isinstance(value, MarkInfo) or isinstance(value, MarkDecorator):
|
||||||
|
mymarks.add(key)
|
||||||
|
self._mymarks = mymarks
|
||||||
|
|
||||||
|
def __getitem__(self, markname):
|
||||||
|
return markname in self._mymarks
|
||||||
|
|
||||||
|
|
||||||
|
class KeywordMapping:
|
||||||
|
"""Provides a local mapping for keywords.
|
||||||
|
Given a list of names, map any substring of one of these names to True.
|
||||||
|
"""
|
||||||
|
def __init__(self, names):
|
||||||
|
self._names = names
|
||||||
|
|
||||||
|
def __getitem__(self, subname):
|
||||||
|
for name in self._names:
|
||||||
|
if subname in name:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def matchmark(colitem, matchexpr):
|
|
||||||
return eval(matchexpr, {}, BoolDict(colitem.keywords))
|
def matchmark(colitem, markexpr):
|
||||||
|
"""Tries to match on any marker names, attached to the given colitem."""
|
||||||
|
return eval(markexpr, {}, MarkMapping(colitem.keywords))
|
||||||
|
|
||||||
|
|
||||||
def matchkeyword(colitem, keywordexpr):
|
def matchkeyword(colitem, keywordexpr):
|
||||||
|
"""Tries to match given keyword expression to given collector item.
|
||||||
|
|
||||||
|
Will match on the name of colitem, including the names of its parents.
|
||||||
|
Only matches names of items which are either a :class:`Class` or a
|
||||||
|
:class:`Function`.
|
||||||
|
Additionally, matches on names in the 'extra_keyword_matches' set of
|
||||||
|
any item, as well as names directly assigned to test functions.
|
||||||
|
"""
|
||||||
keywordexpr = keywordexpr.replace("-", "not ")
|
keywordexpr = keywordexpr.replace("-", "not ")
|
||||||
return eval(keywordexpr, {}, SubstringDict(colitem.keywords))
|
mapped_names = set()
|
||||||
|
|
||||||
|
# Add the names of the current item and any parent items
|
||||||
|
for item in colitem.listchain():
|
||||||
|
if not isinstance(item, pytest.Instance):
|
||||||
|
mapped_names.add(item.name)
|
||||||
|
|
||||||
|
# Add the names added as extra keywords to current or parent items
|
||||||
|
for name in colitem.listextrakeywords():
|
||||||
|
mapped_names.add(name)
|
||||||
|
|
||||||
|
# Add the names attached to the current function through direct assignment
|
||||||
|
for name in colitem.function.func_dict:
|
||||||
|
mapped_names.add(name)
|
||||||
|
|
||||||
|
return eval(keywordexpr, {}, KeywordMapping(mapped_names))
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
if config.option.strict:
|
if config.option.strict:
|
||||||
pytest.mark._config = config
|
pytest.mark._config = config
|
||||||
|
|
||||||
|
|
||||||
class MarkGenerator:
|
class MarkGenerator:
|
||||||
""" Factory for :class:`MarkDecorator` objects - exposed as
|
""" Factory for :class:`MarkDecorator` objects - exposed as
|
||||||
a ``py.test.mark`` singleton instance. Example::
|
a ``py.test.mark`` singleton instance. Example::
|
||||||
|
@ -126,6 +182,7 @@ class MarkGenerator:
|
||||||
if name not in self._markers:
|
if name not in self._markers:
|
||||||
raise AttributeError("%r not a registered marker" % (name,))
|
raise AttributeError("%r not a registered marker" % (name,))
|
||||||
|
|
||||||
|
|
||||||
class MarkDecorator:
|
class MarkDecorator:
|
||||||
""" A decorator for test functions and test classes. When applied
|
""" A decorator for test functions and test classes. When applied
|
||||||
it will create :class:`MarkInfo` objects which may be
|
it will create :class:`MarkInfo` objects which may be
|
||||||
|
@ -149,7 +206,7 @@ class MarkDecorator:
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
d = self.__dict__.copy()
|
d = self.__dict__.copy()
|
||||||
name = d.pop('markname')
|
name = d.pop('markname')
|
||||||
return "<MarkDecorator %r %r>" %(name, d)
|
return "<MarkDecorator %r %r>" % (name, d)
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
""" if passed a single callable argument: decorate it with mark info.
|
""" if passed a single callable argument: decorate it with mark info.
|
||||||
|
@ -162,15 +219,17 @@ class MarkDecorator:
|
||||||
if hasattr(func, 'pytestmark'):
|
if hasattr(func, 'pytestmark'):
|
||||||
l = func.pytestmark
|
l = func.pytestmark
|
||||||
if not isinstance(l, list):
|
if not isinstance(l, list):
|
||||||
func.pytestmark = [l, self]
|
func.pytestmark = [l, self]
|
||||||
else:
|
else:
|
||||||
l.append(self)
|
l.append(self)
|
||||||
else:
|
else:
|
||||||
func.pytestmark = [self]
|
func.pytestmark = [self]
|
||||||
else:
|
else:
|
||||||
holder = getattr(func, self.markname, None)
|
holder = getattr(func, self.markname, None)
|
||||||
if holder is None:
|
if holder is None:
|
||||||
holder = MarkInfo(self.markname, self.args, self.kwargs)
|
holder = MarkInfo(
|
||||||
|
self.markname, self.args, self.kwargs
|
||||||
|
)
|
||||||
setattr(func, self.markname, holder)
|
setattr(func, self.markname, holder)
|
||||||
else:
|
else:
|
||||||
holder.add(self.args, self.kwargs)
|
holder.add(self.args, self.kwargs)
|
||||||
|
@ -180,6 +239,7 @@ class MarkDecorator:
|
||||||
args = self.args + args
|
args = self.args + args
|
||||||
return self.__class__(self.markname, args=args, kwargs=kw)
|
return self.__class__(self.markname, args=args, kwargs=kw)
|
||||||
|
|
||||||
|
|
||||||
class MarkInfo:
|
class MarkInfo:
|
||||||
""" Marking object created by :class:`MarkDecorator` instances. """
|
""" Marking object created by :class:`MarkDecorator` instances. """
|
||||||
def __init__(self, name, args, kwargs):
|
def __init__(self, name, args, kwargs):
|
||||||
|
@ -193,7 +253,8 @@ class MarkInfo:
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<MarkInfo %r args=%r kwargs=%r>" % (
|
return "<MarkInfo %r args=%r kwargs=%r>" % (
|
||||||
self.name, self.args, self.kwargs)
|
self.name, self.args, self.kwargs
|
||||||
|
)
|
||||||
|
|
||||||
def add(self, args, kwargs):
|
def add(self, args, kwargs):
|
||||||
""" add a MarkInfo with the given args and kwargs. """
|
""" add a MarkInfo with the given args and kwargs. """
|
||||||
|
@ -205,4 +266,3 @@ class MarkInfo:
|
||||||
""" yield MarkInfo objects each relating to a marking-call. """
|
""" yield MarkInfo objects each relating to a marking-call. """
|
||||||
for args, kwargs in self._arglist:
|
for args, kwargs in self._arglist:
|
||||||
yield MarkInfo(self.name, args, kwargs)
|
yield MarkInfo(self.name, args, kwargs)
|
||||||
|
|
||||||
|
|
|
@ -345,6 +345,24 @@ class TestFunctional:
|
||||||
assert l[0].args == ("pos0",)
|
assert l[0].args == ("pos0",)
|
||||||
assert l[1].args == ("pos1",)
|
assert l[1].args == ("pos1",)
|
||||||
|
|
||||||
|
def test_no_marker_match_on_unmarked_names(self, testdir):
|
||||||
|
p = testdir.makepyfile("""
|
||||||
|
import pytest
|
||||||
|
@pytest.mark.shouldmatch
|
||||||
|
def test_marked():
|
||||||
|
assert 1
|
||||||
|
|
||||||
|
def test_unmarked():
|
||||||
|
assert 1
|
||||||
|
""")
|
||||||
|
reprec = testdir.inline_run("-m", "test_unmarked", p)
|
||||||
|
passed, skipped, failed = reprec.listoutcomes()
|
||||||
|
assert len(passed) + len(skipped) + len(failed) == 0
|
||||||
|
dlist = reprec.getcalls("pytest_deselected")
|
||||||
|
deselected_tests = dlist[0].items
|
||||||
|
assert len(deselected_tests) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_keywords_at_node_level(self, testdir):
|
def test_keywords_at_node_level(self, testdir):
|
||||||
p = testdir.makepyfile("""
|
p = testdir.makepyfile("""
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -382,7 +400,6 @@ class TestKeywordSelection:
|
||||||
assert len(reprec.getcalls('pytest_deselected')) == 1
|
assert len(reprec.getcalls('pytest_deselected')) == 1
|
||||||
|
|
||||||
for keyword in ['test_one', 'est_on']:
|
for keyword in ['test_one', 'est_on']:
|
||||||
#yield check, keyword, 'test_one'
|
|
||||||
check(keyword, 'test_one')
|
check(keyword, 'test_one')
|
||||||
check('TestClass and test', 'test_method_one')
|
check('TestClass and test', 'test_method_one')
|
||||||
|
|
||||||
|
@ -401,7 +418,7 @@ class TestKeywordSelection:
|
||||||
def pytest_pycollect_makeitem(__multicall__, name):
|
def pytest_pycollect_makeitem(__multicall__, name):
|
||||||
if name == "TestClass":
|
if name == "TestClass":
|
||||||
item = __multicall__.execute()
|
item = __multicall__.execute()
|
||||||
item.keywords["xxx"] = True
|
item.extra_keyword_matches.add("xxx")
|
||||||
return item
|
return item
|
||||||
""")
|
""")
|
||||||
reprec = testdir.inline_run(p.dirpath(), '-s', '-k', keyword)
|
reprec = testdir.inline_run(p.dirpath(), '-s', '-k', keyword)
|
||||||
|
@ -440,3 +457,22 @@ class TestKeywordSelection:
|
||||||
reprec = testdir.inline_run("-k", "mykeyword", p)
|
reprec = testdir.inline_run("-k", "mykeyword", p)
|
||||||
passed, skipped, failed = reprec.countoutcomes()
|
passed, skipped, failed = reprec.countoutcomes()
|
||||||
assert failed == 1
|
assert failed == 1
|
||||||
|
|
||||||
|
def test_no_magic_values(self, testdir):
|
||||||
|
"""Make sure the tests do not match on magic values,
|
||||||
|
no double underscored values, like '__dict__',
|
||||||
|
and no instance values, like '()'.
|
||||||
|
"""
|
||||||
|
p = testdir.makepyfile("""
|
||||||
|
def test_one(): assert 1
|
||||||
|
""")
|
||||||
|
def assert_test_is_not_selected(keyword):
|
||||||
|
reprec = testdir.inline_run("-k", keyword, p)
|
||||||
|
passed, skipped, failed = reprec.countoutcomes()
|
||||||
|
dlist = reprec.getcalls("pytest_deselected")
|
||||||
|
assert passed + skipped + failed == 0
|
||||||
|
deselected_tests = dlist[0].items
|
||||||
|
assert len(deselected_tests) == 1
|
||||||
|
|
||||||
|
assert_test_is_not_selected("__")
|
||||||
|
assert_test_is_not_selected("()")
|
||||||
|
|
Loading…
Reference in New Issue