Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ lib/<component>/
* The directory name must match the driver name (e.g. `mcp23009e`, `wsen-hids`)
* The main class must be exposed in `__init__.py`
* Drivers must be self-contained (no cross-driver dependencies)
* 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.

## Coding conventions

Expand Down
1 change: 1 addition & 0 deletions lib/gc9a01/.not-frozen
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pending freeze, see #368.
1 change: 1 addition & 0 deletions lib/im34dt05/.not-frozen
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not yet integrated into the firmware manifest.
98 changes: 98 additions & 0 deletions tests/test_frozen_manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Verify each driver under lib/ is declared in the upstream frozen manifest.

Catches silent regressions where a driver is accidentally removed from, or
forgotten in, the STEAM32_WB55RG board manifest in `steamicc/micropython-steami`.
"""

import ast
import os
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import urlopen

import pytest

LIB_DIR = Path(__file__).parent.parent / "lib"

# Keep this branch in sync with MICROPYTHON_BRANCH in env.mk.
MANIFEST_URL = os.environ.get(
"STEAMI_FIRMWARE_MANIFEST_URL",
"https://raw.githubusercontent.com/steamicc/micropython-steami/"
"stm32-steami-rev1d-final/ports/stm32/boards/STEAM32_WB55RG/manifest.py",
)

STEAMI_LIBRARY = "micropython-steami-lib"


def _extract_required_drivers(source):
"""Parse a board manifest and return the set of driver names required
from `micropython-steami-lib`. Uses the AST so the check is resilient to
quoting, spacing, and extra keyword arguments."""
tree = ast.parse(source)
required = set()
for node in ast.walk(tree):
if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Name)):
continue
if node.func.id != "require":
continue
library = None
for kw in node.keywords:
if kw.arg == "library" and isinstance(kw.value, ast.Constant):
library = kw.value.value
if library != STEAMI_LIBRARY:
continue
if node.args and isinstance(node.args[0], ast.Constant):
name = node.args[0].value
if isinstance(name, str):
required.add(name)
return required


@pytest.fixture(scope="session")
def frozen_drivers():
"""Fetch the upstream manifest once per session and return the set of
driver names required from micropython-steami-lib."""
try:
with urlopen(MANIFEST_URL, timeout=10) as resp:
content = resp.read().decode("utf-8")
except HTTPError as exc:
pytest.fail(
f"unexpected HTTP {exc.code} while fetching {MANIFEST_URL}: "
f"{exc.reason}. The branch or path may have moved — update "
f"MANIFEST_URL in tests/test_frozen_manifest.py."
)
except (URLError, TimeoutError, OSError) as exc:
pytest.skip(f"cannot fetch upstream manifest: {exc}")
Comment on lines +64 to +65
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception handling treats all URLError as an offline/network condition and skips the entire check. urllib.error.HTTPError is a URLError, so a 404/403 (e.g., upstream path moved, branch renamed, rate-limited) will silently skip and permanently disable this protection in CI. Consider handling HTTPError separately (fail with a clear message for non-2xx responses, or only skip on genuine connectivity/timeouts).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 34281f7: HTTPError is caught separately and fails the test loudly with a pointer to MANIFEST_URL, so a renamed branch or moved path cannot silently disable the check. Only URLError / TimeoutError / OSError still skip (genuine offline cases).

return _extract_required_drivers(content)


def _discover_driver_dirs():
return sorted(d for d in LIB_DIR.iterdir() if d.is_dir())


_driver_dirs = _discover_driver_dirs()


@pytest.mark.parametrize(
"driver_dir",
_driver_dirs,
ids=[d.name for d in _driver_dirs],
)
def test_driver_is_frozen_in_firmware_mock(driver_dir, frozen_drivers):
"""Each driver under lib/ must be required in the upstream firmware manifest.

To intentionally ship a driver outside the firmware, add an empty
`lib/<driver>/.not-frozen` marker (optionally containing a one-line reason).
"""
not_frozen = driver_dir / ".not-frozen"
if not_frozen.exists():
reason = not_frozen.read_text(encoding="utf-8").strip() or "marked .not-frozen"
pytest.skip(f"{driver_dir.name}: {reason}")

assert driver_dir.name in frozen_drivers, (
f"{driver_dir.name} is not required in the frozen manifest "
f"({MANIFEST_URL}). Add "
f'require("{driver_dir.name}", library="micropython-steami-lib") '
f"to the upstream manifest, or add a lib/{driver_dir.name}/.not-frozen "
f"marker if the driver is intentionally not shipped."
)
Loading