153 lines
8.5 KiB
Python
Executable File
153 lines
8.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Simulated Web Server telemetry against the production admin JS/CSS bundle.
|
|
|
|
Browser-only test: does not assert live RustProxy, PHP-FPM, or network behavior.
|
|
Run: python3 scripts/tests/web_server_0330_ui_test.py
|
|
"""
|
|
from __future__ import annotations
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
router = (ROOT / 'src/admin/router.rs').read_text()
|
|
match = re.search(r'fn admin_app_javascript\([^)]*\)[^{]*\{', router)
|
|
assert match, 'admin_app_javascript definition missing'
|
|
portion = router[match.end():]
|
|
portion = portion[:re.search(r'\]\s*\.join\("\\n"\)', portion).start()]
|
|
fragments = []
|
|
for line in portion.splitlines():
|
|
line = line.strip()
|
|
if 'include_str!("' in line:
|
|
path = re.search(r'include_str!\("([^"]+)"\)', line).group(1)
|
|
fragments.append((ROOT / 'src/admin' / path).read_text())
|
|
elif line == 'admin_vhosts_javascript(),':
|
|
fragments.append('(() => {\n' + '\n'.join((ROOT / 'src/admin/web/vhosts' / f'{part}.js').read_text() for part in ['core', 'protocol', 'health', 'rates', 'editor', 'render']) + '\n})();')
|
|
elif line.startswith('"') and line.endswith('",'):
|
|
fragments.append(json.loads(line[:-1]))
|
|
BUNDLE = '\n'.join(fragments)
|
|
assert BUNDLE.index('window.rustProxyOverviewSamples =') < BUNDLE.index('window.rustProxyOverviewSamples.onChange(')
|
|
|
|
WEB = {
|
|
'requests': 100, 'status_2xx': 90, 'status_3xx': 2, 'status_4xx': 5, 'status_5xx': 3,
|
|
'response_headers_us': 2_500_000, 'response_bytes': 10_000, 'active_headers': 2,
|
|
'static_requests': 70, 'php_requests': 25, 'other_requests': 5,
|
|
'methods': [80, 10, 5, 2, 3], 'selected_status': [75, 5, 2, 0, 5, 1, 1, 1],
|
|
'failures': [0] * 9, 'compression': [5, 3, 2, 1],
|
|
}
|
|
PHP = {
|
|
'requests': 25, 'status_2xx': 22, 'status_3xx': 0, 'status_4xx': 1, 'status_5xx': 2,
|
|
'response_headers_us': 1_000_000, 'response_bytes': 2_000,
|
|
'failures': [1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
}
|
|
|
|
def observation(factor: int):
|
|
web = {key: ([factor * v for v in value] if isinstance(value, list) else factor * value)
|
|
for key, value in WEB.items()}
|
|
php = {key: ([factor * v for v in value] if isinstance(value, list) else factor * value)
|
|
for key, value in PHP.items()}
|
|
web['active_headers'] = 2
|
|
return {'revision': 10, 'web_server_metrics': {
|
|
'site-A': {'web': web, 'php': {'metrics': php, 'capacity_limit': 32, 'capacity_used': 18}}
|
|
}}
|
|
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
|
|
for width in (320, 375, 800, 1440):
|
|
page = browser.new_page(viewport={'width': width, 'height': 900})
|
|
errors = []
|
|
page.on('pageerror', lambda err: errors.append(str(err)))
|
|
page.set_content((ROOT / 'src/admin/web/index.html').read_text())
|
|
page.add_style_tag(content=(ROOT / 'src/admin/web/style.css').read_text())
|
|
page.add_script_tag(content=BUNDLE)
|
|
page.evaluate("""() => {
|
|
document.querySelector('#workspace').hidden = false;
|
|
document.querySelector('#panel-overview').hidden = false;
|
|
document.querySelectorAll('[data-dashboard-widget^="www_"]').forEach(n => n.hidden = false);
|
|
currentPanel = 'overview';
|
|
window.__clock = 1000;
|
|
Object.defineProperty(performance, 'now', { configurable: true, value: () => window.__clock });
|
|
}""")
|
|
first, second = observation(1), observation(2)
|
|
state = page.evaluate("""([one, two]) => {
|
|
const api = window.rustProxyWebOverview;
|
|
api.render(one);
|
|
window.__clock = 4000;
|
|
api.render(two);
|
|
return {
|
|
rps: document.getElementById('www_requests-all-value').textContent,
|
|
php: document.getElementById('www_requests-php-value').textContent,
|
|
latency: document.getElementById('www_latency-web-value').textContent,
|
|
capacity: document.getElementById('www_capacity-value').textContent,
|
|
types: document.getElementById('www_kind-value').textContent,
|
|
status: document.getElementById('www_status-value').textContent,
|
|
bytes: document.getElementById('www_traffic-value').textContent,
|
|
top: document.getElementById('www_top_services-value').textContent,
|
|
layoutWidth: document.documentElement.scrollWidth,
|
|
screenWidth: window.innerWidth,
|
|
children: document.querySelectorAll('[data-dashboard-widget^="www_"]').length,
|
|
sampleCount: document.getElementById('www_requests-all-line').getAttribute('d'),
|
|
limit: window.rustProxyOverviewSamples.getLimit(),
|
|
};
|
|
}""", [first, second])
|
|
assert state['children'] == 12, state
|
|
assert state['rps'] != '-' and state['php'] != '-', state
|
|
assert 'ms' in state['latency'] and '18' in state['capacity'], state
|
|
assert state['types'].endswith('%') and state['status'].endswith('%'), state
|
|
assert '/s' in state['top'], state
|
|
assert state['layoutWidth'] <= state['screenWidth'] + 2, (width, state)
|
|
assert not errors, (width, errors)
|
|
# Exercise the Web Server per-service accordion with the same status,
|
|
# without any real HTTP/PHP request or extra polling.
|
|
details = page.evaluate("""(status) => {
|
|
envelope = { revision: 10, config: {
|
|
web_services: [{ id: 'site-A', name: 'Example', document_root: '/var/www/site-A',
|
|
index: ['index.html'], php: { enabled: true } }],
|
|
vhosts: [], php_fpm_profiles: []
|
|
}};
|
|
currentPanel = 'web-server';
|
|
window.rustProxyWebServer.render();
|
|
const detail = document.querySelector('#web-service-list .www-service-panel');
|
|
if (!detail) return { missing: true };
|
|
detail.open = true;
|
|
window.rustProxyWebServer.renderLive(status, false);
|
|
return { text: detail.textContent, rows: detail.querySelectorAll('[data-web-live-field]').length };
|
|
}""", second)
|
|
assert not details.get('missing') and details['rows'] == 7, (width, details)
|
|
assert '18 / 32' in details['text'], (width, details)
|
|
# The first reading after an overdue browser interval must establish a
|
|
# fresh baseline without adding a fake 0/s dip to the service chart.
|
|
continuity = page.evaluate("""([first, second, paused, resumed]) => {
|
|
const api = window.rustProxyWebServer;
|
|
const chart = document.querySelector('#web-service-list [data-web-live-id="site-A"] polyline');
|
|
const rate = document.querySelector('#web-service-list [data-web-live-field="rate"]');
|
|
window.__clock = 5000; api.renderLive(first);
|
|
window.__clock = 8000; api.renderLive(second);
|
|
const before = chart.getAttribute('points');
|
|
window.__clock = 100000; api.renderLive(paused);
|
|
const during = chart.getAttribute('points'), unavailable = rate.textContent;
|
|
window.__clock = 103000; api.renderLive(resumed);
|
|
return { before, during, unavailable, after: chart.getAttribute('points'), resumedRate: rate.textContent };
|
|
}""", [observation(2), observation(3), observation(10), observation(11)])
|
|
assert continuity['before'] == continuity['during'], (width, continuity)
|
|
assert continuity['unavailable'] == '-', (width, continuity)
|
|
assert continuity['resumedRate'] != '-', (width, continuity)
|
|
assert continuity['after'] != continuity['before'], (width, continuity)
|
|
assert not errors, (width, errors)
|
|
old = page.evaluate("""() => {
|
|
currentPanel = 'overview';
|
|
window.rustProxyWebOverview.render({ revision: 11, web_server_metrics: {} });
|
|
return {
|
|
rate: document.getElementById('www_requests-value').textContent,
|
|
status: document.getElementById('www_status-value').textContent,
|
|
top: document.getElementById('www_top_services-bars').childElementCount,
|
|
};
|
|
}""")
|
|
assert old == {'rate': '-', 'status': '-', 'top': 0}, (width, old)
|
|
assert not errors, (width, errors)
|
|
print(f'PASS simulated Web Server cards and service accordion at {width}px: {state["rps"]}, {state["php"]}, layout {state["layoutWidth"]}px')
|
|
page.close()
|
|
browser.close()
|
|
print('PASS production JS bundle + all twelve visible WWW widgets (simulated data only)')
|