mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
231 lines
6.2 KiB
Python
231 lines
6.2 KiB
Python
# SPDX-License-Identifier: 0BSD
|
|
|
|
"""Resilience checks for android/app/src/main/python/meshchat_wrapper.py."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import http.server
|
|
import importlib
|
|
import json
|
|
import socket
|
|
import sys
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
_ANDROID_PY = _REPO_ROOT / "android" / "app" / "src" / "main" / "python"
|
|
if str(_ANDROID_PY) not in sys.path:
|
|
sys.path.insert(0, str(_ANDROID_PY))
|
|
|
|
|
|
def _free_port():
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.bind(("127.0.0.1", 0))
|
|
port = sock.getsockname()[1]
|
|
sock.close()
|
|
return port
|
|
|
|
|
|
class _FakeStatusHandler(http.server.BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
if self.path != "/api/v1/status":
|
|
self.send_response(404)
|
|
self.end_headers()
|
|
return
|
|
body = json.dumps(
|
|
{
|
|
"status": "ok",
|
|
"stage": "ready",
|
|
"network_ready": True,
|
|
"network_degraded": False,
|
|
"listen_port": self.server.server_port,
|
|
"https_enabled": True,
|
|
},
|
|
).encode("utf-8")
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def log_message(self, *_args, **_kwargs):
|
|
pass
|
|
|
|
|
|
def test_start_server_second_call_skips_while_main_blocks(monkeypatch):
|
|
import meshchatx.meshchat as mm
|
|
|
|
entered = threading.Event()
|
|
release = threading.Event()
|
|
calls: list[int] = []
|
|
|
|
def fake_main():
|
|
calls.append(1)
|
|
entered.set()
|
|
assert release.wait(timeout=10.0)
|
|
|
|
monkeypatch.setattr(mm, "main", fake_main)
|
|
import meshchat_wrapper
|
|
|
|
importlib.reload(meshchat_wrapper)
|
|
|
|
th = threading.Thread(
|
|
target=lambda: meshchat_wrapper.start_server(8000, None),
|
|
daemon=True,
|
|
)
|
|
th.start()
|
|
assert entered.wait(timeout=10.0)
|
|
assert calls == [1]
|
|
|
|
meshchat_wrapper.start_server(8000, None)
|
|
assert calls == [1]
|
|
|
|
release.set()
|
|
th.join(timeout=10.0)
|
|
|
|
|
|
def test_start_server_hook_failure_still_invokes_main(monkeypatch):
|
|
import meshchatx.meshchat as mm
|
|
|
|
calls: list[int] = []
|
|
|
|
monkeypatch.setattr(mm, "main", lambda: calls.append(1))
|
|
|
|
def boom(*_a, **_k):
|
|
raise RuntimeError("hook test")
|
|
|
|
monkeypatch.setattr("meshchatx.android_push_bridge.install_websocket_hook", boom)
|
|
import meshchat_wrapper
|
|
|
|
importlib.reload(meshchat_wrapper)
|
|
meshchat_wrapper.start_server(8000, None)
|
|
assert calls == [1]
|
|
|
|
|
|
def test_start_server_clears_stale_storage_lock(monkeypatch, tmp_path):
|
|
import meshchatx.meshchat as mm
|
|
|
|
calls: list[int] = []
|
|
monkeypatch.setattr(mm, "main", lambda: calls.append(1))
|
|
|
|
storage = tmp_path / "meshchatx" / "storage"
|
|
storage.mkdir(parents=True)
|
|
lock_path = storage / ".meshchatx.lock"
|
|
lock_path.write_bytes(b"12345")
|
|
|
|
import meshchat_wrapper
|
|
|
|
importlib.reload(meshchat_wrapper)
|
|
meshchat_wrapper.start_server(8000, str(tmp_path))
|
|
assert calls == [1]
|
|
assert not lock_path.exists()
|
|
|
|
|
|
def test_clear_stale_storage_lock_missing_is_ok(tmp_path):
|
|
import meshchat_wrapper
|
|
|
|
importlib.reload(meshchat_wrapper)
|
|
meshchat_wrapper._clear_stale_storage_lock(str(tmp_path / "missing"))
|
|
storage = tmp_path / "storage"
|
|
storage.mkdir()
|
|
meshchat_wrapper._clear_stale_storage_lock(str(storage))
|
|
|
|
|
|
def test_start_server_systemexit_includes_cause(monkeypatch):
|
|
import pytest
|
|
|
|
import meshchatx.meshchat as mm
|
|
|
|
def fake_main():
|
|
try:
|
|
raise RuntimeError("storage lock held by pid 12345")
|
|
except RuntimeError as exc:
|
|
raise SystemExit(1) from exc
|
|
|
|
monkeypatch.setattr(mm, "main", fake_main)
|
|
import meshchat_wrapper
|
|
|
|
importlib.reload(meshchat_wrapper)
|
|
with pytest.raises(RuntimeError) as excinfo:
|
|
meshchat_wrapper.start_server(8000, None)
|
|
message = str(excinfo.value)
|
|
assert "code=1" in message
|
|
assert "storage lock held by pid 12345" in message
|
|
|
|
|
|
def test_wait_for_own_backend_or_free_port_reports_free_port():
|
|
import meshchat_wrapper
|
|
|
|
importlib.reload(meshchat_wrapper)
|
|
port = _free_port()
|
|
outcome = meshchat_wrapper._wait_for_own_backend_or_free_port(
|
|
port,
|
|
wait_seconds=1.0,
|
|
poll_interval=0.1,
|
|
)
|
|
assert outcome == "free"
|
|
|
|
|
|
def test_wait_for_own_backend_or_free_port_detects_existing_backend():
|
|
import meshchat_wrapper
|
|
|
|
importlib.reload(meshchat_wrapper)
|
|
port = _free_port()
|
|
server = http.server.HTTPServer(("127.0.0.1", port), _FakeStatusHandler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
started = time.monotonic()
|
|
outcome = meshchat_wrapper._wait_for_own_backend_or_free_port(
|
|
port,
|
|
wait_seconds=5.0,
|
|
poll_interval=0.1,
|
|
probe_timeout=1.0,
|
|
)
|
|
elapsed = time.monotonic() - started
|
|
assert outcome == "serving"
|
|
assert elapsed < 4.0
|
|
finally:
|
|
server.shutdown()
|
|
thread.join(timeout=5.0)
|
|
|
|
|
|
def test_wait_for_own_backend_or_free_port_reports_busy_for_foreign_listener():
|
|
import meshchat_wrapper
|
|
|
|
importlib.reload(meshchat_wrapper)
|
|
port = _free_port()
|
|
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
listener.bind(("127.0.0.1", port))
|
|
listener.listen(1)
|
|
try:
|
|
outcome = meshchat_wrapper._wait_for_own_backend_or_free_port(
|
|
port,
|
|
wait_seconds=0.2,
|
|
poll_interval=0.1,
|
|
probe_timeout=0.3,
|
|
)
|
|
assert outcome == "busy"
|
|
finally:
|
|
listener.close()
|
|
|
|
|
|
def test_start_server_skips_duplicate_when_backend_already_serving(monkeypatch):
|
|
import meshchatx.meshchat as mm
|
|
|
|
calls: list[int] = []
|
|
monkeypatch.setattr(mm, "main", lambda: calls.append(1))
|
|
|
|
import meshchat_wrapper
|
|
|
|
importlib.reload(meshchat_wrapper)
|
|
monkeypatch.setattr(
|
|
meshchat_wrapper,
|
|
"_wait_for_own_backend_or_free_port",
|
|
lambda *_a, **_k: "serving",
|
|
)
|
|
|
|
meshchat_wrapper.start_server(8000, None)
|
|
assert calls == []
|
|
assert meshchat_wrapper._server_loop_active is False
|