Skip to content

Commit a9df44c

Browse files
authored
test: Verify each driver is frozen in the upstream firmware manifest. (#393)
* test: Verify each driver is frozen in the upstream firmware manifest. Adds a parametrized pytest test that generates one case per `lib/*/` and asserts the driver name is declared in the STEAM32_WB55RG manifest hosted in steamicc/micropython-steami. Catches regressions like the accidental removal of mcp23009e and the forgotten steami_screen. The upstream manifest is fetched at test time (no dependency on a local clone), with an env override for forks. A `lib/<driver>/.not-frozen` marker opts a driver out with a one-line reason — used here for gc9a01 (pending freeze, #368) and im34dt05 (not yet integrated). Network failures skip cleanly so offline dev is unaffected. Closes #392. * test: Address Copilot review on frozen-manifest check. Three issues raised on #393: 1. URLError catches HTTPError too, so a 404 (branch/path renamed, path moved) would silently skip the test and permanently disable the protection. Handle HTTPError separately and fail loudly with a message pointing to MANIFEST_URL. 2. The regex-based extraction was formatting-sensitive: single quotes, extra keyword arguments, or a different argument order would produce false failures. Replace with an ast-based walker that inspects require() calls and matches on the library= keyword — resilient to any legal Python formatting. 3. CONTRIBUTING.md contradicted itself ("empty file containing a one-line reason"). Reword per Copilot's suggestion: empty by default, optionally with a one-line reason.
1 parent 92f0982 commit a9df44c

4 files changed

Lines changed: 101 additions & 0 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ lib/<component>/
2424
* The directory name must match the driver name (e.g. `mcp23009e`, `wsen-hids`)
2525
* The main class must be exposed in `__init__.py`
2626
* Drivers must be self-contained (no cross-driver dependencies)
27+
* Every driver is automatically checked against the upstream firmware manifest by `tests/test_frozen_manifest.py`. If a driver is intentionally **not** frozen (experimental, not yet integrated, etc.), add a `lib/<driver>/.not-frozen` marker file (it may be empty; optionally include a one-line reason) — the test will skip it and display the reason if present.
2728

2829
## Coding conventions
2930

lib/gc9a01/.not-frozen

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Pending freeze, see #368.

lib/im34dt05/.not-frozen

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Not yet integrated into the firmware manifest.

tests/test_frozen_manifest.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Verify each driver under lib/ is declared in the upstream frozen manifest.
2+
3+
Catches silent regressions where a driver is accidentally removed from, or
4+
forgotten in, the STEAM32_WB55RG board manifest in `steamicc/micropython-steami`.
5+
"""
6+
7+
import ast
8+
import os
9+
from pathlib import Path
10+
from urllib.error import HTTPError, URLError
11+
from urllib.request import urlopen
12+
13+
import pytest
14+
15+
LIB_DIR = Path(__file__).parent.parent / "lib"
16+
17+
# Keep this branch in sync with MICROPYTHON_BRANCH in env.mk.
18+
MANIFEST_URL = os.environ.get(
19+
"STEAMI_FIRMWARE_MANIFEST_URL",
20+
"https://raw.githubusercontent.com/steamicc/micropython-steami/"
21+
"stm32-steami-rev1d-final/ports/stm32/boards/STEAM32_WB55RG/manifest.py",
22+
)
23+
24+
STEAMI_LIBRARY = "micropython-steami-lib"
25+
26+
27+
def _extract_required_drivers(source):
28+
"""Parse a board manifest and return the set of driver names required
29+
from `micropython-steami-lib`. Uses the AST so the check is resilient to
30+
quoting, spacing, and extra keyword arguments."""
31+
tree = ast.parse(source)
32+
required = set()
33+
for node in ast.walk(tree):
34+
if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Name)):
35+
continue
36+
if node.func.id != "require":
37+
continue
38+
library = None
39+
for kw in node.keywords:
40+
if kw.arg == "library" and isinstance(kw.value, ast.Constant):
41+
library = kw.value.value
42+
if library != STEAMI_LIBRARY:
43+
continue
44+
if node.args and isinstance(node.args[0], ast.Constant):
45+
name = node.args[0].value
46+
if isinstance(name, str):
47+
required.add(name)
48+
return required
49+
50+
51+
@pytest.fixture(scope="session")
52+
def frozen_drivers():
53+
"""Fetch the upstream manifest once per session and return the set of
54+
driver names required from micropython-steami-lib."""
55+
try:
56+
with urlopen(MANIFEST_URL, timeout=10) as resp:
57+
content = resp.read().decode("utf-8")
58+
except HTTPError as exc:
59+
pytest.fail(
60+
f"unexpected HTTP {exc.code} while fetching {MANIFEST_URL}: "
61+
f"{exc.reason}. The branch or path may have moved — update "
62+
f"MANIFEST_URL in tests/test_frozen_manifest.py."
63+
)
64+
except (URLError, TimeoutError, OSError) as exc:
65+
pytest.skip(f"cannot fetch upstream manifest: {exc}")
66+
return _extract_required_drivers(content)
67+
68+
69+
def _discover_driver_dirs():
70+
return sorted(d for d in LIB_DIR.iterdir() if d.is_dir())
71+
72+
73+
_driver_dirs = _discover_driver_dirs()
74+
75+
76+
@pytest.mark.parametrize(
77+
"driver_dir",
78+
_driver_dirs,
79+
ids=[d.name for d in _driver_dirs],
80+
)
81+
def test_driver_is_frozen_in_firmware_mock(driver_dir, frozen_drivers):
82+
"""Each driver under lib/ must be required in the upstream firmware manifest.
83+
84+
To intentionally ship a driver outside the firmware, add an empty
85+
`lib/<driver>/.not-frozen` marker (optionally containing a one-line reason).
86+
"""
87+
not_frozen = driver_dir / ".not-frozen"
88+
if not_frozen.exists():
89+
reason = not_frozen.read_text(encoding="utf-8").strip() or "marked .not-frozen"
90+
pytest.skip(f"{driver_dir.name}: {reason}")
91+
92+
assert driver_dir.name in frozen_drivers, (
93+
f"{driver_dir.name} is not required in the frozen manifest "
94+
f"({MANIFEST_URL}). Add "
95+
f'require("{driver_dir.name}", library="micropython-steami-lib") '
96+
f"to the upstream manifest, or add a lib/{driver_dir.name}/.not-frozen "
97+
f"marker if the driver is intentionally not shipped."
98+
)

0 commit comments

Comments
 (0)