1"""Page versions (corpus/page-versions): what OneNote 2010 stores for them and does to them,
2and that it reads, lists and deletes the ones Rust writes."""
3import hashlib
4import json
5from pathlib import Path
6import runpy
7import shutil
8import subprocess
9from tempfile import TemporaryDirectory
10import unittest
11
12from document_model import EXPORTER, ordered_pages, version_pages, walk
13
14ROOT = Path(__file__).resolve().parent.parent
15FIXTURE = ROOT / 'corpus/page-versions'
16compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare']
17HISTORY = '{7111497F-1B6B-4209-9491-C98B04CF4C5A},1'
18HAS_VERSION_PAGES = 0x88003462
19
20
21def model(notebook):
22 with TemporaryDirectory() as temporary:
23 output = Path(temporary) / 'model'
24 subprocess.run([EXPORTER, notebook / 'History.one', output], check=True, capture_output=True)
25 return json.loads((output / 'document.json').read_text())
26
27
28# The title's date and time fields, the same on every state of the page.
29DATE = {'Sunday, September 27, 2026', '5:42 PM'}
30
31
32def texts(space, root):
33 return [node['kind']['text'] for _, node in walk(space, root)
34 if node['kind']['type'] == 'RichText' and node['kind']['text'] and node['kind']['text'] not in DATE]
35
36
37def versions(document):
38 """Each listed page's text and versions, newest first: each version's author and text,
39 and whether its metadata says it has versions."""
40 listed = []
41 for sid, _, page, oid in ordered_pages(document):
42 own = []
43 history = document['spaces'][sid]['revisions'].get(document['spaces'][sid]['contexts'].get(HISTORY))
44 for _, _, revision, version, proxy in version_pages(document, sid, page):
45 author = history['nodes'][proxy['latest_author']]['kind']['name']
46 own.append((author, texts(revision, version)))
47 metadata = page['nodes'][page['roots']['2']]
48 flagged = any(field['id'] == HAS_VERSION_PAGES for fields in metadata['extra'] for field in fields)
49 listed.append((texts(page, oid), own, flagged))
50 return listed
51
52
53FIRST = 'First state. Typed by virtual.'
54SECOND = 'Second author line.'
55
56
57class PageVersionsTest(unittest.TestCase):
58 def test_onenote_keeps_a_version_when_another_author_edits(self):
59 native = FIXTURE / 'native'
60 self.assertEqual(versions(model(native / 'step-01/notebook')),
61 [(['Versioned', FIRST], [], False)])
62 page = ['Versioned', FIRST, SECOND]
63 self.assertEqual(versions(model(native / 'step-02/notebook')),
64 [(page, [('virtual', ['Versioned', FIRST])], True)])
65 # Copy Page To copies the version as a page of its own, without versions.
66 self.assertEqual(versions(model(native / 'step-03/notebook'))[1],
67 (['Versioned', FIRST], [], False))
68 # Restore Version: the page as it stood becomes the newest version.
69 self.assertEqual(versions(model(native / 'step-04/notebook'))[0],
70 (['Versioned', FIRST], [('Other Person', page), ('virtual', ['Versioned', FIRST])], True))
71 # Delete Version, then Delete All Versions in Section, which clears the flag.
72 self.assertEqual(versions(model(native / 'step-05/notebook'))[0][1], [('virtual', ['Versioned', FIRST])])
73 self.assertEqual(versions(model(native / 'step-06/notebook'))[0], (['Versioned', FIRST], [], False))
74 # Optimizing keeps versions.
75 self.assertEqual(len(versions(model(native / 'step-10/notebook'))[0][1]), 1)
76
77 def test_the_scripted_capture_repeats_it(self):
78 scripted = FIXTURE / 'native-scripted'
79 counts = [len(versions(model(scripted / f'step-{n:02}/notebook'))[0][1]) for n in range(1, 10)]
80 self.assertEqual(counts, [0, 1, 1, 2, 1, 0, 1, 0, 1])
81
82 def test_onenote_reads_a_rust_restore_and_deletes_its_version(self):
83 for name in ('restore', 'delete-all'):
84 source = json.loads((FIXTURE / f'cold-{name}/source.json').read_text())
85 written = hashlib.sha256((FIXTURE / f'candidate-{name}/History.one').read_bytes()).hexdigest()
86 self.assertIn(written, [item['sha256'] for item in source])
87 with TemporaryDirectory() as temporary:
88 read = Path(temporary) / 'read'
89 shutil.copytree(FIXTURE / f'cold-{name}/before-read', read)
90 compare(FIXTURE / f'candidate-{name}', read)
91 restored = versions(model(FIXTURE / 'candidate-restore'))[0]
92 self.assertEqual(restored[0], ['Versioned', FIRST])
93 self.assertEqual([author for author, _ in restored[1]], ['Other Person', 'virtual'])
94 self.assertEqual(versions(model(FIXTURE / 'candidate-delete-all'))[0], (['Versioned', FIRST], [], False))
95 # OneNote listed the Rust versions under the page, opened the newest under its bar and
96 # deleted it through the bar's menu, leaving the older one.
97 record = lambda name: json.loads((FIXTURE / f'cold-restore/shots/{name}.json').read_text())
98 self.assertEqual(record('cold-version')['win']['title'], 'Versioned - Microsoft OneNote')
99 after = versions(model(FIXTURE / 'cold-restore/notebook'))[0]
100 self.assertEqual([author for author, _ in after[1]], ['virtual'])
101
102if __name__ == '__main__':
103 unittest.main()