illuscape/tests/test_install.py
pyr0ball 8e245dfe61 feat: cross-platform installer (Windows port)
- install.py / uninstall.py: full Python installer for Linux, macOS, Windows
  - detect_platform.py: shared Inkscape config path detection
  - Windows config path: %APPDATA%\inkscape (native/winget/scoop) or
    %LOCALAPPDATA%\Packages\org.inkscape.Inkscape* (MS Store)
  - Windows desktop: Start Menu .lnk via PowerShell WScript.Shell COM object
  - macOS: XDG / ~/Library/Application Support detection, no desktop step
- install.sh / uninstall.sh: reduced to one-line exec python3 wrappers
  - eliminates SC1091 (source not followed) and SC2155 (declare+assign)
- scripts/detect_platform.sh: export INKSCAPE_CONFIG / INKSCAPE_INSTALL_METHOD
  - fixes SC2034 (variables appear unused to shellcheck)
- assets/illuscape.ico: pre-rendered 16/32/48/64/128/256px ICO (ImageMagick)
- tests/test_install.py: pytest integration tests replacing test_install.bats
  - cross-platform fake inkscape injected via PATH
  - 12 tests covering backup, palettes, templates, prefs, uninstall restore
- ci.yml: add windows-latest matrix leg; shellcheck/inkscape Linux-only;
  bats removed in favour of pytest integration tests; 27 tests total
2026-05-27 11:26:16 -07:00

222 lines
7 KiB
Python

