arboricx_deployment.py (3911B)
1 #!/usr/bin/env python3 2 """Haskell-only deployment runner smoke test (fixed port 8080 must be free).""" 3 import hashlib 4 from pathlib import Path 5 import socket 6 import subprocess 7 import sys 8 import tempfile 9 import time 10 import urllib.error 11 import urllib.request 12 13 14 def main(): 15 runner = str(Path(sys.argv[1]).resolve()) 16 opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) 17 18 def request(path, data=None, status=200): 19 req = urllib.request.Request('http://127.0.0.1:8080' + path, data=data) 20 try: 21 response = opener.open(req, timeout=10) 22 except urllib.error.HTTPError as error: 23 response = error 24 with response: 25 assert response.status == status, (path, response.status) 26 return response.read() 27 28 with tempfile.TemporaryDirectory(prefix='arboricx-deployment-') as temporary: 29 root = Path(temporary) 30 # Startup failures must exit nonzero rather than return an ignored Result. 31 (root / 'store').write_text('not a directory') 32 failed = subprocess.run([runner], cwd=root, capture_output=True, timeout=30) 33 assert failed.returncode != 0, failed 34 (root / 'store').unlink() 35 with socket.socket() as occupied: 36 occupied.bind(('0.0.0.0', 8080)) 37 occupied.listen() 38 failed = subprocess.run([runner], cwd=root, capture_output=True, timeout=30) 39 assert failed.returncode != 0, failed 40 41 payload = b'opaque registry bytes\x00\xff\n' 42 digest = hashlib.sha256(payload).hexdigest() 43 route = '/_arboricx/bundle/hash/' + digest 44 # Second start checks persistence without a compiler cache in the cwd. 45 for iteration in range(2): 46 with (root / 'server.log').open('w+') as log: 47 proc = subprocess.Popen([runner], cwd=root, stdout=log, stderr=log) 48 try: 49 for _ in range(100): 50 if proc.poll() is not None: 51 log.seek(0) 52 raise AssertionError(log.read()) 53 try: 54 assert request('/_arboricx/health') == b'OK\n' 55 break 56 except (OSError, urllib.error.URLError): 57 time.sleep(.1) 58 else: 59 raise AssertionError('server startup timeout') 60 assert b'Arboricx Server' in request('/') 61 if iteration == 0: 62 reply = request('/_arboricx/bundle', payload, status=201) 63 assert reply.strip() == digest.encode(), reply 64 assert request(route) == payload 65 request('/_arboricx/bundle/hash/not-a-hash', status=400) 66 request('/_arboricx/bundle/hash/' + '0' * 64, status=404) 67 request('/_arboricx/package/../bad', status=400) 68 package = root / 'store/packages/example' 69 package.mkdir(parents=True, exist_ok=True) 70 record = b'arboricx.package-release.v1\nexample\t1.0\t' + b'a' * 64 + b'\t' + digest.encode() + b'\n' 71 (package / '1.0').write_bytes(record) 72 assert request('/_arboricx/package/example/1.0') == record 73 request('/_arboricx/package/example/missing', status=404) 74 finally: 75 proc.terminate() 76 try: 77 proc.wait(timeout=5) 78 except subprocess.TimeoutExpired: 79 proc.kill() 80 proc.wait() 81 assert {p.name for p in root.iterdir()} == {'store', 'server.log'} 82 print('PASS: deployment startup failures, health, binary upload/download, path controls, packages, restart') 83 84 85 if __name__ == '__main__': 86 main()