from __future__ import annotations import os import shutil import signal import subprocess import time from pathlib import Path from typing import TextIO import pytest _PROJECT_ROOT: Path = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) _SCRIPTS_DIR: Path = _PROJECT_ROOT / "tests" / "artifacts" / "tier2_state" / "default_layout_install_20260629" _GUI_SCRIPT: str = str(_PROJECT_ROOT / "sloppy.py") _INI_FILENAME: str = "manualslop_layout.ini" def _spawn_sloppy_for(workspace: Path, log_suffix: str) -> subprocess.Popen: _SCRIPTS_DIR.mkdir(parents=True, exist_ok=True) log_path: Path = _SCRIPTS_DIR / f"test_{log_suffix}.log" log_file: TextIO = open(log_path, "w", encoding="utf-8", errors="replace") env: dict[str, str] = os.environ.copy() env["PYTHONPATH"] = str(_PROJECT_ROOT.absolute()) args: list[str] = ["uv", "run", "python", "-u", _GUI_SCRIPT] creation: int = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0 proc: subprocess.Popen = subprocess.Popen( args, stdout=log_file, stderr=log_file, text=True, cwd=str(workspace.absolute()), env=env, creationflags=creation, ) log_file.close() return proc def _terminate(process: subprocess.Popen) -> None: if process.poll() is not None: return pid: int | None = process.pid if pid is None: return try: if os.name == "nt": subprocess.run( ["taskkill", "/F", "/T", "/PID", str(pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, ) else: os.killpg(os.getpgid(pid), signal.SIGKILL) except Exception: pass try: process.wait(timeout=2) except Exception: pass def _read_ini(workspace: Path) -> str: ini_path: Path = workspace / _INI_FILENAME if not ini_path.exists(): return "" try: return ini_path.read_text(encoding="utf-8", errors="replace") except OSError: return "" def _has_dockid_after_window_header(text: str) -> bool: lines: list[str] = text.splitlines() for idx, line in enumerate(lines): if line.startswith("[Window][") and line.rstrip().endswith("]"): tail: str = "\n".join(lines[idx + 1:]) if "DockId=" in tail: return True return False def _workspace_for(tmp_path: Path, test_name: str) -> Path: return tmp_path / f"_default_layout_install_{os.getpid()}_{test_name}" def _start_subprocess_in(workspace: Path, log_suffix: str) -> subprocess.Popen: proc: subprocess.Popen = _spawn_sloppy_for(workspace, log_suffix) time.sleep(5.0) return proc def _assert_installed_default(workspace: Path) -> None: ini_path: Path = workspace / _INI_FILENAME assert ini_path.exists(), f"expected {ini_path} to exist after launch" text: str = _read_ini(workspace) assert "[Window][Project Settings]" in text, ( f"installed INI missing [Window][Project Settings]; got first 400 chars: {text[:400]!r}" ) assert _has_dockid_after_window_header(text), ( f"installed INI has no DockId= following a [Window][...] header; got first 400 chars: {text[:400]!r}" ) def test_default_layout_installed_when_ini_missing(tmp_path: Path) -> None: workspace: Path = _workspace_for(tmp_path, "ini_missing") workspace.mkdir(parents=True, exist_ok=True) ini_path: Path = workspace / _INI_FILENAME if ini_path.exists(): ini_path.unlink() proc: subprocess.Popen = _start_subprocess_in(workspace, "ini_missing") try: _assert_installed_default(workspace) finally: _terminate(proc) shutil.rmtree(workspace, ignore_errors=True) def test_default_layout_installed_when_ini_empty(tmp_path: Path) -> None: workspace: Path = _workspace_for(tmp_path, "ini_empty") workspace.mkdir(parents=True, exist_ok=True) ini_path: Path = workspace / _INI_FILENAME ini_path.write_bytes(b"\n\n\n\n\n") proc: subprocess.Popen = _start_subprocess_in(workspace, "ini_empty") try: _assert_installed_default(workspace) finally: _terminate(proc) shutil.rmtree(workspace, ignore_errors=True) def test_default_layout_NOT_installed_when_layout_present(tmp_path: Path) -> None: workspace: Path = _workspace_for(tmp_path, "ini_custom") workspace.mkdir(parents=True, exist_ok=True) ini_path: Path = workspace / _INI_FILENAME custom_lines: list[str] = ["[Window][CustomPanelPersistent]", "Pos=10,10", "DockId=0xDEAD", ""] for i in range(32): custom_lines.append(f"; padding line {i}") custom_lines.append("[Window][Filler]") custom_lines.append(f"Pos={i},{i}") custom_lines.append("Size=100,100") content: str = "\n".join(custom_lines) + "\n" ini_path.write_text(content, encoding="utf-8") assert ini_path.stat().st_size >= 1000, "custom INI must be at least 1000 bytes for this test" proc: subprocess.Popen = _start_subprocess_in(workspace, "ini_custom") try: text: str = _read_ini(workspace) assert "[Window][CustomPanelPersistent]" in text, ( f"custom INI should be preserved (no overwrite), but content changed: {text[:400]!r}" ) assert "DockId=0xDEAD" in text, "custom DockId entry should survive install" finally: _terminate(proc) shutil.rmtree(workspace, ignore_errors=True)