"""
Integration tests for install.py and uninstall.py — cross-platform.
Replaces tests/test_install.bats.
Each test runs inside a fully isolated tmp_path with a fake inkscape binary
injected via PATH and an overridden config home pointing to tmp_path.
"""
from __future__ import annotations
import os
import platform
import subprocess
import sys
from pathlib import Path
import pytest
REPO = Path(__file__).parent.parent
INSTALL_PY = REPO / "install.py"
UNINSTALL_PY = REPO / "uninstall.py"
_FAKE_INKSCAPE_SH = """\
#!/usr/bin/env sh
case "$1" in
--version) echo 'Inkscape 1.3.2 (1:1.3.2+202311252150+091e20ef0f)' ;;
esac
"""
_FAKE_INKSCAPE_BAT = """\
@echo off
if "%1"=="--version" echo Inkscape 1.3.2 (1:1.3.2+202311252150+091e20ef0f)
"""
def _make_fake_inkscape(bin_dir: Path) -> None:
"""Create a platform-appropriate fake inkscape binary in *bin_dir*."""
if platform.system() == "Windows":
bat = bin_dir / "inkscape.bat"
bat.write_text(_FAKE_INKSCAPE_BAT)
else:
script = bin_dir / "inkscape"
script.write_text(_FAKE_INKSCAPE_SH)
script.chmod(0o755)
def _config_dir(tmp_path: Path) -> Path:
"""
Return the expected config path matching detect_config() for this platform
when the env fixture overrides the config home to *tmp_path*.
"""
if platform.system() == "Windows":
# install.py on Windows: APPDATA/inkscape
return tmp_path / "config" / "inkscape"
else:
# install.py on Linux/macOS: XDG_CONFIG_HOME/inkscape
return tmp_path / "config" / "inkscape"
@pytest.fixture()
def env(tmp_path: Path):
"""
Isolated subprocess environment with a fake inkscape and a temp config dir.
On Linux/macOS: overrides HOME and XDG_CONFIG_HOME.
On Windows: overrides APPDATA and LOCALAPPDATA.
"""
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
_make_fake_inkscape(bin_dir)
cfg_dir = _config_dir(tmp_path)
cfg_dir.mkdir(parents=True, exist_ok=True)
base_env = os.environ.copy()
base_env["PATH"] = str(bin_dir) + os.pathsep + base_env.get("PATH", "")
# Block flatpak / snap detection inside subprocesses
base_env.pop("FLATPAK_ID", None)
if platform.system() == "Windows":
base_env["APPDATA"] = str(tmp_path / "config")
base_env["LOCALAPPDATA"] = str(tmp_path / "local")
else:
base_env["XDG_CONFIG_HOME"] = str(tmp_path / "config")
base_env["HOME"] = str(tmp_path)
return {
"tmp_path": tmp_path,
"config_dir": cfg_dir,
"env": base_env,
}
def _run_install(env_fixture: dict, preset: str = "cc") -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(INSTALL_PY), "--yes", f"--preset={preset}"],
env=env_fixture["env"],
cwd=str(REPO),
capture_output=True,
text=True,
)
def _run_uninstall(env_fixture: dict) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(UNINSTALL_PY)],
env=env_fixture["env"],
cwd=str(REPO),
capture_output=True,
text=True,
)
# ── install tests ─────────────────────────────────────────────────────────────
def test_install_creates_backup(env):
result = _run_install(env)
assert result.returncode == 0, result.stderr
backups = list(env["tmp_path"].glob("config/inkscape.bak-illuscape-*"))
assert backups, "No backup directory created"
def test_install_backup_is_idempotent(env):
_run_install(env)
first_backups = sorted(env["tmp_path"].glob("config/inkscape.bak-illuscape-*"))
_run_install(env)
second_backups = sorted(env["tmp_path"].glob("config/inkscape.bak-illuscape-*"))
assert first_backups == second_backups, "Second install created an extra backup"
def test_install_cc_key_file(env):
result = _run_install(env, preset="cc")
assert result.returncode == 0, result.stderr
assert (env["config_dir"] / "keys/illustrator-cc.xml").exists()
def test_install_cs6_key_file(env):
result = _run_install(env, preset="cs6")
assert result.returncode == 0, result.stderr
assert (env["config_dir"] / "keys/illustrator-cs6.xml").exists()
def test_install_palettes(env):
result = _run_install(env)
assert result.returncode == 0, result.stderr
for name in (
"Illustrator-Defaults.gpl",
"Illustrator-Grays.gpl",
"Illustrator-Earth.gpl",
):
assert (env["config_dir"] / "palettes" / name).exists(), f"Missing palette: {name}"
def test_install_templates(env):
result = _run_install(env)
assert result.returncode == 0, result.stderr
for name in (
"Letter.svg",
"A4.svg",
"Web-1920x1080.svg",
"Web-1280x720.svg",
"Print-CMYK-Letter.svg",
"Print-CMYK-A4.svg",
):
assert (env["config_dir"] / "templates" / name).exists(), f"Missing template: {name}"
def test_install_creates_prefs(env):
result = _run_install(env)
assert result.returncode == 0, result.stderr
assert (env["config_dir"] / "preferences.xml").exists()
def test_install_prefs_contains_units(env):
result = _run_install(env)
assert result.returncode == 0, result.stderr
content = (env["config_dir"] / "preferences.xml").read_text()
assert "px" in content or "pt" in content, "No unit setting found in preferences.xml"
def test_install_noninteractive(env):
"""--yes + --preset should exit 0 without any stdin."""
result = _run_install(env)
assert result.returncode == 0, f"Non-interactive install failed:\n{result.stderr}"
# ── uninstall tests ───────────────────────────────────────────────────────────
def test_uninstall_restores_prefs(env):
"""Original prefs.xml is restored after install + uninstall."""
original_xml = '<inkscape version="1.0" />'
(env["config_dir"] / "preferences.xml").write_text(original_xml)
_run_install(env)
# prefs should now contain the patched version
result = _run_uninstall(env)
assert result.returncode == 0, result.stderr
restored = (env["config_dir"] / "preferences.xml").read_text()
assert restored == original_xml, "prefs.xml was not restored to the original content"
def test_uninstall_removes_palettes(env):
_run_install(env)
result = _run_uninstall(env)
assert result.returncode == 0, result.stderr
for name in (
"Illustrator-Defaults.gpl",
"Illustrator-Grays.gpl",
"Illustrator-Earth.gpl",
):
assert not (
env["config_dir"] / "palettes" / name
).exists(), f"Palette was not removed: {name}"
def test_uninstall_removes_templates(env):
_run_install(env)
result = _run_uninstall(env)
assert result.returncode == 0, result.stderr
for name in ("Letter.svg", "A4.svg"):
assert not (
env["config_dir"] / "templates" / name
).exists(), f"Template was not removed: {name}"