Merge pull request #3317 from RonnyPfannschmidt/marker-pristine-node-storage

introduce a distinct searchable non-broken storage for markers
This commit is contained in:
Bruno Oliveira 2018-04-09 19:40:12 -03:00 committed by GitHub
commit 715337011b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 260 additions and 110 deletions

View File

@ -32,7 +32,8 @@ RESULT_LOG = (
)
MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning(
"MarkInfo objects are deprecated as they contain the merged marks"
"MarkInfo objects are deprecated as they contain the merged marks.\n"
"Please use node.iter_markers to iterate over markers correctly"
)
MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(

View File

@ -5,6 +5,7 @@ import inspect
import sys
import warnings
from collections import OrderedDict, deque, defaultdict
from more_itertools import flatten
import attr
import py
@ -371,10 +372,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
:arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
created by a call to ``pytest.mark.NAME(...)``.
"""
try:
self.node.keywords[marker.markname] = marker
except AttributeError:
raise ValueError(marker)
self.node.add_marker(marker)
def raiseerror(self, msg):
""" raise a FixtureLookupError with the given message. """
@ -985,10 +983,9 @@ class FixtureManager(object):
argnames = getfuncargnames(func, cls=cls)
else:
argnames = ()
usefixtures = getattr(func, "usefixtures", None)
usefixtures = flatten(mark.args for mark in node.iter_markers() if mark.name == "usefixtures")
initialnames = argnames
if usefixtures is not None:
initialnames = usefixtures.args + initialnames
initialnames = tuple(usefixtures) + initialnames
fm = node.session._fixturemanager
names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames,
node)
@ -1070,6 +1067,8 @@ class FixtureManager(object):
fixturedef = faclist[-1]
if fixturedef.params is not None:
parametrize_func = getattr(metafunc.function, 'parametrize', None)
if parametrize_func is not None:
parametrize_func = parametrize_func.combined
func_params = getattr(parametrize_func, 'args', [[None]])
func_kwargs = getattr(parametrize_func, 'kwargs', {})
# skip directly parametrized arguments

View File

@ -4,7 +4,6 @@ import sys
import platform
import traceback
from . import MarkDecorator, MarkInfo
from ..outcomes import fail, TEST_OUTCOME
@ -28,22 +27,15 @@ class MarkEvaluator(object):
self._mark_name = name
def __bool__(self):
self._marks = self._get_marks()
return bool(self._marks)
# dont cache here to prevent staleness
return bool(self._get_marks())
__nonzero__ = __bool__
def wasvalid(self):
return not hasattr(self, 'exc')
def _get_marks(self):
keyword = self.item.keywords.get(self._mark_name)
if isinstance(keyword, MarkDecorator):
return [keyword.mark]
elif isinstance(keyword, MarkInfo):
return [x.combined for x in keyword]
else:
return []
return [x for x in self.item.iter_markers() if x.name == self._mark_name]
def invalidraise(self, exc):
raises = self.get('raises')

View File

@ -4,9 +4,10 @@ from operator import attrgetter
import inspect
import attr
from ..deprecated import MARK_PARAMETERSET_UNPACKING
from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE
from ..compat import NOTSET, getfslineno
from six.moves import map
from six.moves import map, reduce
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
@ -113,11 +114,21 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
@attr.s(frozen=True)
class Mark(object):
name = attr.ib()
args = attr.ib()
kwargs = attr.ib()
#: name of the mark
name = attr.ib(type=str)
#: positional arguments of the mark decorator
args = attr.ib(type="List[object]")
#: keyword arguments of the mark decorator
kwargs = attr.ib(type="Dict[str, object]")
def combined_with(self, other):
"""
:param other: the mark to combine with
:type other: Mark
:rtype: Mark
combines by appending aargs and merging the mappings
"""
assert self.name == other.name
return Mark(
self.name, self.args + other.args,
@ -233,7 +244,7 @@ def store_legacy_markinfo(func, mark):
raise TypeError("got {mark!r} instead of a Mark".format(mark=mark))
holder = getattr(func, mark.name, None)
if holder is None:
holder = MarkInfo(mark)
holder = MarkInfo.for_mark(mark)
setattr(func, mark.name, holder)
else:
holder.add_mark(mark)
@ -260,23 +271,29 @@ def _marked(func, mark):
invoked more than once.
"""
try:
func_mark = getattr(func, mark.name)
func_mark = getattr(func, getattr(mark, 'combined', mark).name)
except AttributeError:
return False
return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs
return any(mark == info.combined for info in func_mark)
@attr.s
class MarkInfo(object):
""" Marking object created by :class:`MarkDecorator` instances. """
def __init__(self, mark):
assert isinstance(mark, Mark), repr(mark)
self.combined = mark
self._marks = [mark]
_marks = attr.ib()
combined = attr.ib(
repr=False,
default=attr.Factory(lambda self: reduce(Mark.combined_with, self._marks),
takes_self=True))
name = alias('combined.name')
args = alias('combined.args')
kwargs = alias('combined.kwargs')
name = alias('combined.name', warning=MARK_INFO_ATTRIBUTE)
args = alias('combined.args', warning=MARK_INFO_ATTRIBUTE)
kwargs = alias('combined.kwargs', warning=MARK_INFO_ATTRIBUTE)
@classmethod
def for_mark(cls, mark):
return cls([mark])
def __repr__(self):
return "<MarkInfo {0!r}>".format(self.combined)
@ -288,7 +305,7 @@ class MarkInfo(object):
def __iter__(self):
""" yield MarkInfo objects each relating to a marking-call. """
return map(MarkInfo, self._marks)
return map(MarkInfo.for_mark, self._marks)
class MarkGenerator(object):
@ -365,3 +382,33 @@ class NodeKeywords(MappingMixin):
def __repr__(self):
return "<NodeKeywords for node %s>" % (self.node, )
@attr.s(cmp=False, hash=False)
class NodeMarkers(object):
"""
internal strucutre for storing marks belongong to a node
..warning::
unstable api
"""
own_markers = attr.ib(default=attr.Factory(list))
def update(self, add_markers):
"""update the own markers
"""
self.own_markers.extend(add_markers)
def find(self, name):
"""
find markers in own nodes or parent nodes
needs a better place
"""
for mark in self.own_markers:
if mark.name == name:
yield mark
def __iter__(self):
return iter(self.own_markers)

View File

@ -8,7 +8,7 @@ import attr
import _pytest
import _pytest._code
from _pytest.mark.structures import NodeKeywords
from _pytest.mark.structures import NodeKeywords, MarkInfo
SEP = "/"
@ -90,6 +90,9 @@ class Node(object):
#: keywords/markers collected from all scopes
self.keywords = NodeKeywords(self)
#: the marker objects belonging to this node
self.own_markers = []
#: allow adding of extra keywords to use for matching
self.extra_keyword_matches = set()
@ -178,15 +181,34 @@ class Node(object):
elif not isinstance(marker, MarkDecorator):
raise ValueError("is not a string or pytest.mark.* Marker")
self.keywords[marker.name] = marker
self.own_markers.append(marker)
def iter_markers(self):
"""
iterate over all markers of the node
"""
return (x[1] for x in self.iter_markers_with_node())
def iter_markers_with_node(self):
"""
iterate over all markers of the node
returns sequence of tuples (node, mark)
"""
for node in reversed(self.listchain()):
for mark in node.own_markers:
yield node, mark
def get_marker(self, name):
""" get a marker object from this node or None if
the node doesn't have a marker with that name. """
val = self.keywords.get(name, None)
if val is not None:
from _pytest.mark import MarkInfo, MarkDecorator
if isinstance(val, (MarkDecorator, MarkInfo)):
return val
the node doesn't have a marker with that name.
..warning::
deprecated
"""
markers = [x for x in self.iter_markers() if x.name == name]
if markers:
return MarkInfo(markers)
def listextrakeywords(self):
""" Return a set of all extra keywords in self and any parents."""

View File

@ -28,7 +28,7 @@ from _pytest.compat import (
safe_str, getlocation, enum,
)
from _pytest.outcomes import fail
from _pytest.mark.structures import transfer_markers
from _pytest.mark.structures import transfer_markers, get_unpacked_marks
# relative paths that we use to filter traceback entries from appearing to the user;
@ -117,11 +117,8 @@ def pytest_generate_tests(metafunc):
if hasattr(metafunc.function, attr):
msg = "{0} has '{1}', spelling should be 'parametrize'"
raise MarkerError(msg.format(metafunc.function.__name__, attr))
try:
markers = metafunc.function.parametrize
except AttributeError:
return
for marker in markers:
for marker in metafunc.definition.iter_markers():
if marker.name == 'parametrize':
metafunc.parametrize(*marker.args, **marker.kwargs)
@ -212,11 +209,20 @@ class PyobjContext(object):
class PyobjMixin(PyobjContext):
_ALLOW_MARKERS = True
def __init__(self, *k, **kw):
super(PyobjMixin, self).__init__(*k, **kw)
def obj():
def fget(self):
obj = getattr(self, '_obj', None)
if obj is None:
self._obj = obj = self._getobj()
# XXX evil hack
# used to avoid Instance collector marker duplication
if self._ALLOW_MARKERS:
self.own_markers.extend(get_unpacked_marks(self.obj))
return obj
def fset(self, value):
@ -363,9 +369,15 @@ class PyCollector(PyobjMixin, nodes.Collector):
cls = clscol and clscol.obj or None
transfer_markers(funcobj, cls, module)
fm = self.session._fixturemanager
fixtureinfo = fm.getfixtureinfo(self, funcobj, cls)
metafunc = Metafunc(funcobj, fixtureinfo, self.config,
cls=cls, module=module)
definition = FunctionDefinition(
name=name,
parent=self,
callobj=funcobj,
)
fixtureinfo = fm.getfixtureinfo(definition, funcobj, cls)
metafunc = Metafunc(definition, fixtureinfo, self.config, cls=cls, module=module)
methods = []
if hasattr(module, "pytest_generate_tests"):
methods.append(module.pytest_generate_tests)
@ -524,6 +536,11 @@ class Class(PyCollector):
class Instance(PyCollector):
_ALLOW_MARKERS = False # hack, destroy later
# instances share the object with their parents in a way
# that duplicates markers instances if not taken out
# can be removed at node strucutre reorganization time
def _getobj(self):
return self.parent.obj()
@ -723,15 +740,17 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
test function is defined.
"""
def __init__(self, function, fixtureinfo, config, cls=None, module=None):
def __init__(self, definition, fixtureinfo, config, cls=None, module=None):
#: access to the :class:`_pytest.config.Config` object for the test session
assert isinstance(definition, FunctionDefinition) or type(definition).__name__ == "DefinitionMock"
self.definition = definition
self.config = config
#: the module object where the test function is defined in.
self.module = module
#: underlying python test function
self.function = function
self.function = definition.obj
#: set of fixture names required by the test function
self.fixturenames = fixtureinfo.names_closure
@ -1103,6 +1122,8 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
Python test function.
"""
_genid = None
# disable since functions handle it themselfes
_ALLOW_MARKERS = False
def __init__(self, name, parent, args=None, config=None,
callspec=None, callobj=NOTSET, keywords=None, session=None,
@ -1114,6 +1135,7 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
self.obj = callobj
self.keywords.update(self.obj.__dict__)
self.own_markers.extend(get_unpacked_marks(self.obj))
if callspec:
self.callspec = callspec
# this is total hostile and a mess
@ -1123,6 +1145,7 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
# feel free to cry, this was broken for years before
# and keywords cant fix it per design
self.keywords[mark.name] = mark
self.own_markers.extend(callspec.marks)
if keywords:
self.keywords.update(keywords)
@ -1181,3 +1204,15 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
def setup(self):
super(Function, self).setup()
fixtures.fillfixtures(self)
class FunctionDefinition(Function):
"""
internal hack until we get actual definition nodes instead of the
crappy metafunc hack
"""
def runtest(self):
raise RuntimeError("function definitions are not supposed to be used")
setup = runtest

View File

@ -2,7 +2,6 @@
from __future__ import absolute_import, division, print_function
from _pytest.config import hookimpl
from _pytest.mark import MarkInfo, MarkDecorator
from _pytest.mark.evaluate import MarkEvaluator
from _pytest.outcomes import fail, skip, xfail
@ -60,15 +59,14 @@ def pytest_configure(config):
def pytest_runtest_setup(item):
# Check if skip or skipif are specified as pytest marks
item._skipped_by_mark = False
skipif_info = item.keywords.get('skipif')
if isinstance(skipif_info, (MarkInfo, MarkDecorator)):
eval_skipif = MarkEvaluator(item, 'skipif')
if eval_skipif.istrue():
item._skipped_by_mark = True
skip(eval_skipif.getexplanation())
skip_info = item.keywords.get('skip')
if isinstance(skip_info, (MarkInfo, MarkDecorator)):
for skip_info in item.iter_markers():
if skip_info.name != 'skip':
continue
item._skipped_by_mark = True
if 'reason' in skip_info.kwargs:
skip(skip_info.kwargs['reason'])

View File

@ -60,8 +60,8 @@ def catch_warnings_for_item(item):
for arg in inifilters:
_setoption(warnings, arg)
mark = item.get_marker('filterwarnings')
if mark:
for mark in item.iter_markers():
if mark.name == 'filterwarnings':
for arg in mark.args:
warnings._setoption(arg)

1
changelog/3317.feature Normal file
View File

@ -0,0 +1 @@
introduce correct per node mark handling and deprecate the always incorrect existing mark handling

View File

@ -330,11 +330,10 @@ specifies via named environments::
"env(name): mark test to run only on named environment")
def pytest_runtest_setup(item):
envmarker = item.get_marker("env")
if envmarker is not None:
envname = envmarker.args[0]
if envname != item.config.getoption("-E"):
pytest.skip("test requires env %r" % envname)
envnames = [mark.args[0] for mark in item.iter_markers() if mark.name == "env"]
if envnames:
if item.config.getoption("-E") not in envnames:
pytest.skip("test requires env in %r" % envnames)
A test file using this local plugin::
@ -403,10 +402,9 @@ Below is the config file that will be used in the next examples::
import sys
def pytest_runtest_setup(item):
marker = item.get_marker('my_marker')
if marker is not None:
for info in marker:
print('Marker info name={} args={} kwars={}'.format(info.name, info.args, info.kwargs))
for marker in item.iter_markers():
if marker.name == 'my_marker':
print(marker)
sys.stdout.flush()
A custom marker can have its argument set, i.e. ``args`` and ``kwargs`` properties, defined by either invoking it as a callable or using ``pytest.mark.MARKER_NAME.with_args``. These two methods achieve the same effect most of the time.
@ -426,7 +424,7 @@ However, if there is a callable as the single positional argument with no keywor
The output is as follows::
$ pytest -q -s
Marker info name=my_marker args=(<function hello_world at 0xdeadbeef>,) kwars={}
Mark(name='my_marker', args=(<function hello_world at 0xdeadbeef>,), kwargs={})
.
1 passed in 0.12 seconds
@ -460,10 +458,9 @@ test function. From a conftest file we can read it like this::
import sys
def pytest_runtest_setup(item):
g = item.get_marker("glob")
if g is not None:
for info in g:
print ("glob args=%s kwargs=%s" %(info.args, info.kwargs))
for mark in item.iter_markers():
if mark.name == 'glob':
print ("glob args=%s kwargs=%s" %(mark.args, mark.kwargs))
sys.stdout.flush()
Let's run this without capturing output and see what we get::
@ -494,11 +491,10 @@ for your particular platform, you could use the following plugin::
ALL = set("darwin linux win32".split())
def pytest_runtest_setup(item):
if isinstance(item, item.Function):
supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers())
plat = sys.platform
if not item.get_marker(plat):
if ALL.intersection(item.keywords):
pytest.skip("cannot run on platform %s" %(plat))
if supported_platforms and plat not in supported_platforms:
pytest.skip("cannot run on platform %s" % (plat))
then tests will be skipped if they were specified for a different platform.
Let's do a little test file to show how this looks like::
@ -532,7 +528,7 @@ then you will see two tests skipped and two executed tests as expected::
test_plat.py s.s. [100%]
========================= short test summary info ==========================
SKIP [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux
SKIP [2] $REGENDOC_TMPDIR/conftest.py:12: cannot run on platform linux
=================== 2 passed, 2 skipped in 0.12 seconds ====================

View File

@ -389,7 +389,7 @@ Now we can profile which test functions execute the slowest::
========================= slowest 3 test durations =========================
0.30s call test_some_are_slow.py::test_funcslow2
0.20s call test_some_are_slow.py::test_funcslow1
0.16s call test_some_are_slow.py::test_funcfast
0.10s call test_some_are_slow.py::test_funcfast
========================= 3 passed in 0.12 seconds =========================
incremental testing - test steps

View File

@ -26,3 +26,33 @@ which also serve as documentation.
:ref:`fixtures <fixtures>`.
.. currentmodule:: _pytest.mark.structures
.. autoclass:: Mark
:members:
:noindex:
.. `marker-iteration`
Marker iteration
=================
.. versionadded:: 3.6
pytest's marker implementation traditionally worked by simply updating the ``__dict__`` attribute of functions to add markers, in a cumulative manner. As a result of the this, markers would unintendely be passed along class hierarchies in surprising ways plus the API for retriving them was inconsistent, as markers from parameterization would be stored differently than markers applied using the ``@pytest.mark`` decorator and markers added via ``node.add_marker``.
This state of things made it technically next to impossible to use data from markers correctly without having a deep understanding of the internals, leading to subtle and hard to understand bugs in more advanced usages.
Depending on how a marker got declared/changed one would get either a ``MarkerInfo`` which might contain markers from sibling classes,
``MarkDecorators`` when marks came from parameterization or from a ``node.add_marker`` call, discarding prior marks. Also ``MarkerInfo`` acts like a single mark, when it in fact repressents a merged view on multiple marks with the same name.
On top of that markers where not accessible the same way for modules, classes, and functions/methods,
in fact, markers where only accessible in functions, even if they where declared on classes/modules.
A new API to access markers has been introduced in pytest 3.6 in order to solve the problems with the initial design, providing :func:`_pytest.nodes.Node.iter_markers` method to iterate over markers in a consistent manner and reworking the internals, which solved great deal of problems with the initial design.
.. note::
in a future major relase of pytest we will introduce class based markers,
at which points markers will no longer be limited to instances of :py:class:`Mark`

View File

@ -94,6 +94,8 @@ Marks can be used apply meta data to *test functions* (but not fixtures), which
fixtures or plugins.
.. _`pytest.mark.filterwarnings ref`:
pytest.mark.filterwarnings
@ -200,9 +202,9 @@ For example:
def test_function():
...
Will create and attach a :class:`MarkInfo <_pytest.mark.MarkInfo>` object to the collected
Will create and attach a :class:`Mark <_pytest.mark.structures.Mark>` object to the collected
:class:`Item <_pytest.nodes.Item>`, which can then be accessed by fixtures or hooks with
:meth:`Node.get_marker <_pytest.nodes.Node.get_marker>`. The ``mark`` object will have the following attributes:
:meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`. The ``mark`` object will have the following attributes:
.. code-block:: python
@ -685,18 +687,28 @@ MarkDecorator
.. autoclass:: _pytest.mark.MarkDecorator
:members:
MarkGenerator
~~~~~~~~~~~~~
.. autoclass:: _pytest.mark.MarkGenerator
:members:
MarkInfo
~~~~~~~~
.. autoclass:: _pytest.mark.MarkInfo
:members:
Mark
~~~~
.. autoclass:: _pytest.mark.structures.Mark
:members:
Metafunc
~~~~~~~~

View File

@ -260,8 +260,8 @@ Alternatively, you can integrate this functionality with custom markers:
def pytest_collection_modifyitems(session, config, items):
for item in items:
marker = item.get_marker('test_id')
if marker is not None:
for marker in item.iter_markers():
if marker.name == 'test_id':
test_id = marker.args[0]
item.user_properties.append(('test_id', test_id))

View File

@ -1781,6 +1781,8 @@ class TestAutouseManagement(object):
import pytest
values = []
def pytest_generate_tests(metafunc):
if metafunc.cls is None:
assert metafunc.function is test_finish
if metafunc.cls is not None:
metafunc.parametrize("item", [1,2], scope="class")
class TestClass(object):
@ -1798,7 +1800,7 @@ class TestAutouseManagement(object):
assert values == ["setup-1", "step1-1", "step2-1", "teardown-1",
"setup-2", "step1-2", "step2-2", "teardown-2",]
""")
reprec = testdir.inline_run()
reprec = testdir.inline_run('-s')
reprec.assertoutcome(passed=5)
def test_ordering_autouse_before_explicit(self, testdir):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import re
import sys
import attr
import _pytest._code
import py
import pytest
@ -24,13 +24,19 @@ class TestMetafunc(object):
def __init__(self, names):
self.names_closure = names
@attr.s
class DefinitionMock(object):
obj = attr.ib()
names = fixtures.getfuncargnames(func)
fixtureinfo = FixtureInfo(names)
return python.Metafunc(func, fixtureinfo, config)
definition = DefinitionMock(func)
return python.Metafunc(definition, fixtureinfo, config)
def test_no_funcargs(self, testdir):
def function():
pass
metafunc = self.Metafunc(function)
assert not metafunc.fixturenames
repr(metafunc._calls)

