mirror of
https://github.com/ElHombreAlex/FTL-Faster-Than-Light-VR.git
synced 2026-10-06 01:00:06 +02:00
173 lines
9.3 KiB
Python
173 lines
9.3 KiB
Python
"""Read-only launch readiness and Windows wrapper regression checks; no FTL needed."""
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import launch
|
|
|
|
|
|
@unittest.skipUnless(os.name == "nt", "The live launcher is Windows only")
|
|
class LaunchSetupTests(unittest.TestCase):
|
|
def setUp(self):
|
|
# Native Windows lookup must be able to read these fixtures too; some
|
|
# sandboxes grant that access in the checkout but not the user's Temp.
|
|
scratch = Path(__file__).resolve().parents[1] / "local_game_data/test_scratch"
|
|
scratch.mkdir(parents=True, exist_ok=True)
|
|
self.temporary = tempfile.TemporaryDirectory(dir=scratch)
|
|
self.root = Path(self.temporary.name) / "FTL checkout with spaces"
|
|
self.root.mkdir()
|
|
self.lab = self.root / "separate lab"
|
|
self.lab.mkdir()
|
|
self.source = self.root / "owned original"
|
|
self.source.mkdir()
|
|
(self.source / "ftl.dat").write_bytes(b"synthetic archive placeholder")
|
|
(self.lab / "FTLGame.exe").write_bytes(b"synthetic executable placeholder")
|
|
(self.lab / "Hyperspace.dll").write_bytes(b"synthetic loader placeholder")
|
|
self.marker = {"source_game": str(self.source), "activated": True, "save_prefix": "ftlvr"}
|
|
self.write_marker()
|
|
names = ("GetShiftState", "GetCtrlState", "ForceAutofireFlag", "OnTextInput",
|
|
"OnTextEvent", "TextInputOnRender", "TextInputStart", "CommandGuiRenderStatic",
|
|
"CommandGuiRenderPause", "TabbedWindowOnRender", "ChoiceBoxOnRender",
|
|
"MouseControlOnRender", "StarMapOnRender", "MenuScreenOnRender", "OptionsScreenOnRender", "CombatControlRenderTarget")
|
|
self.hooks = self.lab / "hooks.json"
|
|
self.hooks.write_text(json.dumps({"rvas": dict.fromkeys(names, 1),"offsets":{"gui_game_over":0x1e88,"focus_window_open":4},
|
|
"sha1": hashlib.sha1((self.lab / "FTLGame.exe").read_bytes()).hexdigest()}))
|
|
self.godot = self.root / "godot.exe"
|
|
self.godot.write_bytes(b"synthetic renderer placeholder")
|
|
self.local = self.root / "local_game_data"
|
|
self.local.mkdir()
|
|
self.manifest = self.local / "manifest.json"
|
|
self.manifest.write_text(json.dumps({"ships": ["test"], "fonts": 1, "ui_assets": 1}))
|
|
self.config = {"lab": str(self.lab), "hooks": str(self.hooks), "godot": str(self.godot)}
|
|
self.project_patch = patch.object(launch, "PROJECT", self.root)
|
|
self.project_patch.start()
|
|
self.packages_patch = patch.object(launch.importlib.util, "find_spec", return_value=object())
|
|
self.packages_patch.start()
|
|
|
|
def tearDown(self):
|
|
self.packages_patch.stop()
|
|
self.project_patch.stop()
|
|
self.temporary.cleanup()
|
|
|
|
def write_marker(self):
|
|
(self.lab / "ftlvr-lab.json").write_text(json.dumps(self.marker))
|
|
|
|
def test_shared_preflight_accepts_complete_desktop_setup(self):
|
|
self.assertEqual(launch.preflight(self.config, desktop=True)["mode"], "desktop")
|
|
|
|
def test_old_hooks_require_modal_hud_capture_update(self):
|
|
value = json.loads(self.hooks.read_text())
|
|
for name in ("MenuScreenOnRender", "OptionsScreenOnRender", "CombatControlRenderTarget"):
|
|
with self.subTest(name=name):
|
|
incomplete = dict(value, rvas={key: address for key, address in value["rvas"].items() if key != name})
|
|
self.hooks.write_text(json.dumps(incomplete))
|
|
with self.assertRaisesRegex(ValueError, "Regenerate lab hooks.json.*live native HUD capture"):
|
|
launch.preflight(self.config, desktop=True)
|
|
|
|
def test_missing_configuration_explains_setup(self):
|
|
with self.assertRaisesRegex(ValueError, "Run SETUP.cmd"):
|
|
launch.load_config(self.local / "launcher.json")
|
|
|
|
def test_config_accepts_utf8_bom(self):
|
|
path = self.local / "launcher.json"
|
|
path.write_text(json.dumps(self.config), encoding="utf-8-sig")
|
|
self.assertEqual(launch.load_config(path), self.config)
|
|
|
|
def test_blank_or_relative_configuration_is_rejected(self):
|
|
for path, message in (("", "nonempty"), ("godot.exe", "absolute")):
|
|
with self.subTest(path=path), self.assertRaisesRegex(ValueError, message):
|
|
launch.preflight(dict(self.config, godot=path), desktop=True)
|
|
|
|
def test_game_and_renderer_need_correct_path_types(self):
|
|
with self.assertRaisesRegex(ValueError, "Missing godot"):
|
|
launch.preflight(dict(self.config, godot=str(self.root)), desktop=True)
|
|
|
|
def test_missing_or_partial_assets_prevent_game_launch(self):
|
|
self.manifest.unlink()
|
|
with self.assertRaisesRegex(ValueError, "Local game assets are missing"):
|
|
launch.preflight(self.config, desktop=True)
|
|
self.manifest.write_text(json.dumps({"ships": ["test"], "fonts": 0, "ui_assets": 1}))
|
|
with self.assertRaisesRegex(ValueError, "extraction is incomplete"):
|
|
launch.preflight(self.config, desktop=True)
|
|
|
|
def test_qa_save_prefix_is_rejected_by_normal_launcher(self):
|
|
self.marker["save_prefix"] = "testvr"
|
|
self.write_marker()
|
|
with self.assertRaisesRegex(ValueError, "ftlvr save prefix"):
|
|
launch.preflight(self.config, desktop=True)
|
|
|
|
def test_click_wrappers_report_unconfigured_checkout_without_hanging(self):
|
|
checkout = self.root / "unconfigured clone"
|
|
checkout.mkdir()
|
|
(checkout / "tools").mkdir()
|
|
actual_project = Path(__file__).resolve().parents[1]
|
|
helper = actual_project / "tools/show_launcher_error.cmd"
|
|
shutil.copyfile(helper, checkout / "tools/show_launcher_error.cmd")
|
|
for name in ("RUN-VR.cmd", "RUN-DESKTOP.cmd", "CHECK-SETUP.cmd"):
|
|
shutil.copyfile(actual_project / name, checkout / name)
|
|
environment = dict(os.environ, FTLVR_NO_PAUSE="1")
|
|
result = subprocess.run([os.environ.get("COMSPEC", "cmd.exe"), "/d", "/c", name],
|
|
cwd=checkout, env=environment, capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(result.returncode, 1, name)
|
|
self.assertIn("SETUP.cmd", result.stdout, name)
|
|
self.assertIn("Launch stopped", result.stdout, name)
|
|
|
|
def test_setup_rejects_microsoft_store_alias(self):
|
|
checkout = self.root / "store alias clone"
|
|
checkout.mkdir()
|
|
(checkout / "tools").mkdir()
|
|
actual_project = Path(__file__).resolve().parents[1]
|
|
for name in ("SETUP.cmd", "tools/show_launcher_error.cmd"):
|
|
shutil.copyfile(actual_project / name, checkout / name)
|
|
alias = checkout / "fake LocalAppData/Microsoft/WindowsApps"
|
|
alias.mkdir(parents=True)
|
|
(alias / "python.exe").touch()
|
|
# System32 can contain a real py.exe on CI runners. Keep only the
|
|
# lookup utility, not that directory, on this synthetic PATH.
|
|
lookup = checkout / "isolated lookup tools"
|
|
lookup.mkdir()
|
|
where = lookup / "where.exe"
|
|
shutil.copyfile(Path(os.environ["SystemRoot"]) / "System32/where.exe", where)
|
|
environment = dict(os.environ, FTLVR_NO_PAUSE="1", LOCALAPPDATA=str(checkout / "fake LocalAppData"),
|
|
PATH=str(lookup) + os.pathsep + str(alias))
|
|
environment.pop("FTLVR_PYTHON", None)
|
|
absent_launcher = subprocess.run([str(where), "py"], cwd=checkout, env=environment,
|
|
capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(absent_launcher.returncode, 1, "Synthetic PATH must not expose the host's py launcher")
|
|
store_alias = subprocess.run([str(where), "python"], cwd=checkout, env=environment,
|
|
capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(store_alias.returncode, 0)
|
|
self.assertEqual(store_alias.stdout.strip().casefold(), str(alias / "python.exe").casefold())
|
|
result = subprocess.run([os.environ.get("COMSPEC", "cmd.exe"), "/d", "/c", "SETUP.cmd --dry-run"],
|
|
cwd=checkout, env=environment, capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(result.returncode, 1)
|
|
self.assertIn("Microsoft Store alias is not a Python installation", result.stdout)
|
|
self.assertFalse((checkout / ".venv").exists())
|
|
|
|
def test_setup_accepts_explicit_python_and_forwards_dry_run(self):
|
|
checkout = self.root / "explicit Python clone"
|
|
checkout.mkdir()
|
|
(checkout / "tools").mkdir()
|
|
actual_project = Path(__file__).resolve().parents[1]
|
|
for name in ("SETUP.cmd", "tools/setup_environment.py", "tools/show_launcher_error.cmd"):
|
|
shutil.copyfile(actual_project / name, checkout / name)
|
|
environment = dict(os.environ, FTLVR_NO_PAUSE="1", FTLVR_PYTHON=sys.executable)
|
|
result = subprocess.run([os.environ.get("COMSPEC", "cmd.exe"), "/d", "/c", "SETUP.cmd --dry-run"],
|
|
cwd=checkout, env=environment, capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertIn("Dry run complete", result.stdout)
|
|
self.assertFalse((checkout / ".venv").exists())
|
|
self.assertFalse((checkout / "local_game_data").exists())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|