e965451842
Bundled layouts/default.ini (relocated from tests/artifacts/ in Phase 1) contained a [Docking] data block with a hardcoded DockSpace ID 0xAFBEEF01 plus per-window DockId references to nodes 0x10 and 0x11. Those IDs were captured at the time the layout was first generated; on any fresh session HelloImgui computes dockspace IDs dynamically (typically a hash of the dockspace name + creation order) so the hardcoded literal is stale by the first render and the orphan docking instructions are silently dropped. Result: window positions stored in the INI render the windows as floating at their absolute Pos coordinates, but the auto-created dockspace captures the full window body, hiding them all. User observed empty dockspace with only the menu ribbon rendering. Two-part fix: 1. layouts/default.ini: remove [Docking] data block and per-window DockId lines. Comment rewritten to explain why the auto-dock strategy is the only session-stable option. Each [Window] entry now has only Pos + Size + Collapsed=0, so HelloImgui's auto-dock layer places the panels as tabs in the central dockspace on first render. 2. _install_default_layout_if_empty: after writing the bundled INI to disk, also call imgui.load_ini_settings_from_memory(src_text) to force the live HelloImgui session to apply the new INI. Without this, the install only takes effect on the NEXT launch (since HelloImgui reads cwd/manualslop_layout.ini BEFORE the post_init callback fires). With it, first-launch panels appear immediately. Tests: - tests/test_default_layout_install.py assertions updated: instead of checking for a per-window DockId line, the install now verifies (a) [Window][Project Settings] entry exists, (b) the INI has at least one [Window] entry, (c) the INI has no [Docking] data block. - New _assert_live_session_apply() on tests 1 and 2 verifies the "(and applied to live session)" log line appears in stderr, confirming imgui.load_ini_settings_from_memory was invoked. 17/17 tests pass (3 install + 2 reset_layout + 8 adjacent gui/commands).
182 lines
5.7 KiB
Python
182 lines
5.7 KiB
Python
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 _read_launch_log(log_suffix: str) -> str:
|
|
log_path: Path = _SCRIPTS_DIR / f"test_{log_suffix}.log"
|
|
if not log_path.exists():
|
|
return ""
|
|
try:
|
|
return log_path.read_text(encoding="utf-8", errors="replace")
|
|
except OSError:
|
|
return ""
|
|
|
|
|
|
def _assert_live_session_apply(log_suffix: str) -> None:
|
|
text: str = _read_launch_log(log_suffix)
|
|
assert "and applied to live session" in text, (
|
|
f"install write succeeded but live-session apply did not happen; "
|
|
f"expected the live-apply confirmation line in stderr, got: "
|
|
f"{[l for l in text.splitlines() if 'installed' in l]!r}"
|
|
)
|
|
|
|
|
|
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_window_with_collapsed_zero(text: str) -> bool:
|
|
for line in text.splitlines():
|
|
if line.startswith("[Window][") and line.rstrip().endswith("]"):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _has_no_docking_block(text: str) -> bool:
|
|
return "[Docking][Data]" not in text
|
|
|
|
|
|
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_window_with_collapsed_zero(text), (
|
|
f"installed INI has no [Window][...] entry; got first 400 chars: {text[:400]!r}"
|
|
)
|
|
assert _has_no_docking_block(text), (
|
|
f"installed INI should not contain a [Docking][Data] block (HelloImgui dockspace IDs are session-specific); 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)
|
|
_assert_live_session_apply("ini_missing")
|
|
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)
|
|
_assert_live_session_apply("ini_empty")
|
|
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)
|