ss
This commit is contained in:
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from src.android_control import enrich_device_row
|
||||
from src.android_control import _is_png, _strip_to_png, enrich_device_row
|
||||
from src.redroid_control import load_redroid_env, redroid_instance_name, redroid_instance_port
|
||||
|
||||
|
||||
@@ -25,6 +25,17 @@ class AndroidRedroidTests(unittest.TestCase):
|
||||
self.assertEqual(row["transport"], "tcp")
|
||||
self.assertEqual(row["kind"], "redroid")
|
||||
|
||||
def test_strip_to_png_preserves_valid_png_signature(self) -> None:
|
||||
png = b"\x89PNG\r\n\x1a\n" + b"\x00\x00\x00\rIHDR" + b"\r\ninside-body"
|
||||
self.assertEqual(_strip_to_png(png), png)
|
||||
self.assertTrue(_is_png(_strip_to_png(png)))
|
||||
|
||||
def test_strip_to_png_repairs_only_normalized_signature(self) -> None:
|
||||
corrupted_sig = b"\n\n\x89PNG\n\x1a\n" + b"\x00\x00\x00\rIHDR"
|
||||
repaired = _strip_to_png(corrupted_sig)
|
||||
self.assertTrue(repaired.startswith(b"\x89PNG\r\n\x1a\n"))
|
||||
self.assertTrue(_is_png(repaired))
|
||||
|
||||
@patch("src.redroid_control.platform.machine", return_value="x86_64")
|
||||
def test_redroid_env_prefers_x86_alt_image(self, _machine) -> None:
|
||||
env = load_redroid_env()
|
||||
|
||||
@@ -5,7 +5,7 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from src.control_plane import SERVICE_SPECS, deployment_report, stack_summary
|
||||
from src.control_plane import SERVICE_SPECS, deployment_report, network_iface_stats, stack_summary
|
||||
|
||||
|
||||
class ControlPlaneServiceTests(unittest.TestCase):
|
||||
@@ -23,7 +23,8 @@ class ControlPlaneServiceTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_stack_summary_exposes_new_quick_links(self) -> None:
|
||||
summary = stack_summary()
|
||||
with patch("src.control_plane.load_stack_env", return_value={"HOSTNAME_ALIAS": "live"}):
|
||||
summary = stack_summary()
|
||||
links = summary["quick_links"]
|
||||
for key in (
|
||||
"mitmproxy",
|
||||
@@ -34,13 +35,29 @@ class ControlPlaneServiceTests(unittest.TestCase):
|
||||
"neko_browser",
|
||||
"neko_browser_1",
|
||||
"neko_browser_2",
|
||||
"neko_browser_3",
|
||||
):
|
||||
self.assertIn(key, links)
|
||||
neko_instances = summary["neko_instances"]
|
||||
self.assertEqual([item["id"] for item in neko_instances], [1, 2, 3])
|
||||
self.assertEqual([item["id"] for item in neko_instances], [1, 2])
|
||||
self.assertEqual(links["neko_browser"], links["neko_browser_1"])
|
||||
|
||||
def test_stack_summary_exposes_pro_neko_links_when_configured(self) -> None:
|
||||
with patch("src.control_plane.load_stack_env", return_value={"HOSTNAME_ALIAS": "live", "NEKO_INSTANCE_COUNT": "4"}):
|
||||
summary = stack_summary()
|
||||
links = summary["quick_links"]
|
||||
self.assertIn("neko_browser_3", links)
|
||||
self.assertIn("neko_browser_4", links)
|
||||
self.assertEqual([item["id"] for item in summary["neko_instances"]], [1, 2, 3, 4])
|
||||
|
||||
def test_network_stats_expose_realtime_and_total_bandwidth_fields(self) -> None:
|
||||
stats = network_iface_stats()
|
||||
for key in ("interfaces", "total_rx_bytes", "total_tx_bytes", "total_rx_bps", "total_tx_bps"):
|
||||
self.assertIn(key, stats)
|
||||
for iface in stats["interfaces"]:
|
||||
self.assertIn("rx_bps", iface)
|
||||
self.assertIn("tx_bps", iface)
|
||||
self.assertIn("sample_interval_seconds", iface)
|
||||
|
||||
def test_deployment_report_marks_live_local_ready_when_checks_pass(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base_dir = Path(tmp)
|
||||
|
||||
134
d2ypp2/tests/test_install_bootstrap.py
Normal file
134
d2ypp2/tests/test_install_bootstrap.py
Normal file
@@ -0,0 +1,134 @@
|
||||
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()
|
||||
37
d2ypp2/tests/test_live_edge_watchdog.py
Normal file
37
d2ypp2/tests/test_live_edge_watchdog.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class LiveEdgeWatchdogTests(unittest.TestCase):
|
||||
def test_install_stack_registers_edge_watchdog(self) -> None:
|
||||
common = (ROOT / "scripts" / "linux" / "lib" / "common.sh").read_text(encoding="utf-8")
|
||||
self.assertIn("live-edge-watch.service", common)
|
||||
self.assertIn("ExecStart=/bin/bash $ROOT_DIR/scripts/linux/live_edge_watch.sh", common)
|
||||
self.assertIn("Restart=always", common)
|
||||
self.assertIn("systemctl enable --now live-edge-watch.service", common)
|
||||
|
||||
def test_watchdog_repairs_inactive_or_unhealthy_edge(self) -> None:
|
||||
watch = (ROOT / "scripts" / "linux" / "live_edge_watch.sh").read_text(encoding="utf-8")
|
||||
self.assertIn("LIVE_EDGE_WATCH_INTERVAL", watch)
|
||||
self.assertIn("systemctl start live-console.service", watch)
|
||||
self.assertIn("systemctl start live-edge.service", watch)
|
||||
self.assertIn("http://127.0.0.1/health", watch)
|
||||
self.assertIn("systemctl restart live-edge.service", watch)
|
||||
|
||||
def test_uninstall_removes_edge_watchdog(self) -> None:
|
||||
uninstall = (ROOT / "scripts" / "linux" / "uninstall_stack.sh").read_text(encoding="utf-8")
|
||||
self.assertIn("live-edge-watch.service live-edge.service", uninstall)
|
||||
self.assertIn("rm -f /etc/systemd/system/live-edge-watch.service", uninstall)
|
||||
|
||||
def test_stack_env_exposes_watch_interval(self) -> None:
|
||||
env_example = (ROOT / "config" / "system-stack.env.example").read_text(encoding="utf-8")
|
||||
self.assertIn("LIVE_EDGE_WATCH_INTERVAL=15", env_example)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
96
d2ypp2/tests/test_neko_capacity.py
Normal file
96
d2ypp2/tests/test_neko_capacity.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from src.neko_capacity import (
|
||||
desired_neko_count_for_youtube_channels,
|
||||
ensure_neko_capacity_for_youtube_channels,
|
||||
upsert_env_value,
|
||||
)
|
||||
|
||||
|
||||
class NekoCapacityTests(unittest.TestCase):
|
||||
def test_desired_count_keeps_two_instance_floor_and_sixty_four_instance_cap(self) -> None:
|
||||
self.assertEqual(desired_neko_count_for_youtube_channels([]), 2)
|
||||
self.assertEqual(desired_neko_count_for_youtube_channels([{"id": "lane1"}]), 2)
|
||||
self.assertEqual(desired_neko_count_for_youtube_channels([{"id": "lane1"}, {"id": "lane2"}]), 2)
|
||||
self.assertEqual(
|
||||
desired_neko_count_for_youtube_channels(
|
||||
[{"id": "lane1"}, {"id": "lane2"}, {"id": "lane3"}, {"id": "lane4"}]
|
||||
),
|
||||
4,
|
||||
)
|
||||
self.assertEqual(desired_neko_count_for_youtube_channels({"id": f"lane{i}"} for i in range(80)), 64)
|
||||
|
||||
def test_upsert_env_value_preserves_file_and_changes_target_key(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
env = Path(tmp) / "config" / "system-stack.env"
|
||||
env.parent.mkdir()
|
||||
env.write_text("HOSTNAME_ALIAS=live\nNEKO_INSTANCE_COUNT=2\nPORT=8001\n", encoding="utf-8")
|
||||
|
||||
changed = upsert_env_value(env, "NEKO_INSTANCE_COUNT", "3")
|
||||
|
||||
self.assertTrue(changed)
|
||||
self.assertEqual(env.read_text(encoding="utf-8"), "HOSTNAME_ALIAS=live\nNEKO_INSTANCE_COUNT=3\nPORT=8001\n")
|
||||
|
||||
def test_ensure_capacity_increases_env_and_restarts_neko_in_background(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
env = root / "config" / "system-stack.env"
|
||||
env.parent.mkdir()
|
||||
env.write_text("NEKO_INSTANCE_COUNT=2\n", encoding="utf-8")
|
||||
service_ctl = root / "scripts" / "linux" / "service_ctl.sh"
|
||||
service_ctl.parent.mkdir(parents=True)
|
||||
service_ctl.write_text("#!/usr/bin/env bash\n", encoding="utf-8")
|
||||
|
||||
with patch("src.neko_capacity.subprocess.Popen") as popen:
|
||||
result = ensure_neko_capacity_for_youtube_channels(
|
||||
root,
|
||||
[{"id": "lane1"}, {"id": "lane2"}, {"id": "lane3"}],
|
||||
)
|
||||
|
||||
self.assertTrue(result["changed"])
|
||||
self.assertTrue(result["restarted"])
|
||||
self.assertIn("NEKO_INSTANCE_COUNT=3", env.read_text(encoding="utf-8"))
|
||||
popen.assert_called_once()
|
||||
self.assertEqual(popen.call_args.args[0][-2:], ["neko", "restart"])
|
||||
|
||||
def test_ensure_capacity_keeps_saved_env_when_restart_spawn_fails(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
env = root / "config" / "system-stack.env"
|
||||
env.parent.mkdir()
|
||||
env.write_text("NEKO_INSTANCE_COUNT=2\n", encoding="utf-8")
|
||||
service_ctl = root / "scripts" / "linux" / "service_ctl.sh"
|
||||
service_ctl.parent.mkdir(parents=True)
|
||||
service_ctl.write_text("#!/usr/bin/env bash\n", encoding="utf-8")
|
||||
|
||||
with patch("src.neko_capacity.subprocess.Popen", side_effect=OSError("fork failed")):
|
||||
result = ensure_neko_capacity_for_youtube_channels(
|
||||
root,
|
||||
[{"id": "lane1"}, {"id": "lane2"}, {"id": "lane3"}],
|
||||
)
|
||||
|
||||
self.assertTrue(result["changed"])
|
||||
self.assertFalse(result["restarted"])
|
||||
self.assertIn("restart_spawn_failed", result["reason"])
|
||||
self.assertIn("NEKO_INSTANCE_COUNT=3", env.read_text(encoding="utf-8"))
|
||||
|
||||
def test_ensure_capacity_does_not_downscale_manual_larger_count(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
env = root / "config" / "system-stack.env"
|
||||
env.parent.mkdir()
|
||||
env.write_text("NEKO_INSTANCE_COUNT=3\n", encoding="utf-8")
|
||||
|
||||
result = ensure_neko_capacity_for_youtube_channels(root, [{"id": "lane1"}], restart=False)
|
||||
|
||||
self.assertFalse(result["changed"])
|
||||
self.assertEqual(env.read_text(encoding="utf-8"), "NEKO_INSTANCE_COUNT=3\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,9 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -11,14 +15,16 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
class NekoConfigTests(unittest.TestCase):
|
||||
def test_browser_launch_cleanup_script_still_prunes_profile_bloat(self) -> None:
|
||||
script = (ROOT / "services" / "neko" / "browser-launch.sh").read_text(encoding="utf-8")
|
||||
script_path = ROOT / "services" / "neko" / "browser-launch.sh"
|
||||
self.assertTrue(os.access(script_path, os.X_OK))
|
||||
script = script_path.read_text(encoding="utf-8")
|
||||
self.assertIn("arch=\"$(uname -m", script)
|
||||
self.assertIn("--disable-software-rasterizer", script)
|
||||
self.assertIn("--use-gl=swiftshader", script)
|
||||
self.assertIn("NEKO_BROWSER_RENDER_MODE", script)
|
||||
self.assertIn("--start-maximized", script)
|
||||
self.assertIn("--disable-background-mode", script)
|
||||
self.assertIn("--disable-background-networking", script)
|
||||
self.assertNotIn("--disable-background-networking", script)
|
||||
self.assertNotIn("--disable-extensions", script)
|
||||
self.assertIn('ui["developer_mode"] = True', script)
|
||||
self.assertIn('"Local State"', script)
|
||||
@@ -36,16 +42,19 @@ class NekoConfigTests(unittest.TestCase):
|
||||
self.assertTrue(policies["VideoCaptureAllowed"])
|
||||
self.assertTrue(policies["AudioCaptureAllowed"])
|
||||
self.assertEqual(policies["DeveloperToolsAvailability"], 1)
|
||||
self.assertNotIn("ExtensionInstallAllowlist", policies)
|
||||
tm_policy = policies["3rdparty"]["extensions"]["dhdgffkkebhmkfjojejmpbldmpobfkfo"]["jsonImport"][0]
|
||||
self.assertEqual(tm_policy["url"], "http://127.0.0.1:8080/provisioning/tampermonkey-provisioning.json")
|
||||
self.assertEqual(tm_policy["url"], "http://127.0.0.1:18080/tampermonkey-provisioning.json")
|
||||
self.assertTrue(tm_policy["hash"].startswith("1:"))
|
||||
self.assertTrue(tm_policy["installAsSystemScripts"])
|
||||
self.assertFalse(tm_policy["installAsSystemScripts"])
|
||||
|
||||
def test_google_chrome_supervisor_uses_direct_stable_command(self) -> None:
|
||||
conf = (ROOT / "services" / "neko" / "google-chrome.conf").read_text(encoding="utf-8")
|
||||
self.assertIn("command=/etc/neko/browser-launch.sh google-chrome", conf)
|
||||
self.assertNotIn("--bwsi", conf)
|
||||
self.assertIn("autorestart=true", conf)
|
||||
self.assertIn("[program:tampermonkey-provisioning]", conf)
|
||||
self.assertIn("127.0.0.1:18080", conf)
|
||||
self.assertIn("[program:openbox]", conf)
|
||||
self.assertIn("/usr/bin/openbox --config-file /etc/neko/openbox.xml", conf)
|
||||
script = (ROOT / "services" / "neko" / "browser-launch.sh").read_text(encoding="utf-8")
|
||||
@@ -57,12 +66,14 @@ class NekoConfigTests(unittest.TestCase):
|
||||
self.assertIn("command=/etc/neko/browser-launch.sh chromium", conf)
|
||||
self.assertNotIn("--bwsi", conf)
|
||||
self.assertIn("autorestart=true", conf)
|
||||
self.assertIn("[program:tampermonkey-provisioning]", conf)
|
||||
self.assertIn("127.0.0.1:18080", conf)
|
||||
self.assertIn("[program:openbox]", conf)
|
||||
script = (ROOT / "services" / "neko" / "browser-launch.sh").read_text(encoding="utf-8")
|
||||
self.assertIn('profile_dir="/home/neko/.config/chromium"', script)
|
||||
self.assertIn('--user-data-dir="$profile_dir"', script)
|
||||
|
||||
def test_neko_compose_mounts_only_selected_supervisor_conf(self) -> None:
|
||||
def test_legacy_neko_compose_template_mounts_only_selected_supervisor_conf(self) -> None:
|
||||
compose = (ROOT / "services" / "neko" / "docker-compose.yml").read_text(encoding="utf-8")
|
||||
self.assertIn("./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/${NEKO_BROWSER_SUPERVISOR_CONF}:ro", compose)
|
||||
self.assertIn("./browser-launch.sh:/etc/neko/browser-launch.sh:ro", compose)
|
||||
@@ -75,6 +86,44 @@ class NekoConfigTests(unittest.TestCase):
|
||||
self.assertIn("${NEKO_HTTP_PORT_3}:8080", compose)
|
||||
self.assertNotIn("/etc/neko/supervisord/browser.conf:ro", compose)
|
||||
|
||||
def test_dynamic_neko_compose_renderer_supports_more_than_three_instances(self) -> None:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"render_neko_compose",
|
||||
ROOT / "tools" / "render_neko_compose.py",
|
||||
)
|
||||
self.assertIsNotNone(spec)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"NEKO_IMAGE": "test-image",
|
||||
"NEKO_PROFILE_HOST_DIR": f"{tmp}/profile",
|
||||
"NEKO_BROWSER_SUPERVISOR_CONF": "google-chrome.conf",
|
||||
},
|
||||
clear=False,
|
||||
):
|
||||
rendered = module.render(4)
|
||||
|
||||
self.assertIn("container_name: live-neko-4", rendered)
|
||||
self.assertIn('"9203:8080"', rendered)
|
||||
self.assertIn('"52303:52303/tcp"', rendered)
|
||||
self.assertIn('"52400-52499:52400-52499/udp"', rendered)
|
||||
self.assertIn(f'"{tmp}/profile-4:/home/neko/.config/google-chrome"', rendered)
|
||||
|
||||
def test_neko_runtime_scripts_use_generated_compose(self) -> None:
|
||||
service_ctl = (ROOT / "scripts" / "linux" / "service_ctl.sh").read_text(encoding="utf-8")
|
||||
common = (ROOT / "scripts" / "linux" / "lib" / "common.sh").read_text(encoding="utf-8")
|
||||
for script in (service_ctl, common):
|
||||
self.assertIn("render_neko_compose_file()", script)
|
||||
self.assertIn("tools/render_neko_compose.py", script)
|
||||
self.assertIn("docker-compose.generated.yml", script)
|
||||
self.assertIn("compose_file=\"$(render_neko_compose_file)\"", script)
|
||||
self.assertIn("NEKO_MAX_INSTANCE_COUNT:-64", script)
|
||||
|
||||
def test_supervisor_uses_browser_launch_script(self) -> None:
|
||||
chrome = (ROOT / "services" / "neko" / "google-chrome.conf").read_text(encoding="utf-8")
|
||||
chromium = (ROOT / "services" / "neko" / "chromium.conf").read_text(encoding="utf-8")
|
||||
@@ -90,7 +139,9 @@ class NekoConfigTests(unittest.TestCase):
|
||||
self.assertEqual(payload["version"], "1")
|
||||
self.assertEqual(payload["settings"]["runtime_content_mode"], "content")
|
||||
self.assertEqual(payload["scripts"][0]["file_url"], "https://greasyfork.org/scripts/557378-youtube-studio-auto-dismiss/code/YouTube%20Studio%20Auto%20Dismiss.user.js")
|
||||
self.assertEqual(payload["scripts"][0]["source"], userscript_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(payload["scripts"][0]["options"], {})
|
||||
decoded_source = base64.b64decode(payload["scripts"][0]["source"]).decode("utf-8")
|
||||
self.assertEqual(decoded_source, userscript_path.read_text(encoding="utf-8"))
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"render_tampermonkey_provisioning",
|
||||
@@ -102,7 +153,9 @@ class NekoConfigTests(unittest.TestCase):
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
policies = json.loads((ROOT / "services" / "neko" / "policies" / "policies.json").read_text(encoding="utf-8"))
|
||||
expected_hash = policies["3rdparty"]["extensions"]["dhdgffkkebhmkfjojejmpbldmpobfkfo"]["jsonImport"][0]["hash"]
|
||||
tm_import = policies["3rdparty"]["extensions"]["dhdgffkkebhmkfjojejmpbldmpobfkfo"]["jsonImport"][0]
|
||||
self.assertFalse(tm_import["installAsSystemScripts"])
|
||||
expected_hash = tm_import["hash"]
|
||||
self.assertEqual(expected_hash, f"1:{module.tm_hash(payload)}")
|
||||
|
||||
def test_neko_scripts_use_family_specific_profile_dirs(self) -> None:
|
||||
@@ -113,9 +166,13 @@ class NekoConfigTests(unittest.TestCase):
|
||||
self.assertIn("resolve_neko_profile_dir()", common)
|
||||
self.assertIn('${family}-profile-v2', common)
|
||||
self.assertIn("resolve_neko_profile_dir_for_instance()", service_ctl)
|
||||
self.assertIn("seed_neko_tampermonkey_profile()", service_ctl)
|
||||
self.assertIn("Local Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo", service_ctl)
|
||||
self.assertIn("NEKO_HTTP_PORT_2", service_ctl)
|
||||
self.assertIn("NEKO_WEBRTC_UDP_RANGE_3", service_ctl)
|
||||
self.assertIn("resolve_neko_profile_dir_for_instance()", common)
|
||||
self.assertIn("seed_neko_tampermonkey_profile()", common)
|
||||
self.assertIn("Local Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo", common)
|
||||
self.assertIn("NEKO_HTTP_PORT_2", common)
|
||||
self.assertIn("NEKO_WEBRTC_UDP_RANGE_3", common)
|
||||
|
||||
|
||||
188
d2ypp2/tests/test_verify_local_install.py
Normal file
188
d2ypp2/tests/test_verify_local_install.py
Normal file
@@ -0,0 +1,188 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import stat
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
"verify_local_install",
|
||||
ROOT / "tools" / "verify_local_install.py",
|
||||
)
|
||||
assert SPEC and SPEC.loader
|
||||
verify_local_install = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(verify_local_install)
|
||||
|
||||
|
||||
class VerifyLocalInstallTests(unittest.TestCase):
|
||||
def make_install_dir(self, env_text: str) -> Path:
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
install_dir = Path(tmp.name)
|
||||
env_path = install_dir / "config" / "system-stack.env"
|
||||
env_path.parent.mkdir(parents=True)
|
||||
env_path.write_text(env_text, encoding="utf-8")
|
||||
web_index = install_dir / "web-console" / "out" / "index.html"
|
||||
web_index.parent.mkdir(parents=True)
|
||||
web_index.write_text("ok", encoding="utf-8")
|
||||
start = install_dir / "start.sh"
|
||||
start.write_text("#!/usr/bin/env bash\n", encoding="utf-8")
|
||||
start.chmod(start.stat().st_mode | stat.S_IXUSR)
|
||||
return install_dir
|
||||
|
||||
def test_build_report_covers_control_plane_neko_and_android_screenshot(self) -> None:
|
||||
install_dir = self.make_install_dir(
|
||||
"\n".join(
|
||||
[
|
||||
"PORT=8100",
|
||||
"HOSTNAME_ALIAS=live",
|
||||
"ENABLE_LIVE_EDGE_PROXY=1",
|
||||
"ENABLE_NEKO=1",
|
||||
"NEKO_INSTANCE_COUNT=2",
|
||||
"ENABLE_ANDROID_PANEL=1",
|
||||
"ENABLE_REDROID=1",
|
||||
"",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def fake_http_json(url: str, *, timeout: float = 5.0):
|
||||
if url.endswith("/health"):
|
||||
return True, 200, {"status": "ok"}, "", 2.0
|
||||
if url.endswith("/deploy/check"):
|
||||
return True, 200, {"ready": True, "pass_count": 7, "total_count": 7}, "", 3.0
|
||||
if url.endswith("/android/devices"):
|
||||
return (
|
||||
True,
|
||||
200,
|
||||
{
|
||||
"available": True,
|
||||
"devices": [{"serial": "127.0.0.1:5555", "state": "device"}],
|
||||
},
|
||||
"",
|
||||
4.0,
|
||||
)
|
||||
if url.endswith("/youtube_pro_channels"):
|
||||
return True, 200, {"channels": [{"id": "lane1"}, {"id": "lane2"}]}, "", 2.0
|
||||
self.fail(f"unexpected JSON URL: {url}")
|
||||
|
||||
def fake_http_bytes(url: str, *, timeout: float = 5.0):
|
||||
if url == "http://127.0.0.1/health":
|
||||
return True, 200, b"ok", "", 2.0
|
||||
if url.startswith("http://127.0.0.1:8100/android/screenshot?"):
|
||||
return True, 200, verify_local_install.PNG_SIG + b"image", "", 6.0
|
||||
self.fail(f"unexpected bytes URL: {url}")
|
||||
|
||||
def fake_http_json_post(url: str, payload: dict, *, timeout: float = 5.0):
|
||||
if url == "http://127.0.0.1:8100/android/action":
|
||||
self.assertEqual(payload["action"], "wake")
|
||||
self.assertEqual(payload["serial"], "127.0.0.1:5555")
|
||||
return True, 200, {"message": "Wake + swipe + home sent"}, "", 7.0
|
||||
self.fail(f"unexpected POST URL: {url}")
|
||||
|
||||
def fake_docker_exec(container: str, command: str, *, timeout: float = 8.0):
|
||||
if "supervisorctl status google-chrome" in command:
|
||||
return 0, "google-chrome RUNNING pid 8", ""
|
||||
if "supervisorctl status tampermonkey-provisioning" in command:
|
||||
return 0, "tampermonkey-provisioning RUNNING pid 9", ""
|
||||
if "tampermonkey-provisioning.json" in command:
|
||||
return 0, "", ""
|
||||
if "grep -R -a -l" in command:
|
||||
return 0, "/home/neko/.config/google-chrome/Default/Local Extension Settings", ""
|
||||
self.fail(f"unexpected docker exec: {container} {command}")
|
||||
|
||||
args = argparse.Namespace(
|
||||
install_dir=str(install_dir),
|
||||
port="",
|
||||
timeout=5.0,
|
||||
skip_neko=False,
|
||||
skip_android=False,
|
||||
)
|
||||
with patch.object(verify_local_install, "systemd_active", return_value=(True, "active")):
|
||||
with patch.object(verify_local_install, "http_json", side_effect=fake_http_json):
|
||||
with patch.object(verify_local_install, "http_bytes", side_effect=fake_http_bytes):
|
||||
with patch.object(verify_local_install, "http_json_post", side_effect=fake_http_json_post):
|
||||
with patch.object(
|
||||
verify_local_install,
|
||||
"docker_ps_names",
|
||||
return_value=(True, ["live-neko-1", "live-neko-2"], ""),
|
||||
):
|
||||
with patch.object(verify_local_install, "docker_exec", side_effect=fake_docker_exec):
|
||||
report = verify_local_install.build_report(args)
|
||||
|
||||
self.assertTrue(report["ready"])
|
||||
ids = {item["id"] for item in report["checks"]}
|
||||
for check_id in (
|
||||
"api_health",
|
||||
"deploy_check",
|
||||
"edge_health",
|
||||
"live_edge_watch_service",
|
||||
"neko_ip_watch_service",
|
||||
"youtube_pro_neko_capacity",
|
||||
"neko_1_browser",
|
||||
"neko_1_tm_payload",
|
||||
"neko_1_tm_storage",
|
||||
"android_devices",
|
||||
"android_screenshot",
|
||||
"android_action",
|
||||
):
|
||||
self.assertIn(check_id, ids)
|
||||
|
||||
def test_android_endpoint_without_devices_is_not_a_required_install_failure(self) -> None:
|
||||
checks: list[dict[str, object]] = []
|
||||
with patch.object(
|
||||
verify_local_install,
|
||||
"http_json",
|
||||
return_value=(True, 200, {"available": False, "devices": []}, "", 3.0),
|
||||
):
|
||||
verify_local_install.verify_android(
|
||||
checks,
|
||||
{"ENABLE_ANDROID_PANEL": "1", "ENABLE_REDROID": "1"},
|
||||
port="8001",
|
||||
timeout=5.0,
|
||||
)
|
||||
|
||||
report = verify_local_install.summarize(checks)
|
||||
self.assertTrue(report["ready"], json.dumps(report, ensure_ascii=False))
|
||||
self.assertFalse(next(item for item in checks if item["id"] == "android_device_presence")["required"])
|
||||
|
||||
def test_required_failure_returns_degraded_summary(self) -> None:
|
||||
checks: list[dict[str, object]] = []
|
||||
verify_local_install.check(checks, "api_health", "FastAPI /health", False, detail="connection refused")
|
||||
report = verify_local_install.summarize(checks)
|
||||
self.assertFalse(report["ready"])
|
||||
self.assertEqual(report["failing_checks"][0]["id"], "api_health")
|
||||
|
||||
def test_youtube_pro_capacity_fails_when_channels_outgrow_neko_count(self) -> None:
|
||||
checks: list[dict[str, object]] = []
|
||||
with patch.object(
|
||||
verify_local_install,
|
||||
"http_json",
|
||||
return_value=(True, 200, {"channels": [{"id": "a"}, {"id": "b"}, {"id": "c"}]}, "", 1.0),
|
||||
):
|
||||
verify_local_install.verify_youtube_pro_capacity(
|
||||
checks,
|
||||
{"ENABLE_NEKO": "1", "NEKO_INSTANCE_COUNT": "2"},
|
||||
port="8001",
|
||||
timeout=5.0,
|
||||
)
|
||||
|
||||
report = verify_local_install.summarize(checks)
|
||||
self.assertFalse(report["ready"])
|
||||
self.assertEqual(report["failing_checks"][0]["id"], "youtube_pro_neko_capacity")
|
||||
|
||||
def test_neko_required_count_matches_current_three_instance_compose_cap(self) -> None:
|
||||
self.assertEqual(verify_local_install.neko_required_count({"NEKO_INSTANCE_COUNT": "1"}), 2)
|
||||
self.assertEqual(verify_local_install.neko_required_count({"NEKO_INSTANCE_COUNT": "2"}), 2)
|
||||
self.assertEqual(verify_local_install.neko_required_count({"NEKO_INSTANCE_COUNT": "4"}), 4)
|
||||
self.assertEqual(verify_local_install.neko_required_count({"NEKO_INSTANCE_COUNT": "99"}), 64)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user