Files
MeshTerm/tests/conftest.py
T
Jean-Pierre MartineauandClaude Opus 5.5 2d2128718a ^O opens the author's node page, and ^G logs in to a room
In a channel, a DM, or a room, ^O on a selected message opens the node page
of its author, the same page the Contacts list opens: our own for a message
of ours, the contact the author's key finds otherwise (ContactPages, the twin
of the map's Locator). It opens to look, without the management verbs, which
belong to the list a contact is in. The hint names ^O only where it acts; the
handheld chip is Author, on Shift+F3 behind Map, since F1-F3 are full.

The room's login moves from ^O to ^G.

The Unreleased changelog now also says where the Map chip went, and covers
the shared lane and the route-end chips.

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

206 lines
8.5 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Shared fixtures and helpers: plain-text screen reading, fixed path rendering, project files.
:func:`plain` is the only way that a test reads a rendered screen. Each screen test must
first remove the ANSI colour escapes and join the rendered lines. Only then can it make
assertions about the content. If each file did this again, the copies would differ over
time.
The powerline verdict depends on the environment. The suite can run inside VS Code or
Windows Terminal (both can draw chips) or in a bare CI shell (which cannot). The screen
assertions must not change with the terminal of the developer. ``MESHTERM_POWERLINE=0``
is the supported way to fix the verdict. The suite clears the cached verdict around each
test, so that the order of the tests has no effect. The tests that are specific to
powerline pass explicit modes, or they patch the widget's own switch with
``monkeypatch``.
``NO_COLOR`` depends on the environment in the same way. Rich obeys it: it removes colour
and keeps attributes. This silently makes each colour assertion in the suite useless (a
shell that an agent harness runs sets it). The autouse fixture below deletes it. Thus the
tests always see the colours that the app really sends.
:func:`not_ignored` keeps a test that walks a folder to the files of the project. Git
ignores the other files in the checkout, and these files are not ours to examine.
"""
from __future__ import annotations
import os
import re
import subprocess
from collections.abc import Callable, Iterable, Iterator
from pathlib import Path
import pytest
from meshterm.platforms import REGULAR, set_platform
from meshterm.ui.termfont import powerline_support
#: ANSI SGR escapes: the colour runs that a rendered line has between its characters.
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
def plain(rendered: str | Iterable[str]) -> str:
"""The plain-text form of rendered screen output, for content assertions.
The function takes the result of a render: a list of ANSI lines (which it joins with
newlines), or one string that is already joined. It removes the colour escapes. Thus
the assertions read the screen in the same way as a viewer.
"""
if not isinstance(rendered, str):
rendered = "\n".join(rendered)
return _ANSI.sub("", rendered)
def recording_locator(markers: list) -> object:
"""A real :class:`~meshterm.ui.map_screen.Locator` whose map is a list, ``shown``.
It finds markers with the rule of the app. Instead of a map, :meth:`show` appends the
marker that the map would open on to ``shown``.
"""
from meshterm.ui.map_screen import Locator
class _Recording(Locator):
async def show(self, marker) -> None: # noqa: ANN001 - a MapMarker
self.shown.append(marker)
locator = _Recording(None, markers) # type: ignore[arg-type]
locator.shown = [] # type: ignore[attr-defined]
return locator
def recording_pages(contacts: list) -> object:
"""A real :class:`~meshterm.ui.node_detail_screen.ContactPages` whose pages are a list.
It finds contacts with the rule of the app. Instead of a node page, :meth:`show` appends
the contact that the page would open on to ``shown`` (``None`` is our node).
"""
from meshterm.ui.node_detail_screen import ContactPages
class _Recording(ContactPages):
async def show(self, contact) -> None: # noqa: ANN001 - a Contact, or None for us
self.shown.append(contact)
pages = _Recording(None, contacts) # type: ignore[arg-type]
pages.shown = [] # type: ignore[attr-defined]
return pages
#: The root of the checkout.
_ROOT = Path(__file__).resolve().parent.parent
def not_ignored(paths: Iterable[Path]) -> list[Path]:
"""The paths in ``paths`` that git does not ignore, in their order.
A test that walks a folder of the checkout must examine only the files of the project.
A folder can also hold files that git ignores. An example is the MeshCore checkout that
``build-firmware.sh`` makes in ``scripts/``, with its third-party code and its build
tree. Git never ignores a tracked file. When git is not available (for example, in an
unpacked source archive), the function returns all the paths, and the test examines
each file, as it did before.
"""
paths = list(paths)
relative = [path.resolve().relative_to(_ROOT).as_posix() for path in paths]
try:
result = subprocess.run(
["git", "check-ignore", "--stdin", "-z"],
cwd=_ROOT,
input="\0".join(relative),
capture_output=True,
encoding="utf-8",
check=False,
)
except OSError:
return paths
if result.returncode not in (0, 1): # 1 is "nothing ignored", 128 is "not a repository"
return paths
ignored = set(result.stdout.split("\0"))
return [path for path, name in zip(paths, relative, strict=True) if name not in ignored]
@pytest.fixture(autouse=True)
def _plain_path_rendering(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
monkeypatch.setenv("MESHTERM_POWERLINE", "0")
# An inherited NO_COLOR makes Rich remove colour (and keep attributes). Then each
# colour assertion silently reads bare text. Refer to the module docstring.
monkeypatch.delenv("NO_COLOR", raising=False)
powerline_support.cache_clear()
yield
powerline_support.cache_clear()
@pytest.fixture
def powerline(monkeypatch: pytest.MonkeyPatch) -> Callable[[bool], None]:
"""Fix the powerline verdict for a test that is about chips.
The suite runs with ``MESHTERM_POWERLINE=0``. Thus the screen assertions do not change
with the terminal of the developer (refer to the module docstring). A test of the chip
language needs the other answer. The test asks for it through the supported override,
and does not go into the widget. ``monkeypatch`` restores the environment, and the
autouse fixture above clears the cached verdict before and after. Thus no state stays.
"""
def _set(on: bool) -> None:
monkeypatch.setenv("MESHTERM_POWERLINE", "1" if on else "0")
powerline_support.cache_clear()
return _set
@pytest.fixture(autouse=True)
def _reset_transmit_gate() -> Iterator[None]:
"""Each test starts with nothing transmitted.
The transmit clock is for the whole process (refer to :mod:`meshterm.core.transmit_gate`).
This is correct for a session and wrong for a suite. If a test sent anything, it would
leave a cooldown that is still active for the next test. Then the flow of the next test
would stop on a countdown that nobody wrote in the script.
"""
from meshterm.core.transmit_gate import current as transmit_clock
transmit_clock().reset()
yield
transmit_clock().reset()
@pytest.fixture(autouse=True)
def _isolate_config_dir(tmp_path_factory: pytest.TempPathFactory) -> Iterator[None]:
"""No test can reach the developer's own ``~/.meshterm``.
``$MESHTERM_HOME`` moves the whole directory: the history, the caches of contacts and
channels, the outbox, the stored admin passwords, the device profiles, the preferences,
and the log. ``--db`` moves only the first of these. Thus a CLI test that used only
``--db`` was not isolated. A synthetic contact that a probe wrote stayed in the real
cache. This made ``test_cli_contract`` fail on one machine and pass on each other
machine. Also, each run added lines to the real ``meshterm.log`` of the developer, and
read the real ``log_level``.
The fixture is autouse and unconditional, because the risk is not only in the CLI.
Any code that uses ``Settings.load()``, a preference, a store, or the logger has it.
A test that needs a directory with data sets the variable itself.
``monkeypatch.setenv`` in a test still wins over this fixture.
"""
home = tmp_path_factory.mktemp("meshterm-home")
previous = os.environ.get("MESHTERM_HOME")
os.environ["MESHTERM_HOME"] = str(home)
yield
if previous is None:
os.environ.pop("MESHTERM_HOME", None)
else:
os.environ["MESHTERM_HOME"] = previous
@pytest.fixture(autouse=True)
def _reset_platform() -> Iterator[None]:
"""Each test starts and ends on :data:`~meshterm.platforms.REGULAR`.
A test can call ``set_platform(PICOCALC_LYRA)`` (directly, or through the gallery
harness). That choice cannot stay for the next test. Regular is the baseline of the
suite. It is also the default for a process that does not get ``--platform`` or
``MESHTERM_PLATFORM`` (refer to :mod:`meshterm.platforms`).
"""
set_platform(REGULAR)
yield
set_platform(REGULAR)