139 lines
6.7 KiB
Python
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')
|