Files
MeshTerm/tests/test_instancelock.py
Jean-Pierre MartineauandClaude Opus 5.5 11b5274b31 Docstrings and comments of the tests are in ASD-STE100
Each test docstring states the behaviour that the test proves, in the
simple present. No assertion message, test id, or other string literal
changed. Four statements now agree with the code: the Trace highlight
stays on the last row, a menu count, and two others.

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

115 lines
4.2 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Tests for the guard of one interactive session for each directory, and the atomic writer.
Both exist for the same reason. Each copy of MeshTerm on a machine uses one data directory,
unless the user sets another. Thus two copies can run at the same time by accident, not by
choice.
"""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
import pytest
from meshterm.core.atomicwrite import write_atomically
from meshterm.core.instancelock import LOCK_NAME, InstanceBusy, hold_instance_lock
REPO_ROOT = Path(__file__).resolve().parent.parent
def test_the_lock_is_released_when_the_holder_leaves(tmp_path: Path) -> None:
"""Two sessions in sequence are permitted. The guard stops an overlap, not a reuse."""
with hold_instance_lock(tmp_path):
pass
with hold_instance_lock(tmp_path):
pass
def test_the_lock_file_lives_beside_the_data(tmp_path: Path) -> None:
"""The lock is in the directory that it guards.
If the directory does not exist, MeshTerm makes it.
"""
target = tmp_path / "not-yet"
with hold_instance_lock(target):
assert (target / LOCK_NAME).exists()
def test_a_second_process_is_turned_away(tmp_path: Path) -> None:
"""The real test of an OS lock: another process cannot take it.
A test in one process proves nothing here. A lock that excludes only the thread that
already holds it passes a test in one process. But it fails in the one case for which
the lock exists.
"""
program = (
"import pathlib, sys\n"
f"sys.path.insert(0, {str(REPO_ROOT)!r})\n"
"from meshterm.core.instancelock import hold_instance_lock, InstanceBusy\n"
"try:\n"
f" with hold_instance_lock(pathlib.Path({str(tmp_path)!r})):\n"
" print('TAKEN')\n"
"except InstanceBusy:\n"
" print('REFUSED')\n"
)
with hold_instance_lock(tmp_path):
done = subprocess.run(
[sys.executable, "-c", program], capture_output=True, text=True, timeout=60
)
assert done.stdout.strip() == "REFUSED", done.stderr[-500:]
def test_the_refusal_says_how_to_run_two(tmp_path: Path) -> None:
"""Most of the message is the way to run two copies, because the user needs this next.
A user who sees this message does something that is reasonable. For example, the user
tries a downloaded build, or runs a second radio. A message that says no, but does not
say how, gives only half of the answer.
"""
message = str(InstanceBusy(tmp_path))
assert "MESHTERM_HOME" in message
assert str(tmp_path) in message
def test_a_write_lands_whole_or_not_at_all(tmp_path: Path) -> None:
"""The written file has exactly the requested content, and no temporary file stays."""
target = tmp_path / "sub" / "contacts.json"
write_atomically(target, json.dumps({"a": 1}))
assert json.loads(target.read_text(encoding="utf-8")) == {"a": 1}
assert list(tmp_path.rglob("*.tmp")) == []
def test_the_scratch_file_is_named_for_its_writer(tmp_path: Path, monkeypatch) -> None:
"""Two processes that write the same file must not use the same temporary name.
The rename is atomic, but the file that the rename moves is not. A fixed ``.tmp`` name
lets two writers interleave their writes in one temporary file. Then each writer renames
the mixed content over the real file.
"""
seen: list[Path] = []
real = Path.write_text
def spy(self: Path, *args: object, **kwargs: object) -> int:
seen.append(self)
return real(self, *args, **kwargs) # type: ignore[arg-type]
monkeypatch.setattr(Path, "write_text", spy)
write_atomically(tmp_path / "x.json", "{}")
assert seen and str(__import__("os").getpid()) in seen[0].name
def test_a_failed_write_leaves_no_litter(tmp_path: Path, monkeypatch) -> None:
"""A write that raises an error removes its temporary file. It does not leave a partial file."""
def boom(self: Path, *args: object, **kwargs: object) -> int:
raise OSError("disk full")
monkeypatch.setattr(Path, "write_text", boom)
with pytest.raises(OSError):
write_atomically(tmp_path / "y.json", "{}")
assert list(tmp_path.rglob("*.tmp")) == []