Files
MeshTerm/tests/test_platforms.py
T
Jean-Pierre MartineauandClaude Opus 5.5 b6e7cd5c16 The handhelds are named for their variant: picocalc-lyra and cardputer-zero
M5Stack makes three Cardputers and only the Zero runs Linux; the PicoCalc
takes several cores and only the Luckfox Lyra runs MeshTerm. So the bare
names were ambiguous, and every reference to the two devices now carries
the variant: the --platform values and MESHTERM_PLATFORM, the Platform
constants (PICOCALC_LYRA, CARDPUTER_ZERO), their lane decks and every
screen's picocalc_lyra_lane / cardputer_zero_lane, the font inventories,
the lane chips' theme styles, the scripts/ folders, the two chrome test
files, and the docs and CLAUDE.md that name them.

There is no alias: --platform picocalc is refused with the three real
choices listed, rather than falling back to the desktop layout. The
PicoCalc is auto-detected from its device tree, so it needs nothing.
What names hardware or a user's data keeps its name: the PicoCalc
keyboard's input-device name, its battery's sysfs supply, and the serial
profile the Lyra setup script writes. .gitignore follows the firmware's
folder, and the changelog keeps the words each release used.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 11:52:41 -04:00

216 lines
8.4 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Tests for the platform seam.
Resolution order, the active-platform singleton, and the two platform specs' own
sanity — PicoCalc being strictly the narrower, plainer flavour.
"""
from __future__ import annotations
import dataclasses
from pathlib import Path
import pytest
import meshterm.platforms as platforms_mod
from meshterm.platforms import (
PICOCALC_LYRA,
REGULAR,
get_platform,
resolve,
set_platform,
without_emoji,
)
def test_regular_is_todays_behaviour() -> None:
"""REGULAR is the desktop terminal exactly as it behaves without this seam."""
assert REGULAR.name == "regular"
assert REGULAR.width_reclaim is True
assert REGULAR.emoji is True
assert REGULAR.truecolor is True
assert REGULAR.frame_border is True
assert REGULAR.readable_cols == 72
def test_picocalc_is_the_narrower_plainer_flavour() -> None:
"""PICOCALC_LYRA never claims a capability the console can't back up."""
assert PICOCALC_LYRA.name == "picocalc-lyra"
assert PICOCALC_LYRA.readable_cols < REGULAR.readable_cols
assert PICOCALC_LYRA.width_reclaim is False # a phantom column would tear the exact-width frame
assert PICOCALC_LYRA.emoji is False
assert PICOCALC_LYRA.truecolor is False
def test_get_platform_defaults_to_regular() -> None:
"""Every test starts on REGULAR (see the autouse fixture in conftest.py)."""
assert get_platform() is REGULAR
def test_set_platform_is_visible_through_get_platform() -> None:
"""set_platform takes effect immediately for any caller reading via get_platform()."""
set_platform(PICOCALC_LYRA)
assert get_platform() is PICOCALC_LYRA
set_platform(REGULAR)
assert get_platform() is REGULAR
def test_set_platform_reaches_a_dotted_import_too() -> None:
"""A consumer that imports the module (not the name) sees the change immediately.
This is the exact footgun the module docstring warns about: ``from meshterm.platforms
import PLATFORM`` at a module's top level would NOT see this, but module-qualified
access (``platforms_mod.PLATFORM``) — and every real binding point in this codebase,
which reads through :func:`get_platform` — does.
"""
set_platform(PICOCALC_LYRA)
assert platforms_mod.PLATFORM is PICOCALC_LYRA
def test_resolve_defaults_to_regular_off_device(monkeypatch: pytest.MonkeyPatch) -> None:
"""No flag, no env, no device tree (every desktop/CI machine) → REGULAR, source 'default'."""
monkeypatch.delenv("MESHTERM_PLATFORM", raising=False)
monkeypatch.setattr(platforms_mod, "_DEVICE_TREE_MODEL_PATH", Path("/nonexistent/model"))
r = resolve()
assert r.platform is REGULAR
assert r.source == "default"
assert r.flag is None and r.env is None and r.detected_model is None
def test_resolve_flag_wins_over_everything(monkeypatch: pytest.MonkeyPatch) -> None:
"""An explicit --platform overrides both the env var and auto-detection."""
monkeypatch.setenv("MESHTERM_PLATFORM", "picocalc-lyra")
monkeypatch.setattr(platforms_mod, "_DEVICE_TREE_MODEL_PATH", Path("/nonexistent/model"))
r = resolve("regular")
assert r.platform is REGULAR
assert r.source == "--platform flag"
assert r.env == "picocalc-lyra" # recorded even though it didn't decide the outcome
def test_resolve_env_wins_when_no_flag(monkeypatch: pytest.MonkeyPatch) -> None:
"""MESHTERM_PLATFORM decides when --platform wasn't passed."""
monkeypatch.setenv("MESHTERM_PLATFORM", "picocalc-lyra")
monkeypatch.setattr(platforms_mod, "_DEVICE_TREE_MODEL_PATH", Path("/nonexistent/model"))
r = resolve(None)
assert r.platform is PICOCALC_LYRA
assert r.source == "MESHTERM_PLATFORM env var"
def test_resolve_matching_is_case_insensitive_and_trims_space() -> None:
"""A flag/env value matches regardless of case or stray whitespace."""
assert resolve(" PicoCalc-Lyra ").platform is PICOCALC_LYRA
assert resolve("REGULAR").platform is REGULAR
@pytest.mark.parametrize("old", ["picocalc", "cardputer"])
def test_the_old_device_names_are_refused_with_the_new_ones_listed(old: str) -> None:
"""``picocalc`` and ``cardputer`` were renamed for the device variant, with no alias.
M5Stack makes three Cardputers and the PicoCalc takes several cores, so the bare names
were ambiguous. A launcher still passing one fails loudly, naming the real choices,
rather than quietly falling back to the desktop layout.
"""
with pytest.raises(ValueError) as exc:
resolve(old)
assert "picocalc-lyra" in str(exc.value) and "cardputer-zero" in str(exc.value)
def test_resolve_unknown_flag_raises_with_the_choices(monkeypatch: pytest.MonkeyPatch) -> None:
"""A typo'd --platform is a clear ValueError, not a silent fallback to REGULAR."""
monkeypatch.delenv("MESHTERM_PLATFORM", raising=False)
with pytest.raises(ValueError) as exc:
resolve("laptop")
message = str(exc.value)
assert "laptop" in message and "--platform" in message
assert "picocalc-lyra" in message and "regular" in message
def test_resolve_unknown_env_raises_only_when_it_would_decide(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A bad env var is only an error when nothing else (the flag) overrides it."""
monkeypatch.setenv("MESHTERM_PLATFORM", "laptop")
# A valid flag wins outright — the bad env var is recorded but never validated.
assert resolve("regular").platform is REGULAR
with pytest.raises(ValueError) as exc:
resolve(None)
assert "MESHTERM_PLATFORM" in str(exc.value)
def test_resolve_auto_detects_the_lyra_model(monkeypatch: pytest.MonkeyPatch) -> None:
"""The exact on-device string (P0, 2026-08-01) — trailing space and NUL included."""
monkeypatch.delenv("MESHTERM_PLATFORM", raising=False)
class _FakePath:
def read_bytes(self) -> bytes:
return b"Luckfox Lyra \x00"
monkeypatch.setattr(platforms_mod, "_DEVICE_TREE_MODEL_PATH", _FakePath())
r = resolve()
assert r.platform is PICOCALC_LYRA
assert r.source == "auto-detect"
assert r.detected_model == "Luckfox Lyra" # trailing space/NUL stripped
def test_resolve_auto_detect_is_conservative(monkeypatch: pytest.MonkeyPatch) -> None:
"""A device tree that exists but names different hardware never guesses picocalc-lyra."""
monkeypatch.delenv("MESHTERM_PLATFORM", raising=False)
class _FakePath:
def read_bytes(self) -> bytes:
return b"Raspberry Pi 4 Model B\x00"
monkeypatch.setattr(platforms_mod, "_DEVICE_TREE_MODEL_PATH", _FakePath())
r = resolve()
assert r.platform is REGULAR
assert r.detected_model == "Raspberry Pi 4 Model B"
# -- the emoji-less variant ------------------------------------------------------------
def test_without_emoji_moves_one_flag_and_nothing_else() -> None:
"""A terminal that can't draw emoji is not a different flavour.
The classic Windows console has the desktop's width, colour depth, borders and
keyboard — only the icons have to change. Every other field must survive, and
``font`` above all: folding to a handheld's font there would strip accents from names
the console renders perfectly well.
"""
plain = without_emoji(REGULAR)
assert plain.emoji is False
assert plain.font == ""
assert plain.truecolor is True
assert plain.readable_cols == REGULAR.readable_cols
assert plain.footer_fkeys is REGULAR.footer_fkeys
changed = {
f.name
for f in dataclasses.fields(REGULAR)
if getattr(plain, f.name) != getattr(REGULAR, f.name)
}
assert changed == {"emoji"}
def test_without_emoji_leaves_an_already_compact_platform_alone() -> None:
"""A platform whose icons are already compact comes back untouched.
The same object, not a copy that would compare equal but break every ``is`` check
the suite makes against PICOCALC_LYRA.
"""
assert without_emoji(PICOCALC_LYRA) is PICOCALC_LYRA
def test_the_emoji_less_variant_still_drives_the_theme() -> None:
"""The derived platform is a real one: binding it routes icons through the table.
This is the whole point — the seam's flags are independent, so clearing one reaches
the glyph funnel without dragging the console font's fold along with it.
"""
from meshterm.ui.theme import fold_text, glyph
set_platform(without_emoji(REGULAR))
assert glyph("📡") == "☼" # the advert icon, compacted
assert fold_text("café Montréal") == "café Montréal" # no fold
assert get_platform().readable_cols == 72