Commit 6d4cd584 authored by Eric Duminil's avatar Eric Duminil
Browse files

Adding simstadt --install

parent cbe708e5
......@@ -6,10 +6,10 @@ import re
import subprocess
import sys
from pathlib import Path
from .install import cmd_install
from .utils import chdir
def cmd_info(args: argparse.Namespace) -> None:
from .runner import get_simstadt_folder, get_simstadt_output, _simstadt_script
......@@ -82,6 +82,7 @@ def build_parser() -> argparse.ArgumentParser:
description="simstadt - Python library for SimStadt workflows.",
)
parser.add_argument("--gui", action="store_true", help="Launch the SimStadt GUI.")
parser.add_argument("--install", action="store_true", help="Download and install the latest SimStadt release to ~/Desktop.")
parser.add_argument("template", nargs="?", help="Template name (from SIMSTADT_TEMPLATE_PATH) or path to a .flow directory.")
parser.add_argument("citygml", nargs="?", type=Path, help="Path to the CityGML input file.")
parser.add_argument("-d", "--description", help="Human-readable label for the result.")
......@@ -96,6 +97,10 @@ def build_parser() -> argparse.ArgumentParser:
def app() -> None:
parser = build_parser()
args = parser.parse_args()
if args.install:
cmd_install(args)
if args.template and args.citygml:
cmd_run(args)
else:
......
"""Download and install SimStadt from the official release ZIP."""
import io
import re
import shutil
import subprocess
import sys
import tempfile
import urllib.request
import zipfile
from pathlib import Path
def _parse_simstadt_name(manifest: str) -> str:
m = re.search(
r"Implementation-Version:\s*(\S+)\s+\((\w+),\s*rev\.\s*(\w+),\s*(\d+)",
manifest,
)
if not m:
raise ValueError("Cannot parse Implementation-Version from MANIFEST.MF")
version, branch, rev, date = m.groups()
return f"SimStadt2_{version}_{branch}_{date}_{rev}"
def _get_java_version() -> int | None:
try:
result = subprocess.run(["java", "-version"], capture_output=True, text=True)
m = re.search(r'version "(\d+)', result.stderr)
return int(m.group(1)) if m else None
except FileNotFoundError:
return None
def cmd_install(args) -> None:
url = "https://simstadt.hft-stuttgart.de/download/InstallFiles/SimStadt2_latest.zip"
desktop = Path.home() / "Desktop"
if not desktop.exists():
print(f"Error: {desktop} not found", file=sys.stderr)
raise SystemExit(1)
print("Downloading SimStadt...", file=sys.stderr)
tmp = None
try:
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as f:
tmp = Path(f.name)
try:
urllib.request.urlretrieve(url, tmp)
except Exception as e:
print(f"Error: Download failed: {e}", file=sys.stderr)
raise SystemExit(1)
try:
with zipfile.ZipFile(tmp) as zf:
jar_entry = next(
(n for n in zf.namelist() if re.match(r"lib/simstadt-desktop-.*\.jar$", n)),
None,
)
if not jar_entry:
print("Error: simstadt-desktop JAR not found in ZIP", file=sys.stderr)
raise SystemExit(1)
with zipfile.ZipFile(io.BytesIO(zf.read(jar_entry))) as jar:
if "META-INF/MANIFEST.MF" not in jar.namelist():
print("Error: MANIFEST.MF not found in JAR", file=sys.stderr)
raise SystemExit(1)
manifest = jar.read("META-INF/MANIFEST.MF").decode(errors="replace")
try:
name = _parse_simstadt_name(manifest)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
raise SystemExit(1)
dest = desktop / name
if dest.exists():
print(f"Already installed: {name}")
return
dest.mkdir()
try:
for member in zf.infolist():
member_path = (dest / member.filename).resolve()
if not member_path.is_relative_to(dest.resolve()):
print(f"Error: ZIP contains unsafe path: {member.filename}", file=sys.stderr)
raise SystemExit(1)
zf.extractall(dest)
for member in zf.infolist():
mode = member.external_attr >> 16
if mode:
(dest / member.filename).chmod(mode)
except BaseException:
shutil.rmtree(dest, ignore_errors=True)
raise
print(f"Installed: {dest}")
except zipfile.BadZipFile as e:
print(f"Error: Invalid ZIP file: {e}", file=sys.stderr)
raise SystemExit(1)
finally:
if tmp and tmp.exists():
tmp.unlink()
java_version = _get_java_version()
if java_version is None:
print("Warning: Java not found. SimStadt requires Java 17. Download: https://bell-sw.com/pages/downloads/#jdk-17-lts", file=sys.stderr)
elif java_version < 17:
print(f"Warning: Java {java_version} found but SimStadt requires Java 17. Download: https://bell-sw.com/pages/downloads/#jdk-17-lts", file=sys.stderr)
"""Tests for the simstadt --install command."""
import io
import sys
import zipfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from simstadt.cli import app, build_parser
from simstadt.install import _get_java_version, _parse_simstadt_name
def _make_test_zip(version="0.14.0-SNAPSHOT", branch="develop", rev="4431e10", date="20260520"):
manifest = f"Implementation-Version: {version} ({branch}, rev. {rev}, {date}\n )\n"
jar_buf = io.BytesIO()
with zipfile.ZipFile(jar_buf, "w") as jar:
jar.writestr("META-INF/MANIFEST.MF", manifest)
zip_buf = io.BytesIO()
with zipfile.ZipFile(zip_buf, "w") as zf:
zf.writestr(f"lib/simstadt-desktop-{version}.jar", jar_buf.getvalue())
info = zipfile.ZipInfo("SimStadt.sh")
info.external_attr = 0o755 << 16
zf.writestr(info, "#!/bin/bash\n")
return zip_buf.getvalue()
# ---------------------------------------------------------------------------
# _parse_simstadt_name
# ---------------------------------------------------------------------------
def test_parse_simstadt_name():
manifest = "Implementation-Version: 0.14.0-SNAPSHOT (develop, rev. 4431e10, 20260520\n )\n"
assert _parse_simstadt_name(manifest) == "SimStadt2_0.14.0-SNAPSHOT_develop_20260520_4431e10"
def test_parse_simstadt_name_invalid():
with pytest.raises(ValueError, match="Cannot parse"):
_parse_simstadt_name("Manifest-Version: 1.0\nCreated-By: Maven\n")
# ---------------------------------------------------------------------------
# _get_java_version
# ---------------------------------------------------------------------------
def test_get_java_version_17():
with patch("simstadt.install.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stderr='openjdk version "17.0.3" 2023-04-18\n')
assert _get_java_version() == 17
def test_get_java_version_old():
with patch("simstadt.install.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stderr='java version "11.0.2" 2019-01-15\n')
assert _get_java_version() == 11
def test_get_java_version_not_found():
with patch("simstadt.install.subprocess.run", side_effect=FileNotFoundError):
assert _get_java_version() is None
def test_get_java_version_unrecognised_output():
with patch("simstadt.install.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stderr="GraalVM CE 21 (2024-01-16)\n")
assert _get_java_version() is None
# ---------------------------------------------------------------------------
# cmd_install
# ---------------------------------------------------------------------------
def test_install_new_version(monkeypatch, capsys, tmp_path):
desktop = tmp_path / "Desktop"
desktop.mkdir()
zip_bytes = _make_test_zip()
def mock_urlretrieve(url, dest):
Path(dest).write_bytes(zip_bytes)
monkeypatch.setattr("urllib.request.urlretrieve", mock_urlretrieve)
monkeypatch.setattr("simstadt.install._get_java_version", lambda: 17)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setattr(sys, "argv", ["simstadt", "--install"])
app()
out, _ = capsys.readouterr()
assert "Installed:" in out
dest = desktop / "SimStadt2_0.14.0-SNAPSHOT_develop_20260520_4431e10"
assert dest.is_dir()
sh = dest / "SimStadt.sh"
assert sh.exists()
assert sh.stat().st_mode & 0o111 # executable bits preserved from ZIP
def test_install_already_installed(monkeypatch, capsys, tmp_path):
desktop = tmp_path / "Desktop"
desktop.mkdir()
(desktop / "SimStadt2_0.14.0-SNAPSHOT_develop_20260520_4431e10").mkdir()
zip_bytes = _make_test_zip()
def mock_urlretrieve(url, dest):
Path(dest).write_bytes(zip_bytes)
monkeypatch.setattr("urllib.request.urlretrieve", mock_urlretrieve)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setattr(sys, "argv", ["simstadt", "--install"])
app()
out, _ = capsys.readouterr()
assert "Already installed" in out
def test_install_warns_missing_java(monkeypatch, capsys, tmp_path):
desktop = tmp_path / "Desktop"
desktop.mkdir()
zip_bytes = _make_test_zip()
def mock_urlretrieve(url, dest):
Path(dest).write_bytes(zip_bytes)
monkeypatch.setattr("urllib.request.urlretrieve", mock_urlretrieve)
monkeypatch.setattr("simstadt.install._get_java_version", lambda: None)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setattr(sys, "argv", ["simstadt", "--install"])
app()
_, err = capsys.readouterr()
assert "Java not found" in err
assert "bell-sw.com" in err
def test_install_warns_old_java(monkeypatch, capsys, tmp_path):
desktop = tmp_path / "Desktop"
desktop.mkdir()
zip_bytes = _make_test_zip()
def mock_urlretrieve(url, dest):
Path(dest).write_bytes(zip_bytes)
monkeypatch.setattr("urllib.request.urlretrieve", mock_urlretrieve)
monkeypatch.setattr("simstadt.install._get_java_version", lambda: 11)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setattr(sys, "argv", ["simstadt", "--install"])
app()
_, err = capsys.readouterr()
assert "Java 11" in err
assert "bell-sw.com" in err
def test_install_parser_flag():
args = build_parser().parse_args(["--install"])
assert args.install is True
def test_install_no_desktop(monkeypatch, capsys, tmp_path):
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setattr(sys, "argv", ["simstadt", "--install"])
with pytest.raises(SystemExit):
app()
_, err = capsys.readouterr()
assert "Desktop" in err
import sys
import pytest
from simstadt import main
def test_simstadt_script(simstadt_folder, capsys):
@pytest.mark.integration
def test_simstadt_script(simstadt_folder, monkeypatch, capsys):
monkeypatch.setattr(sys, "argv", ["simstadt"])
main()
captured = capsys.readouterr()
assert "SimStadt folder :" in captured.out
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment