| 1 | """Page versions (corpus/page-versions): what OneNote 2010 stores for them and does to them, |
| 2 | and that it reads, lists and deletes the ones Rust writes.""" |
| 3 | import hashlib |
| 4 | import json |
| 5 | from pathlib import Path |
| 6 | import runpy |
| 7 | import shutil |
| 8 | import subprocess |
| 9 | from tempfile import TemporaryDirectory |
| 10 | import unittest |
| 11 | |
| 12 | from document_model import EXPORTER, ordered_pages, version_pages, walk |
| 13 | |
| 14 | ROOT = Path(__file__).resolve().parent.parent |
| 15 | FIXTURE = ROOT / 'corpus/page-versions' |
| 16 | compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] |
| 17 | HISTORY = '{7111497F-1B6B-4209-9491-C98B04CF4C5A},1' |
| 18 | HAS_VERSION_PAGES = 0x88003462 |
| 19 | |
| 20 | |
| 21 | def 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. |
| 29 | DATE = {'Sunday, September 27, 2026', '5:42 PM'} |
| 30 | |
| 31 | |
| 32 | def 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 | |
| 37 | def 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 | |
| 53 | FIRST = 'First state. Typed by virtual.' |
| 54 | SECOND = 'Second author line.' |
| 55 | |
| 56 | |
| 57 | class 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 | |
| 102 | if __name__ == '__main__': |
| 103 | unittest.main() |