| 1 | import hashlib |
| 2 | import json |
| 3 | from pathlib import Path |
| 4 | import runpy |
| 5 | import shutil |
| 6 | from tempfile import TemporaryDirectory |
| 7 | import unittest |
| 8 | import xml.etree.ElementTree as ET |
| 9 | |
| 10 | from native_page_removal import CASES |
| 11 | from native_xml import ns |
| 12 | |
| 13 | ROOT = Path(__file__).resolve().parent.parent |
| 14 | FIXTURE = ROOT / 'corpus/page-lifecycle/removal' |
| 15 | compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] |
| 16 | |
| 17 | |
| 18 | class PageRemovalTest(unittest.TestCase): |
| 19 | def test_captures_match_requests_and_native_navigation(self): |
| 20 | provenance = json.loads((FIXTURE / 'provenance.json').read_text()) |
| 21 | self.assertEqual(provenance.keys(), CASES.keys()) |
| 22 | machines = set() |
| 23 | for case, (_, count, selected, permanent) in CASES.items(): |
| 24 | with self.subTest(case=case): |
| 25 | root = FIXTURE / case |
| 26 | request = json.loads((root / 'removal.json').read_text()) |
| 27 | self.assertEqual(request['selected'], selected) |
| 28 | self.assertEqual(request['permanent'], permanent) |
| 29 | for relative, digest in provenance[case]['sha256'].items(): |
| 30 | self.assertEqual(hashlib.sha256((root / relative).read_bytes()).hexdigest(), digest) |
| 31 | for phase in ['native', 'cold']: |
| 32 | machine = json.loads((root / phase / 'machine.json').read_text())['name'] |
| 33 | self.assertNotIn(machine, machines) |
| 34 | machines.add(machine) |
| 35 | self.assertTrue(json.loads((root / phase / 'teardown.json').read_text())['absent']) |
| 36 | before = ET.parse(root / 'native/before/hierarchy.xml').findall('.//one:Page', ns) |
| 37 | self.assertEqual(len(before), count) |
| 38 | expected = [(page.get('name'), page.get('pageLevel', '1')) |
| 39 | for index, page in enumerate(before) if index not in selected] |
| 40 | if expected: |
| 41 | expected[0] = (expected[0][0], '1') |
| 42 | for hierarchy in ['hierarchy.xml', 'reopened-hierarchy.xml']: |
| 43 | xml = (root / 'native/after' / hierarchy).read_text(encoding='utf-8-sig').strip() |
| 44 | pages = ET.fromstring(xml).findall('.//one:Page', ns) if xml not in ('', '<?xml version="1.0"?>') else [] |
| 45 | self.assertEqual([(page.get('name'), page.get('pageLevel', '1')) for page in pages], expected) |
| 46 | self.assertEqual(len(machines), 22) |
| 47 | |
| 48 | def test_native_xml_and_payloads_match_source_and_cold_stores(self): |
| 49 | for case in CASES: |
| 50 | for notebook, read in [('native/before/notebook', 'native/before-read'), |
| 51 | ('native/after/notebook', 'native/read'), |
| 52 | ('native/after/notebook', 'cold/read'), |
| 53 | ('cold/notebook', 'cold/read')]: |
| 54 | with self.subTest(case=case, notebook=notebook, read=read), TemporaryDirectory() as temporary: |
| 55 | destination = Path(temporary) / 'read' |
| 56 | shutil.copytree(FIXTURE / case / read, destination) |
| 57 | compare(FIXTURE / case / notebook, destination) |
| 58 | |
| 59 | |
| 60 | if __name__ == '__main__': |
| 61 | unittest.main() |
| 62 | |
| 63 | |
| 64 | class RustRemovalFollowupTest(unittest.TestCase): |
| 65 | """OneNote creates a page in a section after Rust removed pages from it, then reopens cold.""" |
| 66 | |
| 67 | CASES = ('parent', 'all', 'features', 'leading-parent') |
| 68 | |
| 69 | def test_native_page_after_rust_removal_matches_its_captures(self): |
| 70 | for case in self.CASES: |
| 71 | with self.subTest(case=case): |
| 72 | root = FIXTURE / 'rust-followup' / case |
| 73 | title = (root / 'followup/native-title.txt').read_text(encoding='utf-8-sig') |
| 74 | self.assertTrue(title.startswith('Native after Rust removal')) |
| 75 | for phase in ('followup', 'followup-cold'): |
| 76 | compare(root / phase / 'notebook', root / phase / 'read') |
| 77 | hierarchy = ET.parse(root / phase / 'read/hierarchy.xml').getroot() |
| 78 | names = [page.get('name') for page in hierarchy.findall('.//one:Page', ns)] |
| 79 | self.assertEqual(names.count(title), 1, names) |
| 80 | self.assertTrue(json.loads((root / phase / 'teardown.json').read_text())['absent']) |