135 lines
6.0 KiB
Python
135 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import os
|
|
import re
|
|
import stat
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class InstallBootstrapTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.script = (ROOT / "install.sh").read_text(encoding="utf-8")
|
|
|
|
def test_install_sh_is_remote_bootstrapper_not_thin_wrapper(self) -> None:
|
|
mode = (ROOT / "install.sh").stat().st_mode
|
|
self.assertTrue(mode & stat.S_IXUSR, "install.sh must stay executable for local installs")
|
|
self.assertIn("SHD2YPP2 remote bootstrapper", self.script)
|
|
self.assertIn("curl -fsSL <url>/install.sh | bash", self.script)
|
|
self.assertIn("DEFAULT_INSTALL_DIR=\"/opt/live/d2ypp\"", self.script)
|
|
self.assertIn("DEFAULT_LOG_DIR=\"/var/log/shd2ypp2\"", self.script)
|
|
self.assertIn("DEFAULT_LOCK_FILE=\"/run/lock/shd2ypp2-install.lock\"", self.script)
|
|
self.assertNotRegex(self.script, r"exec bash \"\\$ROOT_DIR/install-all\\.sh\"")
|
|
|
|
def test_remote_source_defaults_match_project_layout(self) -> None:
|
|
self.assertIn('DEFAULT_REPO_URL="http://gitlab.nomadro.com/gitlab-instance-0a899031/sh.git"', self.script)
|
|
self.assertIn('DEFAULT_REPO_REF="best"', self.script)
|
|
self.assertIn('DEFAULT_REPO_SUBDIR="d2ypp2"', self.script)
|
|
self.assertIn("LIVE_REPO_URL", self.script)
|
|
self.assertIn("LIVE_REPO_REF", self.script)
|
|
self.assertIn("LIVE_REPO_SUBDIR", self.script)
|
|
self.assertIn("git clone --depth 1 --branch", self.script)
|
|
self.assertIn("Fetched source does not contain SHD2YPP2 project", self.script)
|
|
|
|
def test_profiles_are_normalized_for_existing_stack_installer(self) -> None:
|
|
self.assertRegex(self.script, r"all\|full\).*PROFILE=\"all\"")
|
|
self.assertRegex(self.script, r"hub\|core\|sh\).*PROFILE=\"hub\"")
|
|
self.assertRegex(self.script, r"business\|live\|app\|sh2\).*PROFILE=\"business\"")
|
|
self.assertIn('bash "$stack" "$PROFILE"', self.script)
|
|
|
|
def test_bootstrap_has_lock_logging_and_privilege_handling(self) -> None:
|
|
self.assertIn("setup_logging()", self.script)
|
|
self.assertIn("acquire_lock()", self.script)
|
|
self.assertIn("flock -n 9", self.script)
|
|
self.assertIn("sudo -E", self.script)
|
|
self.assertIn("LIVE_INSTALL_LOG_FILE", self.script)
|
|
self.assertIn("tee -a \"$LOG_FILE\"", self.script)
|
|
|
|
def test_sync_excludes_runtime_state_and_preserves_config(self) -> None:
|
|
required_excludes = [
|
|
".venv/",
|
|
"node_modules/",
|
|
"web-console/node_modules/",
|
|
"logs/",
|
|
"downloads/",
|
|
"backup_config/",
|
|
".pm2/",
|
|
".process_runner/",
|
|
"services/neko/data/",
|
|
"config/system-stack.env",
|
|
]
|
|
for item in required_excludes:
|
|
self.assertIn(f"--exclude '{item}'", self.script)
|
|
self.assertIn("rsync -a --delete", self.script)
|
|
self.assertIn('LIVE_STACK_REPO_ROOT="$INSTALL_DIR"', self.script)
|
|
self.assertIn('project="$(sync_to_install_dir "$project")"', self.script)
|
|
|
|
def test_post_install_verifies_real_control_plane(self) -> None:
|
|
self.assertIn("post_verify()", self.script)
|
|
self.assertIn("http://127.0.0.1:$port/health", self.script)
|
|
self.assertIn("http://127.0.0.1:$port/deploy/check", self.script)
|
|
self.assertIn("journalctl -u live-console.service -n 120", self.script)
|
|
self.assertIn("Control plane did not become healthy", self.script)
|
|
self.assertIn('tools/verify_local_install.py', self.script)
|
|
self.assertIn('python3 "$verifier" --install-dir "$INSTALL_DIR"', self.script)
|
|
|
|
def test_launch_exports_cli_host_and_port_to_runtime_environment(self) -> None:
|
|
spec = importlib.util.spec_from_file_location("launch_for_test", ROOT / "scripts" / "launch.py")
|
|
self.assertIsNotNone(spec)
|
|
module = importlib.util.module_from_spec(spec)
|
|
assert spec and spec.loader
|
|
sys.modules[spec.name] = module
|
|
try:
|
|
spec.loader.exec_module(module)
|
|
finally:
|
|
sys.modules.pop(spec.name, None)
|
|
|
|
captured: dict[str, str | bool] = {}
|
|
|
|
def fake_run_prod(host: str, port: str, skip_build: bool, reload: bool) -> None:
|
|
captured.update({"host": host, "port": port, "skip_build": skip_build, "reload": reload})
|
|
|
|
old_host = os.environ.pop("HOST", None)
|
|
old_port = os.environ.pop("PORT", None)
|
|
try:
|
|
with patch.object(
|
|
sys,
|
|
"argv",
|
|
["launch.py", "--host", "127.0.0.1", "--port", "18101", "--skip-build", "--no-env-check"],
|
|
):
|
|
with patch.object(module, "load_runtime_env"):
|
|
with patch.object(module, "raise_nofile_limit"):
|
|
with patch.object(module, "ensure_runtime_layout", return_value=[]):
|
|
with patch.object(module, "run_prod", side_effect=fake_run_prod):
|
|
module.main()
|
|
self.assertEqual(captured["host"], "127.0.0.1")
|
|
self.assertEqual(captured["port"], "18101")
|
|
self.assertEqual(os.environ["HOST"], "127.0.0.1")
|
|
self.assertEqual(os.environ["PORT"], "18101")
|
|
finally:
|
|
if old_host is not None:
|
|
os.environ["HOST"] = old_host
|
|
else:
|
|
os.environ.pop("HOST", None)
|
|
if old_port is not None:
|
|
os.environ["PORT"] = old_port
|
|
else:
|
|
os.environ.pop("PORT", None)
|
|
|
|
def test_no_placeholder_language_in_bootstrap_contract(self) -> None:
|
|
forbidden = ["TODO", "FIXME", "theoretically", "manual only", "手动修改"]
|
|
for marker in forbidden:
|
|
self.assertNotIn(marker, self.script)
|
|
self.assertIsNone(re.search(r"\bpass\b", self.script))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|