Files
RawMark/scripts/smoke-release.py
T

139 lines
6.7 KiB
Python

#!/usr/bin/env python3
"""Exercise a release binary using disposable data only, on every supported native platform."""
import contextlib
import hashlib
import json
import os
from pathlib import Path
import secrets
import socket
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
binary = str(Path(sys.argv[1]).resolve())
version = Path('VERSION').read_text().strip()
assert subprocess.check_output([binary, '--version'], text=True).strip() == f'RawMark {version}'
with tempfile.TemporaryDirectory(prefix='rawmark-release-') as directory:
data = Path(directory) / '数据 data'
with socket.socket() as listener:
listener.bind(('127.0.0.1', 0))
port = listener.getsockname()[1]
origin = f'http://127.0.0.1:{port}'
environment = {**os.environ, 'RAWMARK_DATA': str(data),
'RAWMARK_VAULT': str(data / 'vault'), 'RAWMARK_ADDR': f'127.0.0.1:{port}'}
def request(path, *, method='GET', body=None, token=None, status=200, raw=False, headers=None):
headers = {'Accept-Language': 'en', **(headers or {})}
if token:
headers['Authorization'] = 'Bearer ' + token
if body is not None and not isinstance(body, bytes):
headers['Content-Type'] = 'application/json'
body = json.dumps(body).encode()
req = urllib.request.Request(origin + '/api/v1/' + path, data=body, headers=headers, method=method)
try:
response = urllib.request.urlopen(req, timeout=5)
except urllib.error.HTTPError as error:
response = error
with response:
assert response.status == status, (path, response.status, response.read())
if raw:
return response.read()
result = json.load(response)
assert (result['code'] == 0) == (status < 400), result
return result['data']
@contextlib.contextmanager
def running():
with (Path(directory) / 'server.log').open('w+', encoding='utf-8') as log:
process = subprocess.Popen([binary], env=environment, stdout=log, stderr=log)
try:
for _ in range(100):
if process.poll() is not None:
raise RuntimeError(f'Release binary exited with {process.returncode}')
try:
request('health')
break
except urllib.error.URLError:
time.sleep(0.1)
else:
raise TimeoutError('Release binary did not become healthy')
yield
except BaseException:
log.flush()
log.seek(0)
print(log.read(), file=sys.stderr)
raise
finally:
if process.poll() is None:
process.terminate()
process.wait(timeout=20)
with running():
assert request('auth/state')['setUp'] is False
request('tree', status=401)
token = request('auth/setup', method='POST', body={'password': secrets.token_urlsafe(24)})['token']
assert token.startswith('rms_')
entries = request('tree', token=token)['entries']
assert len([entry for entry in entries if not entry['dir'] and entry['path'].endswith('.md')]) == 7
for note in Path('deploy/data/vault').rglob('*.md'):
assert (data / 'vault' / note.relative_to('deploy/data/vault')).read_bytes() == note.read_bytes()
path = urllib.parse.quote('Getting Started/Welcome to RawMark.md')
original = (data / 'vault/Getting Started/Welcome to RawMark.md').read_bytes()
request('files/' + path, method='PUT', body=original + b'\nRelease smoke test.\n', token=token)
note = '中文 folder/笔记 note.md'
moved = '移动 folder/新笔记 note.md'
content = '# Portable note\n\n中文 content.\n'.encode()
request('files/' + urllib.parse.quote(note), method='PUT', body=content, token=token)
request('files/' + urllib.parse.quote(note), method='PUT', body=content + b'Updated.\n', token=token)
request('move', method='POST', body={'from': note, 'to': moved}, token=token)
assert not (data / 'vault' / note).exists()
item = request('files/' + urllib.parse.quote(moved), method='DELETE', token=token)
assert item['id'] in [entry['id'] for entry in request('trash', token=token)['items']]
request('trash/' + item['id'] + '/restore', method='POST', token=token)
assert (data / 'vault' / moved).read_bytes() == content + b'Updated.\n'
before = {str(p.relative_to(data / 'vault')): hashlib.sha256(p.read_bytes()).hexdigest()
for p in (data / 'vault').rglob('*') if p.is_file()}
with running():
assert request('auth/state')['setUp'] is True
request('auth/me', token=token)
after = {str(p.relative_to(data / 'vault')): hashlib.sha256(p.read_bytes()).hexdigest()
for p in (data / 'vault').rglob('*') if p.is_file()}
assert before == after, 'Restart modified existing notes'
archive = request('maintenance/export/full', method='POST', token=token, raw=True)
restored_content = (data / 'vault' / moved).read_bytes()
request('files/' + urllib.parse.quote(moved), method='PUT', body=b'Changed after backup', token=token)
upload = request('maintenance/uploads', method='POST', body={'kind': 'full', 'size': len(archive)},
token=token, status=201)['id']
for offset in range(0, len(archive), 8 << 20):
chunk = archive[offset:offset + (8 << 20)]
result = request('maintenance/uploads/' + upload, method='PUT', body=chunk, token=token,
headers={'Upload-Offset': str(offset)})
assert result['offset'] == offset + len(chunk)
request('maintenance/uploads/' + upload + '/validate', method='POST', token=token)
request('maintenance/restore', method='POST', body={'id': upload, 'confirmed': True}, token=token, status=202)
for _ in range(200):
if ((data / 'vault' / moved).is_file()
and (data / 'vault' / moved).read_bytes() == restored_content
and not (data / '.maintenance/restore.json').exists()):
try:
request('health')
break
except urllib.error.URLError:
pass
time.sleep(0.1)
else:
raise TimeoutError('Full restore did not complete')
request('auth/me', token=token, status=401)
print(f'RawMark {version}: initialization, authentication, examples, overwrite, Unicode paths, '
'move, recycle bin, restart and full backup/restore passed')