| 1 | import json |
| 2 | from pathlib import Path |
| 3 | import runpy |
| 4 | import shutil |
| 5 | import subprocess |
| 6 | from tempfile import TemporaryDirectory |
| 7 | import unittest |
| 8 | |
| 9 | from document_model import EXPORTER, ordered_pages, view, walk |
| 10 | |
| 11 | ROOT = Path(__file__).resolve().parent.parent |
| 12 | FIXTURE = ROOT / 'corpus/page-lifecycle' |
| 13 | PHASES = ('01-blank', '02-authored', '03-renamed', '04-nested', '05-reordered', '06-deleted-parent') |
| 14 | compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] |
| 15 | |
| 16 | |
| 17 | class PageLifecycleTest(unittest.TestCase): |
| 18 | @classmethod |
| 19 | def setUpClass(cls): |
| 20 | temporary = TemporaryDirectory() |
| 21 | cls.addClassCleanup(temporary.cleanup) |
| 22 | cls.models = {} |
| 23 | for phase in PHASES: |
| 24 | for version in ('', 'cold'): |
| 25 | output = Path(temporary.name) / phase / (version or 'source') |
| 26 | output.parent.mkdir(parents=True, exist_ok=True) |
| 27 | subprocess.run([EXPORTER, FIXTURE / phase / version / 'notebook/Lifecycle.one', output], check=True) |
| 28 | cls.models[phase, version] = json.loads((output / 'document.json').read_text()) |
| 29 | |
| 30 | def test_each_native_phase_survives_an_independent_cold_reopen(self): |
| 31 | for phase, count in zip(PHASES, (3, 9, 9, 9, 9, 8), strict=True): |
| 32 | with self.subTest(phase=phase), TemporaryDirectory() as temporary: |
| 33 | source = Path(temporary) / 'notebook' |
| 34 | source.mkdir() |
| 35 | (source / 'Lifecycle.one').symlink_to(FIXTURE / phase / 'notebook/Lifecycle.one') |
| 36 | native = Path(temporary) / 'read' |
| 37 | cold = Path(temporary) / 'cold/read' |
| 38 | shutil.copytree(FIXTURE / phase / 'read', native) |
| 39 | shutil.copytree(FIXTURE / phase / 'cold/read', cold) |
| 40 | compare(source, native) |
| 41 | for version in ('', 'cold'): |
| 42 | compare(FIXTURE / phase / version / 'notebook', cold) |
| 43 | before = list(ordered_pages(self.models[phase, ''])) |
| 44 | after = list(ordered_pages(self.models[phase, 'cold'])) |
| 45 | self.assertEqual(len(before), count) |
| 46 | self.assertEqual(len(after), count) |
| 47 | for (sid, _, old, page), (new_sid, _, new, new_page) in zip(before, after, strict=True): |
| 48 | self.assertEqual((sid, page), (new_sid, new_page)) |
| 49 | self.assertEqual(dict(walk(old, page)), dict(walk(new, page))) |
| 50 | self.assertEqual(old['nodes'][old['roots']['2']]['kind'], new['nodes'][new['roots']['2']]['kind']) |
| 51 | groups = [] |
| 52 | for version in ('', 'cold'): |
| 53 | document = self.models[phase, version] |
| 54 | _, section = view(document, document['root']) |
| 55 | groups.append([section['nodes'][oid]['spaces'] |
| 56 | for oid in section['nodes'][section['roots']['1']]['children']]) |
| 57 | self.assertEqual(groups[0], groups[1]) |
| 58 | |
| 59 | def test_atomic_rust_nesting_matches_native_and_survives_cold_reopen(self): |
| 60 | fixture = FIXTURE / 'atomic-nesting' |
| 61 | self.assertEqual((fixture / 'candidate/Lifecycle.one').read_bytes()[1024:], |
| 62 | (fixture / 'cold/notebook/Lifecycle.one').read_bytes()[1024:]) |
| 63 | with TemporaryDirectory() as temporary: |
| 64 | native_read = Path(temporary) / 'native/read' |
| 65 | cold_read = Path(temporary) / 'cold/read' |
| 66 | shutil.copytree(FIXTURE / '04-nested/read', native_read) |
| 67 | shutil.copytree(fixture / 'cold/read', cold_read) |
| 68 | models = [] |
| 69 | for ordinal, source in enumerate((fixture / 'candidate', fixture / 'cold/notebook')): |
| 70 | output = Path(temporary) / str(ordinal) |
| 71 | subprocess.run([EXPORTER, source / 'Lifecycle.one', output], check=True) |
| 72 | models.append(json.loads((output / 'document.json').read_text())) |
| 73 | compare(source, native_read) |
| 74 | compare(source, cold_read) |
| 75 | pages = [list(ordered_pages(model)) for model in models] |
| 76 | expected = list(ordered_pages(self.models['04-nested', ''])) |
| 77 | original = list(ordered_pages(self.models['03-renamed', ''])) |
| 78 | self.assertEqual(len(pages[0]), 9) |
| 79 | for before, native, candidate, cold in zip(original, expected, *pages, strict=True): |
| 80 | sid, _, revision, page = candidate |
| 81 | self.assertEqual((sid, page), (before[0], before[3])) |
| 82 | self.assertEqual((sid, page), (cold[0], cold[3])) |
| 83 | self.assertEqual(dict(walk(revision, page)), dict(walk(before[2], before[3]))) |
| 84 | self.assertEqual(revision['nodes'], cold[2]['nodes']) |
| 85 | self.assertEqual(revision['roots'], cold[2]['roots']) |
| 86 | metadata = revision['nodes'][revision['roots']['2']]['kind'] |
| 87 | self.assertEqual(metadata, native[2]['nodes'][native[2]['roots']['2']]['kind']) |
| 88 | groups = [] |
| 89 | for model in (self.models['04-nested', ''], *models): |
| 90 | _, revision = view(model, model['root']) |
| 91 | groups.append([revision['nodes'][oid]['spaces'] |
| 92 | for oid in revision['nodes'][revision['roots']['1']]['children']]) |
| 93 | self.assertEqual(groups[0], groups[1]) |
| 94 | self.assertEqual(groups[1], groups[2]) |
| 95 | _, candidate = view(models[0], models[0]['root']) |
| 96 | _, cold = view(models[1], models[1]['root']) |
| 97 | self.assertEqual(candidate['nodes'], cold['nodes']) |
| 98 | self.assertEqual(candidate['roots'], cold['roots']) |
| 99 | |
| 100 | def test_native_page_styles_titles_order_and_parent_deletion(self): |
| 101 | phases = {phase: list(ordered_pages(self.models[phase, ''])) for phase in PHASES} |
| 102 | initial = [sid for sid, _, _, _ in phases['02-authored']] |
| 103 | for ordinal, (_, _, revision, page) in enumerate(phases['01-blank']): |
| 104 | self.assertEqual(revision['nodes'][page]['children'], []) |
| 105 | self.assertEqual(bool(revision['nodes'][page]['structure']), ordinal < 2) |
| 106 | titles = [''] * 3 + ['Same title', 'child', 'grandchild', 'Same title', 'trailing', 'Body automatic 🦀 é preserved'] |
| 107 | self.assertEqual([r['nodes'][r['roots']['2']]['kind']['title'] for _, _, r, _ in phases['02-authored']], titles) |
| 108 | titles[6:8] = ['Renamed 🦋 é', 'Body trailing 🦀 é preserved'] |
| 109 | self.assertEqual([r['nodes'][r['roots']['2']]['kind']['title'] for _, _, r, _ in phases['03-renamed']], titles) |
| 110 | for phase in ('03-renamed', '04-nested'): |
| 111 | self.assertEqual([sid for sid, _, _, _ in phases[phase]], initial) |
| 112 | for phase, order, levels, groups in [ |
| 113 | ('04-nested', list(range(9)), [1, 1, 1, 1, 2, 3, 1, 1, 1], [1, 1, 1, 3, 1, 1, 1]), |
| 114 | ('05-reordered', [0, 1, 2, 6, 3, 4, 5, 7, 8], [1, 1, 1, 1, 1, 2, 3, 1, 1], [1, 1, 1, 1, 3, 1, 1]), |
| 115 | ('06-deleted-parent', [0, 1, 2, 6, 4, 5, 7, 8], [1, 1, 1, 1, 2, 3, 1, 1], [1, 1, 1, 3, 1, 1]), |
| 116 | ]: |
| 117 | self.assertEqual([sid for sid, _, _, _ in phases[phase]], [initial[i] for i in order]) |
| 118 | self.assertEqual([r['nodes'][r['roots']['2']]['kind']['level'] for _, _, r, _ in phases[phase]], levels) |
| 119 | document = self.models[phase, ''] |
| 120 | _, section = view(document, document['root']) |
| 121 | series = section['nodes'][section['roots']['1']]['children'] |
| 122 | self.assertEqual([len(section['nodes'][oid]['spaces']) for oid in series], groups) |
| 123 | original = {sid: (r, page) for sid, _, r, page in phases['02-authored']} |
| 124 | for phase in PHASES[2:]: |
| 125 | for sid, _, revision, page in phases[phase]: |
| 126 | before, old_page = original[sid] |
| 127 | self.assertEqual(page, old_page) |
| 128 | self.assertEqual(before['nodes'][page]['children'], revision['nodes'][page]['children']) |
| 129 | for outline in before['nodes'][page]['children']: |
| 130 | self.assertEqual(dict(walk(before, outline)), dict(walk(revision, outline))) |
| 131 | before = {sid: (r, page) for sid, _, r, page in phases['03-renamed']} |
| 132 | for phase in ('04-nested', '05-reordered', '06-deleted-parent'): |
| 133 | for sid, _, revision, page in phases[phase]: |
| 134 | old, old_page = before[sid] |
| 135 | self.assertEqual(dict(walk(old, old_page)), dict(walk(revision, page))) |
| 136 | with TemporaryDirectory() as temporary: |
| 137 | output = Path(temporary) / 'deleted' |
| 138 | subprocess.run([EXPORTER, FIXTURE / '06-deleted-parent/notebook/OneNote_RecycleBin/OneNote_DeletedPages.one', output], check=True) |
| 139 | document = json.loads((output / 'document.json').read_text()) |
| 140 | (_, _, deleted, page), = ordered_pages(document) |
| 141 | _, _, parent, parent_page = phases['05-reordered'][4] |
| 142 | self.assertEqual(deleted['nodes'][deleted['roots']['2']]['kind']['title'], 'Same title') |
| 143 | self.assertEqual([node['kind']['text'] for _, node in walk(deleted, page) if node['kind']['type'] == 'RichText'], |
| 144 | [node['kind']['text'] for _, node in walk(parent, parent_page) if node['kind']['type'] == 'RichText']) |
| 145 | |
| 146 | |
| 147 | if __name__ == '__main__': |
| 148 | unittest.main() |