103 lines
4.3 KiB
Python
103 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Run the browser against an isolated real backend/core and a local subscription fixture."""
|
|
import contextlib
|
|
import http.server
|
|
import json
|
|
import os
|
|
import shutil
|
|
import socket
|
|
import sqlite3
|
|
import subprocess
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
root = Path(__file__).resolve().parent.parent
|
|
for port in (18080, 9090, 7890, 1053):
|
|
with socket.socket() as probe:
|
|
if probe.connect_ex(('127.0.0.1', port)) == 0:
|
|
raise SystemExit(f'Port {port} is occupied. E2E aborted to avoid changing another service.')
|
|
|
|
class Fixture(http.server.BaseHTTPRequestHandler):
|
|
protocol_version = 'HTTP/1.1'
|
|
|
|
def do_CONNECT(self):
|
|
self.send_response(200, 'Connection Established')
|
|
self.end_headers()
|
|
self.close_connection = False
|
|
self.handle_one_request()
|
|
|
|
def do_HEAD(self):
|
|
# Mihomo's unified-delay check repeats HEAD on the established tunnel.
|
|
# Supply a framed keep-alive response, rather than the default HTTP 501.
|
|
time.sleep(0.03)
|
|
self.send_response(200)
|
|
self.send_header('Content-Length', '0')
|
|
self.end_headers()
|
|
|
|
def do_GET(self):
|
|
if self.path == '/subscription':
|
|
data = 'proxies:\n'
|
|
for name in ['香港 · 测试节点', '日本 · 测试节点', '新加坡 · 测试节点']:
|
|
data += f' - name: {name}\n type: http\n server: 127.0.0.1\n port: {self.server.server_port}\n'
|
|
body = data.encode()
|
|
else:
|
|
# Keep successful local health checks above Mihomo's integer-ms zero.
|
|
time.sleep(0.03)
|
|
body = b'local-e2e-proxy'
|
|
self.send_response(200)
|
|
self.send_header('Content-Length', str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
def log_message(self, *args):
|
|
pass
|
|
|
|
fixture = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Fixture)
|
|
threading.Thread(target=fixture.serve_forever, daemon=True).start()
|
|
|
|
def wait_url(url, timeout=20):
|
|
until = time.monotonic() + timeout
|
|
while time.monotonic() < until:
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=1) as response:
|
|
return response.status
|
|
except Exception:
|
|
time.sleep(0.1)
|
|
raise RuntimeError(f'Service failed to start: {url}')
|
|
|
|
root.joinpath('.cache').mkdir(exist_ok=True)
|
|
with tempfile.TemporaryDirectory(prefix='e2e-', dir=root / '.cache') as temporary:
|
|
work = Path(temporary)
|
|
core = work / 'mihomo'
|
|
shutil.copytree(root / 'data/mihomo/rules', core / 'rules')
|
|
command = [str(root / 'bin/clashweb'), '--data', str(work / 'app'), '--core-dir', str(core), '--mihomo', str(root / 'bin/mihomo')]
|
|
initialized = subprocess.run(command + ['--init'], text=True, capture_output=True)
|
|
if initialized.returncode:
|
|
raise SystemExit('Initialization failed: ' + initialized.stderr)
|
|
with contextlib.closing(sqlite3.connect(work / 'app/clashweb.sqlite')) as db:
|
|
token = db.execute("SELECT value FROM kv WHERE key='setup_token'").fetchone()[0]
|
|
processes = []
|
|
try:
|
|
with open(work / 'core.log', 'w') as log:
|
|
processes.append(subprocess.Popen([str(root / 'bin/mihomo'), '-d', str(core), '-f', str(core / 'config.yaml')], stdout=log, stderr=log))
|
|
with open(work / 'app.log', 'w') as log:
|
|
processes.append(subprocess.Popen(command + ['--listen', '127.0.0.1:18080'], stdout=log, stderr=log))
|
|
wait_url('http://127.0.0.1:18080/api/v1/auth/status')
|
|
environment = dict(os.environ, CW_E2E_URL='http://127.0.0.1:18080', CW_E2E_SETUP_TOKEN=token, CW_E2E_SUBSCRIPTION_URL=f'http://127.0.0.1:{fixture.server_port}/subscription')
|
|
result = subprocess.run(['npm', 'run', 'test:e2e'], cwd=root / 'web', env=environment)
|
|
if result.returncode:
|
|
# This harness contains only credential-free local proxy fixtures.
|
|
print('Local fixture core log:', (work / 'core.log').read_text()[-8000:])
|
|
raise SystemExit(result.returncode)
|
|
finally:
|
|
for process in reversed(processes):
|
|
process.terminate()
|
|
try:
|
|
process.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait()
|
|
fixture.shutdown()
|