1import json
2from pathlib import Path
3import runpy
4import shutil
5import subprocess
6from tempfile import TemporaryDirectory
7import unittest
8
9from document_model import EXPORTER, ordered_pages, view, walk
10
11ROOT = Path(__file__).resolve().parent.parent
12FIXTURE = ROOT / 'corpus/page-lifecycle'
13PHASES = ('01-blank', '02-authored', '03-renamed', '04-nested', '05-reordered', '06-deleted-parent')
14compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare']
15
16
17class 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
147if __name__ == '__main__':
148 unittest.main()