| 1 | import copy |
| 2 | import json |
| 3 | from pathlib import Path |
| 4 | import subprocess |
| 5 | import unittest |
| 6 | |
| 7 | from crash_recovery import active_text, verify_text |
| 8 | from document_model import EXPORTER, ordered_pages, view, walk |
| 9 | |
| 10 | |
| 11 | class RecoveryOracleTest(unittest.TestCase): |
| 12 | def test_current_page_is_distinguished_from_native_conflict_content(self): |
| 13 | source = Path(__file__).resolve().parent.parent / 'corpus/collaboration/round-01/offline/notebook/synthetic.one' |
| 14 | model = json.loads(subprocess.check_output([EXPORTER, source])) |
| 15 | (sid, _, revision, page), = ordered_pages(model) |
| 16 | main = next(node for _, node in walk(revision, page) if node['kind']['type'] == 'RichText') |
| 17 | main['kind']['text'] = 'Concurrent edits: current' |
| 18 | manifest = revision['nodes'][revision['roots']['1']] |
| 19 | self.assertTrue(manifest['spaces']) |
| 20 | for conflict in manifest['spaces']: |
| 21 | self.assertNotEqual(sid, conflict) |
| 22 | _, older = view(model, conflict) |
| 23 | node = next(node for node in older['nodes'].values() if node['kind']['type'] == 'RichText') |
| 24 | node['kind']['text'] = 'Concurrent edits: older' |
| 25 | self.assertEqual(active_text(model), 'Concurrent edits: current') |
| 26 | |
| 27 | def test_accounting_and_corruption_controls(self): |
| 28 | logs = {'w0': [ |
| 29 | {'event': 'intent', 'token': ' [w0:0]', 'replacement': ' [w0:0]', 'before': 'Base', 'range': [4, 4], 'attempt': 1}, |
| 30 | {'event': 'commit', 'token': ' [w0:0]', 'attempt': 1, 'started_us': 5, 'finished_us': 10}, |
| 31 | {'event': 'intent', 'token': ' [w0:1]', 'replacement': ' [w0:1]', 'before': 'Base [w0:0]', 'range': [11, 11], 'attempt': 2}, |
| 32 | {'event': 'commit_error', 'token': ' [w0:1]', 'attempt': 2, 'state': 'Unknown', 'started_us': 18, 'finished_us': 20}, |
| 33 | {'event': 'read', 'text': 'Base [w0:0]', 'started_us': 15, 'finished_us': 17}, |
| 34 | ]} |
| 35 | self.assertEqual(verify_text('Base', 'Base [w0:0] [w0:1]', logs)['unacknowledged_retained'], [' [w0:1]']) |
| 36 | self.assertEqual(verify_text('Base', 'Base [w0:0]', logs)['acknowledged'], 1) |
| 37 | for text in ['Base', 'Other [w0:0]', 'Base [w0:0] [w0:0]', 'Base [w0:0] [w0:2]', 'Base [w0:0] [w0:']: |
| 38 | with self.assertRaises(AssertionError): |
| 39 | verify_text('Base', text, logs) |
| 40 | lost_read = copy.deepcopy(logs) |
| 41 | lost_read['w0'][-1]['text'] = 'Base' |
| 42 | with self.assertRaisesRegex(AssertionError, 'read missed'): |
| 43 | verify_text('Base', 'Base [w0:0]', lost_read) |
| 44 | committed_error = copy.deepcopy(logs) |
| 45 | committed_error['w0'][3]['state'] = 'Committed' |
| 46 | with self.assertRaisesRegex(AssertionError, 'acknowledged edit was lost'): |
| 47 | verify_text('Base', 'Base [w0:0]', committed_error) |
| 48 | rejected = copy.deepcopy(logs) |
| 49 | rejected['w0'][3]['state'] = 'NotCommitted' |
| 50 | with self.assertRaisesRegex(AssertionError, 'recorded publication'): |
| 51 | verify_text('Base', 'Base [w0:0] [w0:1]', rejected) |
| 52 | with self.assertRaisesRegex(AssertionError, 'recorded publication'): |
| 53 | verify_text('Base', 'Base [w0:1] [w0:0]', logs) |
| 54 | future = copy.deepcopy(logs) |
| 55 | future['w0'][1]['started_us'] = 18 |
| 56 | with self.assertRaisesRegex(AssertionError, 'future edit'): |
| 57 | verify_text('Base', 'Base [w0:0]', future) |