66 lines
3.9 KiB
Python
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()
|