| 1 | """Independent append-intent accounting across an abrupt storage interruption.""" |
| 2 | from collections import Counter |
| 3 | import re |
| 4 | from document_model import ordered_pages, walk |
| 5 | |
| 6 | |
| 7 | def active_text(model): |
| 8 | (_, _, revision, page), = ordered_pages(model) |
| 9 | text, = [node['kind']['text'] for _, node in walk(revision, page) |
| 10 | if node['kind']['type'] == 'RichText' and node['kind']['text'].startswith('Concurrent edits:')] |
| 11 | return text |
| 12 | |
| 13 | |
| 14 | def verify_text(baseline, current, logs): |
| 15 | events = [event for rows in logs.values() for event in rows] |
| 16 | intents = {event['token'] for event in events if event['event'] == 'intent'} |
| 17 | possible = set() |
| 18 | versions = {baseline} |
| 19 | predecessors = set() |
| 20 | for rows in logs.values(): |
| 21 | outcomes = {event['attempt']: event for event in rows if event['event'] in ('commit', 'retry', 'commit_error')} |
| 22 | for event in rows: |
| 23 | if event['event'] != 'intent': continue |
| 24 | outcome = outcomes.get(event['attempt']) |
| 25 | if outcome is None or outcome['event'] == 'commit' or (outcome['event'] == 'commit_error' and outcome['state'] != 'NotCommitted'): |
| 26 | possible.add(event['token']) |
| 27 | assert event['replacement'] == event['token'] and event['range'] == [len(event['before'].encode('utf-16-le')) // 2] * 2 |
| 28 | predecessors.add(event['before']) |
| 29 | versions.add(event['before'] + event['token']) |
| 30 | assert predecessors <= versions, 'A publication does not follow recorded history' |
| 31 | acknowledged = [event for event in events if event['event'] == 'commit' or |
| 32 | (event['event'] == 'commit_error' and event['state'] == 'Committed')] |
| 33 | assert len({event['token'] for event in acknowledged}) == len(acknowledged), 'An edit was acknowledged twice' |
| 34 | |
| 35 | def tokens(text): |
| 36 | assert text.startswith(baseline), 'Previously retained content changed' |
| 37 | assert text in versions, 'Text is not a recorded publication result' |
| 38 | suffix = text[len(baseline):] |
| 39 | found = re.findall(r' \[w\d+:\d+\]', suffix) |
| 40 | assert ''.join(found) == suffix, 'Unexpected or partially persisted text' |
| 41 | assert all(count == 1 for count in Counter(found).values()), 'An edit was replayed twice' |
| 42 | assert set(found) <= intents, 'Text has no matching editing intent' |
| 43 | return set(found) |
| 44 | |
| 45 | persisted = tokens(current) |
| 46 | assert persisted <= possible, 'A definitively uncommitted edit appeared in storage' |
| 47 | assert {event['token'] for event in acknowledged} <= persisted, 'A durability-acknowledged edit was lost' |
| 48 | reads = [event for event in events if event['event'] == 'read'] |
| 49 | for read in reads: |
| 50 | observed = tokens(read['text']) |
| 51 | assert {event['token'] for event in acknowledged if event['finished_us'] < read['started_us']} <= observed, 'A read missed an acknowledged edit' |
| 52 | assert not {event['token'] for event in acknowledged if event['started_us'] > read['finished_us']} & observed, 'A read observed a future edit' |
| 53 | return {'acknowledged': len(acknowledged), 'retained': len(persisted), |
| 54 | 'unacknowledged_retained': sorted(persisted - {event['token'] for event in acknowledged}), |
| 55 | 'reads': len(reads), 'text': current} |