| 1 | #!/usr/bin/env python3 |
| 2 | """Verify captured native reads of Rust-written files.""" |
| 3 | import copy |
| 4 | import hashlib |
| 5 | import json |
| 6 | from pathlib import Path |
| 7 | import subprocess |
| 8 | import xml.etree.ElementTree as ET |
| 9 | from native_xml import ns, pages, texts |
| 10 | |
| 11 | root = Path(__file__).resolve().parent.parent |
| 12 | corpus = root / 'corpus/writer' |
| 13 | manifest = json.loads((corpus / 'manifest.json').read_text()) |
| 14 | for item in manifest['files']: |
| 15 | data = (corpus / item['path']).read_bytes() |
| 16 | assert len(data) == item['bytes'] |
| 17 | assert hashlib.sha256(data).hexdigest() == item['sha256'] |
| 18 | |
| 19 | for run in ['cold-edit-01', 'cold-edit-02']: |
| 20 | captured = pages(corpus / run / 'read') |
| 21 | assert len(captured) == 1 and texts(captured[0]) == ['Portable plain text...'] |
| 22 | |
| 23 | for before, after in [ |
| 24 | (root / 'corpus/native/20260905-05/snapshots/02-text/notebook/synthetic.one', corpus / 'edit-01/notebook/synthetic.one'), |
| 25 | (corpus / 'native-edit-02/notebook/synthetic.one', corpus / 'edit-02/notebook/synthetic.one'), |
| 26 | (root / 'corpus/native-ink/20260905-ui/notebook/synthetic.one', corpus / 'complex-01/notebook/synthetic.one'), |
| 27 | ]: |
| 28 | source = before.read_bytes() |
| 29 | assert after.read_bytes()[1024:len(source)] == source[1024:] |
| 30 | |
| 31 | native_saved = subprocess.check_output(['cargo', 'run', '-p', 'onestore', '--quiet', '--example', 'inventory', '--', str(corpus / 'native-edit-02/notebook/synthetic.one')], cwd=root, text=True) |
| 32 | assert [bytes.fromhex(row.split('\t')[2]).decode('ascii') for row in native_saved.splitlines() if row.startswith('ascii\t')] == ['Fictitious plain text.'] |
| 33 | |
| 34 | before = pages(root / 'corpus/native-ink/cold-ui-ink/read')[0] |
| 35 | after = pages(corpus / 'cold-complex-01/read')[0] |
| 36 | assert texts(after) == ['Compatible: café, 東京, مرحبا', 'Fictitious positioned outline.', 'Left cell', 'Right cell'] |
| 37 | |
| 38 | |
| 39 | def normalized(page): |
| 40 | page = copy.deepcopy(page) |
| 41 | for node in page.iter(): |
| 42 | for name in ['ID', 'objectID', 'pathCache', 'selected']: |
| 43 | node.attrib.pop(name, None) |
| 44 | if node.text: |
| 45 | node.text = node.text.replace('Fictitious: café', 'Compatible: café') |
| 46 | if not node.text.strip(): |
| 47 | node.text = None |
| 48 | node.tail = None |
| 49 | for outline in page.findall('one:Outline', ns): |
| 50 | if texts(outline) == ['Compatible: café, 東京, مرحبا']: |
| 51 | # OneNote recalculates outline height when the replacement text wraps. |
| 52 | outline.find('one:Size', ns).attrib.pop('height') |
| 53 | return page |
| 54 | |
| 55 | |
| 56 | def fingerprint(node): |
| 57 | return node.tag, sorted(node.attrib.items()), node.text, [fingerprint(child) for child in node] |
| 58 | |
| 59 | |
| 60 | assert fingerprint(normalized(before)) == fingerprint(normalized(after)) |
| 61 | assert next((root / 'corpus/native-ink/cold-ui-ink/read').glob('*.attachment')).read_bytes() == next((corpus / 'cold-complex-01/read').glob('*.attachment')).read_bytes() |
| 62 | print('Passed: native edit/save/read cycle; Unicode edit preserves native formatting, positions, table, image, ink, and attachment bytes') |
| 63 | |
| 64 | assert ET.parse(corpus / 'toc-01/hierarchy.xml').getroot().get('color') == '#336699' |
| 65 | created = ET.parse(corpus / 'create-05/page-000.xml').getroot() |
| 66 | assert texts(created) == ['Created in Rust: café 東京 🦀'] |
| 67 | assert created.find('one:Outline', ns).get('author') == 'Fixture Author' |
| 68 | assert texts(ET.parse(corpus / 'create-05/returned-page.xml').getroot()) == ['Rust edited it again, with a longer paragraph.'] |
| 69 | assert texts(ET.parse(corpus / 'create-notebook-01/page-000.xml').getroot()) == ['A notebook created in Rust.'] |
| 70 | assert len(ET.parse(corpus / 'create-notebook-01/hierarchy.xml').getroot().findall('one:Section', ns)) == 1 |
| 71 | print('Passed: native TOC color edit, template-free section and notebook creation, native save, and longer Rust replacement') |
| 72 | |
| 73 | append = root / 'corpus/append/round-01' |
| 74 | for item in json.loads((append / 'manifest.json').read_text())['files']: |
| 75 | data = (append / item['path']).read_bytes() |
| 76 | assert len(data) == item['bytes'] and hashlib.sha256(data).hexdigest() == item['sha256'] |
| 77 | |
| 78 | expected = { |
| 79 | 'tx-17': 'Transaction 17', 'tx-255': 'Transaction 255', |
| 80 | 'tx-256': 'Transaction 256', 'tx-511': 'Transaction 256', |
| 81 | 'prepared-old': 'Transaction 255', 'tx-65535': 'Fictitious plain text.', |
| 82 | 'tx-65536': 'Crossed the second counter byte.', |
| 83 | 'tx-131071': 'Crossed the second counter byte.', |
| 84 | 'tx-130816': 'Crossed the second counter byte.', |
| 85 | 'shared-result': 'Rust committed through the SMB mount.', |
| 86 | **{f'metadata-{i}': 'Transaction 255' for i in range(8)}, |
| 87 | } |
| 88 | for name, text in expected.items(): |
| 89 | captured = pages(append / 'native' / f'append-01-{name}' / 'read') |
| 90 | assert len(captured) == 1 and texts(captured[0]) == [text], name |
| 91 | |
| 92 | after = pages(append / 'native/append-01-complex/read')[0] |
| 93 | assert fingerprint(normalized(before)) == fingerprint(normalized(after)) |
| 94 | assert next((root / 'corpus/native-ink/cold-ui-ink/read').glob('*.attachment')).read_bytes() == next((append / 'native/append-01-complex/read').glob('*.attachment')).read_bytes() |
| 95 | assert ET.parse(append / 'native/append-01-toc/read/hierarchy.xml').getroot().get('color') == '#336699' |
| 96 | saved = subprocess.check_output(['cargo', 'run', '-p', 'onestore', '--quiet', '--example', 'inventory', '--', str(append / 'native-after-511.one')], cwd=root, text=True) |
| 97 | assert [bytes.fromhex(row.split('\t')[2]).decode('ascii') for row in saved.splitlines() if row.startswith('ascii\t')] == ['Native after interrupted cleanup.'] |
| 98 | print('Passed: append encoding, native metadata and counter recovery, post-recovery native save, and filesystem-backed SMB commit') |