]> git.madduck.net Git - etc/vim.git/blob - tests/optional.py

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Upgrade to Furo 2023.9.10 to fix docs build (#3873)
[etc/vim.git] / tests / optional.py
1 """
2 Allows configuring optional test markers in config, see pyproject.toml.
3
4 Run optional tests with `pytest --run-optional=...`.
5
6 Mark tests to run only if an optional test ISN'T selected by prepending the mark with
7 "no_".
8
9 You can specify a "no_" prefix straight in config, in which case you can mark tests
10 to run when this tests ISN'T selected by omitting the "no_" prefix.
11
12 Specifying the name of the default behavior in `--run-optional=` is harmless.
13
14 Adapted from https://pypi.org/project/pytest-optional-tests/, (c) 2019 Reece Hart
15 """
16
17 import itertools
18 import logging
19 import re
20 from functools import lru_cache
21 from typing import TYPE_CHECKING, FrozenSet, List, Set
22
23 import pytest
24
25 try:
26     from pytest import StashKey
27 except ImportError:
28     # pytest < 7
29     from _pytest.store import StoreKey as StashKey  # type: ignore[no-redef]
30
31 log = logging.getLogger(__name__)
32
33
34 if TYPE_CHECKING:
35     from _pytest.config import Config
36     from _pytest.config.argparsing import Parser
37     from _pytest.mark.structures import MarkDecorator
38     from _pytest.nodes import Node
39
40
41 ALL_POSSIBLE_OPTIONAL_MARKERS = StashKey[FrozenSet[str]]()
42 ENABLED_OPTIONAL_MARKERS = StashKey[FrozenSet[str]]()
43
44
45 def pytest_addoption(parser: "Parser") -> None:
46     group = parser.getgroup("collect")
47     group.addoption(
48         "--run-optional",
49         action="append",
50         dest="run_optional",
51         default=None,
52         help="Optional test markers to run; comma-separated",
53     )
54     parser.addini("optional-tests", "List of optional tests markers", "linelist")
55
56
57 def pytest_configure(config: "Config") -> None:
58     """Optional tests are markers.
59
60     Use the syntax in https://docs.pytest.org/en/stable/mark.html#registering-marks.
61     """
62     ot_ini = config.inicfg.get("optional-tests") or []
63     ot_markers = set()
64     ot_run: Set[str] = set()
65     if isinstance(ot_ini, str):
66         ot_ini = ot_ini.strip().split("\n")
67     marker_re = re.compile(r"^\s*(?P<no>no_)?(?P<marker>\w+)(:\s*(?P<description>.*))?")
68     for ot in ot_ini:
69         m = marker_re.match(ot)
70         if not m:
71             raise ValueError(f"{ot!r} doesn't match pytest marker syntax")
72
73         marker = (m.group("no") or "") + m.group("marker")
74         description = m.group("description")
75         config.addinivalue_line("markers", f"{marker}: {description}")
76         config.addinivalue_line(
77             "markers", f"{no(marker)}: run when `{marker}` not passed"
78         )
79         ot_markers.add(marker)
80
81     # collect requested optional tests
82     passed_args = config.getoption("run_optional")
83     if passed_args:
84         ot_run.update(itertools.chain.from_iterable(a.split(",") for a in passed_args))
85     ot_run |= {no(excluded) for excluded in ot_markers - ot_run}
86     ot_markers |= {no(m) for m in ot_markers}
87
88     log.info("optional tests to run:", ot_run)
89     unknown_tests = ot_run - ot_markers
90     if unknown_tests:
91         raise ValueError(f"Unknown optional tests wanted: {unknown_tests!r}")
92
93     store = config._store
94     store[ALL_POSSIBLE_OPTIONAL_MARKERS] = frozenset(ot_markers)
95     store[ENABLED_OPTIONAL_MARKERS] = frozenset(ot_run)
96
97
98 def pytest_collection_modifyitems(config: "Config", items: "List[Node]") -> None:
99     store = config._store
100     all_possible_optional_markers = store[ALL_POSSIBLE_OPTIONAL_MARKERS]
101     enabled_optional_markers = store[ENABLED_OPTIONAL_MARKERS]
102
103     for item in items:
104         all_markers_on_test = {m.name for m in item.iter_markers()}
105         optional_markers_on_test = all_markers_on_test & all_possible_optional_markers
106         if not optional_markers_on_test or (
107             optional_markers_on_test & enabled_optional_markers
108         ):
109             continue
110         log.info("skipping non-requested optional", item)
111         item.add_marker(skip_mark(frozenset(optional_markers_on_test)))
112
113
114 @lru_cache()
115 def skip_mark(tests: FrozenSet[str]) -> "MarkDecorator":
116     names = ", ".join(sorted(tests))
117     return pytest.mark.skip(reason=f"Marked with disabled optional tests ({names})")
118
119
120 @lru_cache()
121 def no(name: str) -> str:
122     if name.startswith("no_"):
123         return name[len("no_") :]
124     return "no_" + name