-
Notifications
You must be signed in to change notification settings - Fork 196
Expand file tree
/
Copy pathconftest.py
More file actions
123 lines (93 loc) · 4.34 KB
/
Copy pathconftest.py
File metadata and controls
123 lines (93 loc) · 4.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"""Sybil-powered doctest for README.md.
Validates that Python code blocks in the project README remain importable
and syntactically valid after refactors.
This conftest lives at the project root so Sybil can discover README.md
relative to its path. It is picked up because pyproject.toml includes
"README.md" in testpaths.
"""
from collections.abc import Iterable
import pytest
from sybil import Document, Region, Sybil
from sybil.parsers.abstract import AbstractSkipParser
from sybil.parsers.markdown.clear import ClearNamespaceParser
from sybil.parsers.markdown.codeblock import PythonCodeBlockParser
from sybil.parsers.markdown.lexers import DirectiveInHTMLCommentLexer
from sybil.parsers.markdown.skip import SkipParser
from tests.offline import is_offline
def _live_rpc_blocks_enabled() -> bool:
"""Report whether the README's live-RPC examples should execute.
They run in local dev (RPC + anvil reachable) and are skipped in the constrained
CI/CD runner that has neither. Offline mode is the CI default, shared with the
test-suite fork fixtures via :func:`tests._offline.is_offline`.
Returns:
True when live-RPC blocks should run (online); False to skip them (offline).
"""
return not is_offline()
class _LiveRpcSkipParser(AbstractSkipParser):
"""Skip parser for the README's live-RPC code blocks.
Recognises ``<!-- live-rpc: start -->`` / ``<!-- live-rpc: end -->`` directives.
When live-RPC blocks are enabled (local dev) these directives are inert and the
enclosed code runs; when disabled (offline CI) they raise ``pytest.skip`` so the
doctest stays green without network access.
This is deliberately independent of the standard :class:`SkipParser` (``skip:``)
and of the document namespace — the flag lives in this process, so the
``clear-namespace`` directive mid-document cannot erase it. Pre-existing
unconditional ``skip:`` directives keep their always-on behaviour.
"""
directive = "live-rpc"
def __init__(self, *, enabled: bool) -> None:
self._enabled = enabled
super().__init__([DirectiveInHTMLCommentLexer(self.directive)])
def __call__(self, document: Document) -> Iterable[Region]:
if self._enabled:
# Online: don't emit the skip regions at all, so the blocks execute.
return ()
return super().__call__(document)
pytest_collect_file = Sybil(
parsers=[
PythonCodeBlockParser(),
SkipParser(),
_LiveRpcSkipParser(enabled=_live_rpc_blocks_enabled()),
ClearNamespaceParser(),
],
path=".",
patterns=["README.md"],
excludes=["*/README.md"],
).pytest()
_EXPECTED_LINE_PARTS = 2
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
"""Restore document order for README.md sybil items and pin them to one xdist worker.
Sybil's skip directive state is tracked per-document and depends on sequential, ordered
evaluation. Two pytest plugins break this:
- pytest-randomly reorders items, breaking the skip state machine.
- pytest-xdist distributes items across workers, so each worker sees
an unbalanced subset of skip:start/skip:end pairs.
This hook runs before pytest-randomly's shuffle (``tryfirst=True``) and uses
``pytest.mark.order(N)`` to pin each README item to its document position. pytest-order
(``trylast=True`` by default) then re-orders these items back into sequence after the random
shuffle.
It also marks them with a shared ``xdist_group`` so ``--dist=loadgroup`` sends them to a single
worker.
"""
readme_items = []
other_items = []
for item in items:
if "README.md" in item.nodeid:
readme_items.append(item)
else:
other_items.append(item)
# Sort README items by line number to restore document order
def readme_sort_key(item: pytest.Item) -> int:
parts = item.nodeid.split("line:")
if len(parts) == _EXPECTED_LINE_PARTS:
try:
return int(parts[1].split(",")[0])
except ValueError:
return 0
return 0
readme_items.sort(key=readme_sort_key)
for index, item in enumerate(readme_items):
item.add_marker(pytest.mark.order(index))
item.add_marker(pytest.mark.xdist_group("readme_sybil"))
items[:] = readme_items + other_items