81 lines
3.9 KiB
Python
Executable File
81 lines
3.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Start an isolated native process and verify HTTP readiness and PWA resources."""
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import socket
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('binary', type=Path)
|
|
parser.add_argument('--timeout', type=int, default=90)
|
|
args = parser.parse_args()
|
|
binary = args.binary.resolve(strict=True)
|
|
with socket.socket() as sock:
|
|
sock.bind(('127.0.0.1', 0))
|
|
port = sock.getsockname()[1]
|
|
base = f'http://127.0.0.1:{port}'
|
|
# Never inherit application secrets, OIDC settings or a developer's JAVA_HOME.
|
|
env = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8'}
|
|
with tempfile.TemporaryDirectory(prefix='werkjournal-smoke-') as directory:
|
|
env['HOME'] = directory
|
|
logfile = Path(directory) / 'server.log'
|
|
with logfile.open('wb') as log:
|
|
process = subprocess.Popen(
|
|
[str(binary), f'--server.port={port}', '--server.address=127.0.0.1'],
|
|
cwd=directory, env=env, stdout=log, stderr=subprocess.STDOUT,
|
|
)
|
|
try:
|
|
deadline = time.monotonic() + args.timeout
|
|
while True:
|
|
if process.poll() is not None:
|
|
raise RuntimeError(f'Native process exited with {process.returncode}')
|
|
try:
|
|
with urllib.request.urlopen(base + '/api/v1/info', timeout=2) as response:
|
|
info = json.load(response)
|
|
if info.get('application') != 'werkjournal' or not info.get('version'):
|
|
raise RuntimeError(f'Unexpected readiness response: {info}')
|
|
break
|
|
except (urllib.error.URLError, TimeoutError):
|
|
if time.monotonic() >= deadline:
|
|
raise RuntimeError('Native readiness timed out')
|
|
time.sleep(0.25)
|
|
for resource, expected in [('/', b'<html'), ('/offline.html', b'Werkjournal')]:
|
|
with urllib.request.urlopen(base + resource, timeout=5) as response:
|
|
if expected not in response.read():
|
|
raise RuntimeError(f'Missing content in {resource}')
|
|
with urllib.request.urlopen(base + '/manifest.webmanifest', timeout=5) as response:
|
|
manifest = json.load(response)
|
|
if manifest.get('name') != 'Werkjournal' or not manifest.get('icons'):
|
|
raise RuntimeError('Incomplete PWA manifest')
|
|
for icon in manifest['icons']:
|
|
icon_url = urllib.parse.urljoin(base + '/', icon['src'])
|
|
if urllib.parse.urlparse(icon_url).netloc != urllib.parse.urlparse(base).netloc:
|
|
raise RuntimeError('Unexpected external PWA icon')
|
|
with urllib.request.urlopen(icon_url, timeout=5) as response:
|
|
if not response.headers.get('Content-Type', '').startswith('image/') or not response.read():
|
|
raise RuntimeError('PWA icon is missing or is not an image')
|
|
with urllib.request.urlopen(base + '/sw.js', timeout=5) as response:
|
|
if 'javascript' not in response.headers.get('Content-Type', '') or not response.read():
|
|
raise RuntimeError('Service worker is missing or is not JavaScript')
|
|
if process.poll() is not None:
|
|
raise RuntimeError('Process stopped during smoke checks')
|
|
print(json.dumps({'result': 'PASS', 'info': info, 'binary': str(binary)}))
|
|
except Exception:
|
|
print(logfile.read_text(errors='replace'))
|
|
raise
|
|
finally:
|
|
if process.poll() is None:
|
|
process.terminate()
|
|
try:
|
|
process.wait(timeout=20)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait()
|