In a channel, a DM, or a room, ^K toggles the lock of the contact that wrote the selected message, so the archive sweep never takes it. The hint says ^K lock or ^K unlock, but only where it fits without dropping another atom, and the status line confirms what the key did. On the handhelds, Lock (or Unlock) rides Shift+F2 in a channel and a DM. A room keeps Log in there, so the Author page that a conversation opens now carries the lock row, and no other management verb. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
227 lines
9.5 KiB
Python
227 lines
9.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, *, store: object = None, self_key: str = "") -> 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). With a
|
|
contact ``store`` and the key of the device, ^K can lock the contacts too.
|
|
"""
|
|
from types import SimpleNamespace
|
|
|
|
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)
|
|
|
|
ctx = None if store is None else SimpleNamespace(contact_store=store)
|
|
pages = _Recording(ctx, contacts, self_key=self_key) # 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 _empty_registries(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Each test starts with no registered secret and no registered host.
|
|
|
|
The two registries are process-wide on purpose: a secret that MeshTerm read once must
|
|
stay hidden for the rest of the run. In a suite, that means that a password one test
|
|
used ("hello", the default room password) hides the same word in each later test that
|
|
looks for it. ``monkeypatch`` puts the shared objects back after the test.
|
|
"""
|
|
from meshterm.core import anonymize, secrets
|
|
|
|
monkeypatch.setattr(secrets, "_known", set())
|
|
monkeypatch.setattr(secrets, "_known_pattern", None)
|
|
monkeypatch.setattr(anonymize, "_hosts", {})
|
|
monkeypatch.setattr(anonymize, "_host_pattern", None)
|
|
|
|
|
|
@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)
|