]> 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:

chore: dump docs deps and pre-commit hooks (#2676)
[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 from functools import lru_cache
18 import itertools
19 import logging
20 import re
21 from typing import FrozenSet, List, Set, TYPE_CHECKING
22
23 import pytest
24 from _pytest.store import StoreKey
25
26 log = logging.getLogger(__name__)
27
28
29 if TYPE_CHECKING:
30     from _pytest.config.argparsing import Parser
31     from _pytest.config import Config
32     from _pytest.mark.structures import MarkDecorator
33     from _pytest.nodes import Node
34
35
36 ALL_POSSIBLE_OPTIONAL_MARKERS = StoreKey[FrozenSet[str]]()
37 ENABLED_OPTIONAL_MARKERS = StoreKey[FrozenSet[str]]()
38
39
40 def pytest_addoption(parser: "Parser") -> None:
41     group = parser.getgroup("collect")
42     group.addoption(
43         "--run-optional",
44         action="append",
45         dest="run_optional",
46         default=None,
47         help="Optional test markers to run; comma-separated",
48     )
49     parser.addini("optional-tests", "List of optional tests markers", "linelist")
50
51
52 def pytest_configure(config: "Config") -> None:
53     """Optional tests are markers.
54
55     Use the syntax in https://docs.pytest.org/en/stable/mark.html#registering-marks.
56     """
57     ot_ini = config.inicfg.get("optional-tests") or []
58     ot_markers = set()
59     ot_run: Set[str] = set()
60     if isinstance(ot_ini, str):
61         ot_ini = ot_ini.strip().split("\n")
62     marker_re = re.compile(r"^\s*(?P<no>no_)?(?P<marker>\w+)(:\s*(?P<description>.*))?")
63     for ot in ot_ini:
64         m = marker_re.match(ot)
65         if not m:
66             raise ValueError(f"{ot!r} doesn't match pytest marker syntax")
67
68         marker = (m.group("no") or "") + m.group("marker")
69         description = m.group("description")
70         config.addinivalue_line("markers", f"{marker}: {description}")
71         config.addinivalue_line(
72             "markers", f"{no(marker)}: run when `{marker}` not passed"
73         )
74         ot_markers.add(marker)
75
76     # collect requested optional tests
77     passed_args = config.getoption("run_optional")
78     if passed_args:
79         ot_run.update(itertools.chain.from_iterable(a.split(",") for a in passed_args))
80     ot_run |= {no(excluded) for excluded in ot_markers - ot_run}
81     ot_markers |= {no(m) for m in ot_markers}
82
83     log.info("optional tests to run:", ot_run)
84     unknown_tests = ot_run - ot_markers
85     if unknown_tests:
86         raise ValueError(f"Unknown optional tests wanted: {unknown_tests!r}")
87
88     store = config._store
89     store[ALL_POSSIBLE_OPTIONAL_MARKERS] = frozenset(ot_markers)
90     store[ENABLED_OPTIONAL_MARKERS] = frozenset(ot_run)
91
92
93 def pytest_collection_modifyitems(config: "Config", items: "List[Node]") -> None:
94     store = config._store
95     all_possible_optional_markers = store[ALL_POSSIBLE_OPTIONAL_MARKERS]
96     enabled_optional_markers = store[ENABLED_OPTIONAL_MARKERS]
97
98     for item in items:
99         all_markers_on_test = {m.name for m in item.iter_markers()}
100         optional_markers_on_test = all_markers_on_test & all_possible_optional_markers
101         if not optional_markers_on_test or (
102             optional_markers_on_test & enabled_optional_markers
103         ):
104             continue
105         log.info("skipping non-requested optional", item)
106         item.add_marker(skip_mark(frozenset(optional_markers_on_test)))
107
108
109 @lru_cache()
110 def skip_mark(tests: FrozenSet[str]) -> "MarkDecorator":
111     names = ", ".join(sorted(tests))
112     return pytest.mark.skip(reason=f"Marked with disabled optional tests ({names})")
113
114
115 @lru_cache()
116 def no(name: str) -> str:
117     if name.startswith("no_"):
118         return name[len("no_") :]
119     return "no_" + name