Raiju/tests/events-storage-smoke.py
2026-10-25 06:05:42 +03:00

66 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Event writes share one lock: parallel saves lose nothing, damaged storage survives."""
from concurrent.futures import ThreadPoolExecutor
import json
import os
import tempfile
from support import Client, Site
os.environ['PHP_CLI_SERVER_WORKERS'] = '8' # real parallel requests instead of one queue
with tempfile.TemporaryDirectory(prefix='raiju-events-') as folder:
site = Site(folder).start()
try:
events_file = site.root / 'content/events.json'
clients = [Client(site).login() for _ in range(8)] # one session each, so requests do not queue on the session lock
def event(index):
return json.dumps({'id': f'2026-10-{index + 1:02d}-parallel', 'date': f'2026-10-{index + 1:02d}',
'title': {'en': f'Parallel {index}'}})
def save(index):
return clients[index % len(clients)].api('save-event', event=event(index))[1]
with ThreadPoolExecutor(max_workers=8) as workers:
results = list(workers.map(save, range(24)))
assert all(result['success'] for result in results), results
stored = json.loads(events_file.read_text())
assert sorted(item['id'] for item in stored['events']) == sorted(f'2026-10-{i + 1:02d}-parallel' for i in range(24)), 'parallel saves lost events'
assert not list(events_file.parent.glob('.raiju-json-*')), 'temporary files left behind'
admin = clients[0]
# Update in place keeps one entry
changed = json.loads(event(0)); changed['title'] = {'en': 'Changed'}
assert admin.api('save-event', event=json.dumps(changed))[1]['message'] == 'Event updated'
assert [e['title']['en'] for e in json.loads(events_file.read_text())['events'] if e['id'] == changed['id']] == ['Changed']
# Hide, trash, restore, delete for good, empty trash: parallel toggles must all land
with ThreadPoolExecutor(max_workers=8) as workers:
toggled = list(workers.map(lambda i: clients[i % 8].api('toggle-event-visibility', id=f'2026-10-{i + 1:02d}-parallel')[1], range(1, 9)))
assert all(result['success'] and result['data']['hidden'] for result in toggled), toggled
hidden = {e['id'] for e in json.loads(events_file.read_text())['events'] if e.get('hidden')}
assert len(hidden) == 8, 'parallel visibility changes lost updates'
victim = '2026-10-03-parallel'
assert admin.api('delete-event', id=victim)[1]['success']
data = json.loads(events_file.read_text())
assert victim not in [e['id'] for e in data['events']] and data['trash'][0]['event']['id'] == victim
assert admin.api('delete-event', id=victim)[1]['message'] == 'Event not found'
assert admin.api('restore-event', id=victim)[1]['success']
assert victim in [e['id'] for e in json.loads(events_file.read_text())['events']]
assert admin.api('delete-event', id=victim)[1]['success'] and admin.api('delete-event-permanent', id=victim)[1]['success']
assert json.loads(events_file.read_text())['trash'] == []
assert admin.api('delete-event-permanent', id=victim)[1]['message'] == 'Event not found in trash'
assert admin.api('empty-events-trash')[1]['success']
assert list((site.root / 'backups').glob('events_*.json')), 'backups are still written'
# A damaged events file is never replaced by an empty one
events_file.write_text('{damaged-json')
assert not admin.api('save-event', event=event(30))[1]['success']
assert not admin.api('toggle-event-visibility', id='2026-10-04-parallel')[1]['success']
assert not admin.api('delete-event', id='2026-10-04-parallel')[1]['success']
assert events_file.read_text() == '{damaged-json', 'damaged events file was overwritten'
print('PASS events: 24 parallel saves and 8 parallel toggles keep every entry, trash cycle intact, damaged store preserved')
finally:
site.close()