View File

@ -8,11 +8,13 @@ from _pytest.mark import (
EMPTY_PARAMETERSET_OPTION,
)
ignore_markinfo = pytest.mark.filterwarnings('ignore:MarkInfo objects:_pytest.deprecated.RemovedInPytest4Warning')
class TestMark(object):
def test_markinfo_repr(self):
from _pytest.mark import MarkInfo, Mark
m = MarkInfo(Mark("hello", (1, 2), {}))
m = MarkInfo.for_mark(Mark("hello", (1, 2), {}))
repr(m)
@pytest.mark.parametrize('attr', ['mark', 'param'])
@ -51,6 +53,7 @@ class TestMark(object):
mark.hello(f)
assert f.hello
@ignore_markinfo
def test_pytest_mark_keywords(self):
mark = Mark()
@ -62,6 +65,7 @@ class TestMark(object):
assert f.world.kwargs['x'] == 3
assert f.world.kwargs['y'] == 4
@ignore_markinfo
def test_apply_multiple_and_merge(self):
mark = Mark()
@ -78,6 +82,7 @@ class TestMark(object):
assert f.world.kwargs['y'] == 1
assert len(f.world.args) == 0
@ignore_markinfo
def test_pytest_mark_positional(self):
mark = Mark()
@ -88,6 +93,7 @@ class TestMark(object):
assert f.world.args[0] == "hello"
mark.world("world")(f)
@ignore_markinfo
def test_pytest_mark_positional_func_and_keyword(self):
mark = Mark()
@ -103,6 +109,7 @@ class TestMark(object):
assert g.world.args[0] is f
assert g.world.kwargs["omega"] == "hello"
@ignore_markinfo
def test_pytest_mark_reuse(self):
mark = Mark()
@ -484,6 +491,7 @@ class TestFunctional(object):
assert 'hello' in keywords
assert 'world' in keywords
@ignore_markinfo
def test_merging_markers(self, testdir):
p = testdir.makepyfile("""
import pytest
@ -509,7 +517,6 @@ class TestFunctional(object):
assert values[1].args == ()
assert values[2].args == ("pos1", )
@pytest.mark.xfail(reason='unfixed')
def test_merging_markers_deep(self, testdir):
# issue 199 - propagate markers into nested classes
p = testdir.makepyfile("""
@ -526,7 +533,7 @@ class TestFunctional(object):
items, rec = testdir.inline_genitems(p)
for item in items:
print(item, item.keywords)
assert 'a' in item.keywords
assert [x for x in item.iter_markers() if x.name == 'a']
def test_mark_decorator_subclass_does_not_propagate_to_base(self, testdir):
p = testdir.makepyfile("""
@ -622,6 +629,7 @@ class TestFunctional(object):
"keyword: *hello*"
])
@ignore_markinfo
def test_merging_markers_two_functions(self, testdir):
p = testdir.makepyfile("""
import pytest
@ -676,6 +684,7 @@ class TestFunctional(object):
reprec = testdir.inline_run()
reprec.assertoutcome(passed=1)
@ignore_markinfo
def test_keyword_added_for_session(self, testdir):
testdir.makeconftest("""
import pytest
@ -715,8 +724,8 @@ class TestFunctional(object):
if isinstance(v, MarkInfo)])
assert marker_names == set(expected_markers)
@pytest.mark.xfail(reason='callspec2.setmulti misuses keywords')
@pytest.mark.issue1540
@pytest.mark.filterwarnings("ignore")
def test_mark_from_parameters(self, testdir):
testdir.makepyfile("""
import pytest