MIL-013: Debian container and Windows Sandbox checks

A Debian image (python:3.13-slim-bookworm with Xvfb and GNOME's window
manager) runs the linters, the whole test suite, the screenshots and the
smoke test of a PyInstaller Linux binary and of the source program, so no
window opens on the development machine. The screenshot tool works on
Linux, a smoke driver presses the known button with the virtual mouse, and
Windows Sandbox scripts are written (not run, they open a window on the
host). README and the wiki Install pages describe it. Review records
RC-077 (Python) and RC-078 (shell).

Task: MIL-013#2
Task: MIL-013#3
Task: MIL-013#4
Task: MIL-013#5
Task: MIL-013#6
Task: MIL-013#7
Task: MIL-013#9
Task: MIL-013#10
This commit is contained in:
2026-10-15 10:09:48 +08:00
parent a72ddb9175
commit f8ccfca02e
24 changed files with 1205 additions and 10 deletions
+41
View File
@@ -0,0 +1,41 @@
"""Tests of the pure parts of the screenshot tool.
@file test_capture_screenshots.py
@brief Checks platform_kind and window_box of tools/capture_screenshots.py.
"""
import importlib.util
from pathlib import Path
from types import ModuleType
import pytest
pytest.importorskip("PIL")
_TOOL_PATH = Path(__file__).resolve().parents[2] / "tools" / "capture_screenshots.py"
def _load_tool() -> ModuleType:
spec = importlib.util.spec_from_file_location("capture_screenshots", _TOOL_PATH)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_platform_kind_is_windows_for_win32() -> None:
assert _load_tool().platform_kind("win32") == "windows"
def test_platform_kind_is_x11_for_linux() -> None:
assert _load_tool().platform_kind("linux") == "x11"
def test_window_box_adds_menu_height_on_x11() -> None:
assert _load_tool().window_box("x11", 10, 100, 900, 726, 28) == (10, 72, 910, 826)
def test_window_box_ignores_menu_height_on_windows() -> None:
box = _load_tool().window_box("windows", 10, 100, 900, 726, 28)
assert box == (10, 100, 910, 826)
+122
View File
@@ -0,0 +1,122 @@
"""Tests of the smoke driver tool.
@file test_smoke_driver.py
@brief Checks the pure helpers and the run loop of tools/docker/smoke_driver.py
with a fake xdotool and a fake program.
"""
import importlib.util
import json
import sys
from collections.abc import Sequence
from pathlib import Path
from types import ModuleType
import pytest
_TOOL_PATH = (
Path(__file__).resolve().parents[2] / "tools" / "docker" / "smoke_driver.py"
)
_GEOMETRY = "WINDOW=7\nX=0\nY=0\nWIDTH=900\nHEIGHT=800\nSCREEN=0\n"
def _load_tool() -> ModuleType:
spec = importlib.util.spec_from_file_location("smoke_driver", _TOOL_PATH)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
# dataclasses looks the module up by name while the class is built
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
class _FakeProgram:
def __init__(self) -> None:
self.stopped = False
def stop(self) -> None:
self.stopped = True
class _FakeSystem:
"""Fake xdotool and program; a click writes the progress file."""
def __init__(self, has_window: bool, writes_progress: bool) -> None:
self.has_window = has_window
self.writes_progress = writes_progress
self.program = _FakeProgram()
self.data_dir: Path | None = None
def start(self, command: Sequence[str]) -> _FakeProgram:
self.data_dir = Path(command[command.index("--data-dir") + 1])
return self.program
def run(self, args: Sequence[str]) -> str:
if args[1] == "search":
return "7\n" if self.has_window else ""
if args[1] == "getwindowgeometry":
return _GEOMETRY
if args[1] == "mousemove" and self.writes_progress:
assert self.data_dir is not None
progress = self.data_dir / "words_to_learn.csv"
progress.write_text("ms;da\nw1;t1\nw2;t2\n", encoding="utf-8")
return ""
def test_known_button_point_is_101_above_bottom() -> None:
assert _load_tool().known_button_point(900, 800) == (650, 699)
def test_words_file_text_has_header_and_cards() -> None:
assert _load_tool().words_file_text(2) == "ms;da\nw1;t1\nw2;t2\n"
def test_parse_geometry_reads_width_and_height() -> None:
assert _load_tool().parse_geometry(_GEOMETRY) == (900, 800)
def test_parse_geometry_fails_without_size() -> None:
with pytest.raises(ValueError):
_load_tool().parse_geometry("X=0\n")
def test_progress_ok_needs_exactly_one_card_less() -> None:
tool = _load_tool()
assert tool.progress_ok(3, 3)
assert not tool.progress_ok(4, 3)
assert not tool.progress_ok(0, 3)
def test_main_returns_zero_when_click_saves_progress(
capsys: pytest.CaptureFixture[str],
) -> None:
system = _FakeSystem(has_window=True, writes_progress=True)
code = _load_tool().main(
["--repeat", "2", "--words", "3", "--command", "prog", "-m", "x"],
run=system.run,
start=system.start,
sleep=lambda _: None,
)
lines = [json.loads(x) for x in capsys.readouterr().out.splitlines()]
assert code == 0
assert lines[0]["ok"] is True
assert lines[0]["clicked"] == [650, 699]
assert lines[-1] == {"runs": 2, "passed": 2}
assert system.program.stopped
def test_main_fails_when_no_window_appears(
capsys: pytest.CaptureFixture[str],
) -> None:
system = _FakeSystem(has_window=False, writes_progress=False)
code = _load_tool().main(
["--repeat", "1", "--timeout", "1", "--command", "prog"],
run=system.run,
start=system.start,
sleep=lambda _: None,
)
first = json.loads(capsys.readouterr().out.splitlines()[0])
assert code == 1
assert first["window"] is False
assert system.program.stopped