Files
MeshTerm/tests/test_macterminal.py
T
Jean-Pierre MartineauandClaude Opus 5.5 efc02ddced The code text uses the glossary words in more places
A final pass over the docstrings and comments: "the own X of Y" is now
"Y's own X" (84 places), a navigation entry is a screen on the stack, our
node is our node, a paint is a paint, the quoted UX labels are in code
font, and a list of punctuation glyphs is in code font.

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

266 lines
10 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""The macOS Terminal profile: what it contains, and what it refuses to change.
The archive shapes in these tests are not invented. They were read from a profile that was
installed on a real Mac, and the profile was confirmed to render. Thus the tests fix the
encoder to bytes that are known to work. They do not fix it to an interpretation of the
format of Apple.
"""
from __future__ import annotations
import plistlib
import sys
from pathlib import Path
import pytest
from meshterm.core import macterminal
from meshterm.core.relaunch import REOPENED_ENV
from meshterm.ui.theme import _VT_SLOTS
def test_color_archive_is_the_shape_appkit_writes() -> None:
"""The colour archive has the shape that AppKit writes.
A colour decodes to a calibrated-RGB ``NSColor``, with six places and a NUL at the end.
"""
decoded = plistlib.loads(macterminal.ns_color("#aa0000"))
assert decoded["$archiver"] == "NSKeyedArchiver"
assert decoded["$version"] == 100000
colour = decoded["$objects"][1]
assert colour["NSColorSpace"] == 1
assert colour["NSRGB"] == b"0.666667 0.000000 0.000000\x00"
assert decoded["$objects"][2]["$classname"] == "NSColor"
def test_font_archive_names_the_bundled_face() -> None:
"""The font archive names the bundled face.
It has the PostScript name and not the family name.
"""
decoded = plistlib.loads(macterminal.ns_font())
assert macterminal.FONT_FACE in decoded["$objects"]
assert decoded["$objects"][1]["NSSize"] == macterminal.FONT_SIZE
assert decoded["$objects"][3]["$classname"] == "NSFont"
def test_every_palette_slot_comes_from_the_theme() -> None:
"""Each palette slot comes from the theme.
The window cannot differ from the screen that it holds. The profile has sixteen keys,
in the order that Terminal needs (dim, then bright). Each key has its own hex value in
the theme. A change to a slot in :data:`~meshterm.ui.theme._VT_SLOTS` appears here at
the next write. This is the reason that the code generates the profile, and does not
ship it.
"""
profile = macterminal.build_profile()
assert len(macterminal._ANSI_KEYS) == len(_VT_SLOTS) == 16
for key, (_slot, _label, rgb) in zip(macterminal._ANSI_KEYS, _VT_SLOTS, strict=True):
assert profile[key] == macterminal.ns_color(rgb), key
def test_the_window_is_opaque_and_bold_is_not_brightness() -> None:
"""The window is opaque, and bold is not brightness.
These are the two settings, with the font, that the profile exists to guarantee.
"""
profile = macterminal.build_profile()
assert profile["BackgroundColor"] == macterminal.ns_color("#000000")
assert "BackgroundBlur" not in profile
assert profile["UseBrightBold"] is False
# The window closes after a clean exit. It stays after a crash, so that the user can
# read the error.
assert profile["shellExitAction"] == 1
def test_the_profile_is_the_same_bytes_whatever_the_command_line() -> None:
"""The profile is the same bytes for each command line.
This test fixes the finding that shaped the module. Terminal names an imported
settings set from the file. It uses the set again when the bytes are the same, and it
adds "MeshTerm 1" when they are not. Thus the profile must not have anything that
changes between starts. The launcher path is fixed, and the command that changes is in
the file that the path points at.
"""
import plistlib
plain = plistlib.dumps(macterminal.build_profile("/Users/x/.meshterm/launch.sh"))
again = plistlib.dumps(macterminal.build_profile("/Users/x/.meshterm/launch.sh"))
assert plain == again
# The value that goes in is a path. It is never an argv or an environment.
profile = macterminal.build_profile("/Users/x/.meshterm/launch.sh")
assert profile["CommandString"] == "/Users/x/.meshterm/launch.sh"
assert "--mock" not in str(profile["CommandString"])
assert "MESHTERM_" not in str(profile["CommandString"])
def test_the_launcher_carries_the_command_and_is_executable(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The launcher has the command and is executable.
The part that changes is in the launcher instead, and the code generates it again at
each start.
"""
monkeypatch.setenv("MESHTERM_HOME", str(tmp_path / "home"))
monkeypatch.setattr(sys, "argv", ["meshterm", "--mock"])
path = macterminal.write_launcher()
body = path.read_text(encoding="utf-8")
assert path.name == "launch.sh"
assert body.startswith("#!/bin/sh")
assert "exec /usr/bin/env " in body
assert body.rstrip().endswith("--mock")
if sys.platform != "win32": # NTFS has no execute bit for chmod to set
assert path.stat().st_mode & 0o111
def test_a_bare_profile_runs_nothing() -> None:
"""A profile with no command runs nothing.
Without a command, the profile is an entry in the list and not a launcher.
"""
profile = macterminal.build_profile()
assert "CommandString" not in profile
assert "RunCommandAsShell" not in profile
def test_the_profile_round_trips_as_a_terminal_file() -> None:
"""The profile makes a round trip as a Terminal file.
What MeshTerm writes is what Terminal reads: a plist, with the nested archives
unchanged.
"""
written = plistlib.dumps(macterminal.build_profile("true"), fmt=plistlib.FMT_XML)
reloaded = plistlib.loads(written)
assert reloaded["name"] == macterminal.PROFILE_NAME
assert reloaded["type"] == "Window Settings"
assert reloaded["CommandString"] == "true"
assert plistlib.loads(reloaded["ANSIRedColor"])["$objects"][1]["NSColorSpace"] == 1
def test_launch_command_carries_meshterm_variables_and_the_marker(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The launch command has the MeshTerm variables and the marker.
Terminal starts the command from a new shell, so what the command needs goes with it.
"""
monkeypatch.setenv("MESHTERM_HOME", "/Users/someone/meshterm test")
monkeypatch.delenv(REOPENED_ENV, raising=False)
monkeypatch.setattr(sys, "argv", ["meshterm", "--mock"])
command = macterminal.launch_command()
assert f"{REOPENED_ENV}=1" in command
# The value is in quotes, because the path of the user can have a space, and the shell
# would split it.
assert "MESHTERM_HOME='/Users/someone/meshterm test'" in command
assert command.endswith("--mock")
# The command starts with a real executable and not a `VAR=value` shell prefix. A
# measurement on macOS 26 showed this. Terminal reads the string as a bare program name
# under one setting, and as a shell line under the other setting. A prefix puts
# "Command not found: MESHTERM_REOPENED=1" on the screen.
assert command.startswith("/usr/bin/env ")
def test_an_unrelated_variable_is_not_carried(monkeypatch: pytest.MonkeyPatch) -> None:
"""The launch command does not carry a variable that is not related to MeshTerm.
The step to a new window must not copy the whole environment into a new shell.
"""
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "nope")
assert "AWS_SECRET_ACCESS_KEY" not in macterminal.launch_command()
def test_a_reopened_session_never_reopens_again(monkeypatch: pytest.MonkeyPatch) -> None:
"""A session that was reopened never reopens again.
This is the guard that stops a relaunch from chaining. The command of the profile sets
the marker.
"""
monkeypatch.setattr(sys, "platform", "darwin")
monkeypatch.setenv("TERM_PROGRAM", "Apple_Terminal")
monkeypatch.setenv(REOPENED_ENV, "1")
assert macterminal.available() is False
assert macterminal.reopen() is False
@pytest.mark.parametrize("program", ["iTerm.app", "ghostty", "vscode", "WezTerm"])
def test_another_terminal_is_left_alone(program: str, monkeypatch: pytest.MonkeyPatch) -> None:
"""The code leaves another terminal alone.
MeshTerm makes no offer to a terminal that already has truecolor and a chosen font.
"""
monkeypatch.setattr(sys, "platform", "darwin")
monkeypatch.delenv(REOPENED_ENV, raising=False)
monkeypatch.setenv("TERM_PROGRAM", program)
assert macterminal.in_apple_terminal() is False
assert macterminal.available() is False
def test_nothing_happens_off_macos(monkeypatch: pytest.MonkeyPatch) -> None:
"""Nothing happens off macOS.
Each entry point does nothing on the platforms that this module is not for.
"""
monkeypatch.setattr(sys, "platform", "win32")
monkeypatch.setenv("TERM_PROGRAM", "Apple_Terminal")
assert macterminal.in_apple_terminal() is False
assert macterminal.available() is False
assert macterminal.reopen() is False
assert macterminal.install_font() is False
def test_the_profile_is_written_under_meshterm_home(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The profile is in MeshTerm's own directory.
It follows ``MESHTERM_HOME`` when that is set.
"""
monkeypatch.setenv("MESHTERM_HOME", str(tmp_path / "home"))
path = macterminal.write_profile()
assert path == tmp_path / "home" / "MeshTerm.terminal"
assert plistlib.loads(path.read_bytes())["name"] == "MeshTerm"
def test_the_readers_preferences_are_never_named(tmp_path, monkeypatch) -> None:
"""The code never names the preferences of the user.
Terminal discards external edits to its preferences when it quits, so no code here
writes them. This test guards against an easy shortcut in the future. It does not guard
against the code of today. The whole design depends on this: MeshTerm gives Terminal a
file, and Terminal does the import.
"""
source = (macterminal.__file__).replace(".pyc", ".py")
with open(source, encoding="utf-8") as handle:
body = handle.read().split('"""', 2)[2]
assert "com.apple.Terminal.plist" not in body
assert "Library/Preferences" not in body
assert "defaults" not in body.replace("default_config_dir", "")
def test_the_font_goes_to_the_users_own_library() -> None:
"""The font goes to the user's own library.
No administrator rights are necessary, and the user can undo it from Font Book.
"""
assert macterminal.USER_FONT_DIR == Path.home() / "Library" / "Fonts"