diff --git a/changelog/4826.feature.rst b/changelog/4826.feature.rst new file mode 100644 index 000000000..2afcba1ad --- /dev/null +++ b/changelog/4826.feature.rst @@ -0,0 +1,2 @@ +A warning is now emitted when unknown marks are used as a decorator. +This is often due to a typo, which can lead to silently broken tests. diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 0ce9cb31b..5801dccde 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -31,7 +31,10 @@ which also serve as documentation. Raising errors on unknown marks ------------------------------- -Marks can be registered in ``pytest.ini`` like this: +Unknown marks applied with the ``@pytest.mark.name_of_the_mark`` decorator +will always emit a warning, in order to avoid silently doing something +surprising due to mis-typed names. You can disable the warning for custom +marks by registering them in ``pytest.ini`` like this: .. code-block:: ini diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index e7f08e163..ef81784f4 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -147,8 +147,7 @@ def pytest_collection_modifyitems(items, config): def pytest_configure(config): config._old_mark_config = MARK_GEN._config - if config.option.strict: - MARK_GEN._config = config + MARK_GEN._config = config empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 5186e7545..dfb358052 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -11,6 +11,7 @@ from ..compat import getfslineno from ..compat import MappingMixin from ..compat import NOTSET from _pytest.outcomes import fail +from _pytest.warning_types import PytestWarning EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" @@ -283,28 +284,37 @@ class MarkGenerator(object): on the ``test_function`` object. """ _config = None + _markers = set() def __getattr__(self, name): if name[0] == "_": raise AttributeError("Marker name must NOT start with underscore") + if self._config is not None: - self._check(name) + self._update_markers(name) + if name not in self._markers: + warnings.warn( + "Unknown mark %r. You can register custom marks to avoid this " + "warning, without risking typos that break your tests. See " + "https://docs.pytest.org/en/latest/mark.html for details." % name, + PytestWarning, + ) + if self._config.option.strict: + fail("{!r} not a registered marker".format(name), pytrace=False) + return MarkDecorator(Mark(name, (), {})) - def _check(self, name): - try: - if name in self._markers: - return - except AttributeError: - pass - self._markers = values = set() - for line in self._config.getini("markers"): - marker = line.split(":", 1)[0] - marker = marker.rstrip() - x = marker.split("(", 1)[0] - values.add(x) + def _update_markers(self, name): + # We store a set of registered markers as a performance optimisation, + # but more could be added to `self._config` by other plugins at runtime. + # If we see an unknown marker, we therefore update the set and try again! if name not in self._markers: - fail("{!r} not a registered marker".format(name), pytrace=False) + for line in self._config.getini("markers"): + # example lines: "skipif(condition): skip the given test if..." + # or "hypothesis: tests which use Hypothesis", so to get the + # marker name we we split on both `:` and `(`. + marker = line.split(":")[0].split("(")[0].strip() + self._markers.add(marker) MARK_GEN = MarkGenerator()