1from pathlib import Path
2import hashlib
3import json
4import runpy
5import shutil
6from tempfile import TemporaryDirectory
7import unittest
8import xml.etree.ElementTree as ET
9
10from native_xml import ns, texts
11
12ROOT = Path(__file__).resolve().parent.parent
13FIXTURE = ROOT / 'corpus/recording'
14compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare']
15
16
17def recorded(read):
18 """The page holding a recording, its playlist, and each paragraph's text or file with
19 the moment it links to and the recording it holds."""
20 page = next(page for page in (ET.parse(path).getroot() for path in sorted(read.glob('page-*.xml')))
21 if page.find('one:MediaPlaylist', ns) is not None)
22 playlist = [reference.get('mediaID') for reference in
23 page.findall('one:MediaPlaylist/one:MediaReference', ns)]
24 links = []
25 for element in page.findall('.//one:OE', ns):
26 index = element.find('one:MediaIndex', ns)
27 media = element.find('one:MediaFile', ns)
28 text = element.find('one:T', ns)
29 shown = '[%s]' % media.get('preferredName') if media is not None else texts(element)[0] if text is not None else ''
30 link = None
31 if index is not None:
32 link = (index.find('one:MediaReference', ns).get('mediaID'), int(index.get('timeIndex')))
33 links.append((shown, link, media.find('one:MediaReference', ns).get('mediaID') if media is not None else None))
34 return page, playlist, links
35
36
37def payload(read, name):
38 payloads = json.loads((read / 'payloads.json').read_text(encoding='utf-8-sig'))
39 entry, = (entry for entry in payloads if entry['name'] == name)
40 data = (read / (entry['sha256'] + '.attachment')).read_bytes()
41 assert hashlib.sha256(data).hexdigest() == entry['sha256']
42 return entry, data
43
44
45class RecordingTest(unittest.TestCase):
46 def cold(self, row):
47 read = FIXTURE / row / 'cold/read'
48 with TemporaryDirectory() as temporary:
49 copy = Path(temporary) / 'read'
50 shutil.copytree(read, copy)
51 compare((FIXTURE / row / 'candidate').resolve(), copy)
52 return read
53
54 def test_snowbound_s_recording_opens_in_onenote_with_its_line_links_and_audio(self):
55 read = self.cold('.')
56 _, playlist, links = recorded(read)
57 recording, = playlist
58 shown = [text for text, _, _ in links]
59 self.assertEqual(shown, ['Before recording', '[Recorded.wav]', '',
60 'Audio recording started: 5:18 PM Tuesday, September 29, 2026',
61 'First linked note', 'Second linked note'])
62 self.assertEqual(links[1][2], recording)
63 self.assertEqual([link for _, link, _ in links[1:4]], [(recording, 0), None, (recording, 0)])
64 first, second = (link[1] for _, link, _ in links[4:])
65 self.assertTrue(40 <= first < second)
66 _, data = payload(read, 'Recorded.wav')
67 # 4-bit IMA ADPCM (WAVE_FORMAT_DVI_ADPCM), mono 16 kHz, which OneNote plays
68 # (`played-from-note.png`).
69 self.assertEqual(data[:4] + data[8:16], b'RIFFWAVEfmt ')
70 self.assertEqual(int.from_bytes(data[20:22], 'little'), 0x11)
71 self.assertEqual(int.from_bytes(data[24:28], 'little'), 16000)
72
73 def test_snowbound_s_video_recording_opens_in_onenote_as_a_video_recording(self):
74 read = self.cold('video')
75 _, playlist, links = recorded(read)
76 recording, = playlist
77 shown = [text for text, _, _ in links]
78 self.assertEqual(shown, ['Before recording', '[Recorded.avi]', '',
79 'Video recording started: 5:18 PM Tuesday, September 29, 2026',
80 'First linked note', 'Second linked note'])
81 self.assertEqual(links[1][2], recording)
82 first, second = (link[1] for _, link, _ in links[4:])
83 self.assertTrue(40 <= first < second)
84 entry, data = payload(read, 'Recorded.avi')
85 self.assertEqual(entry['kind'], 'MediaFile')
86 # Motion JPEG and PCM in AVI, which OneNote plays (`video/played-from-note.png`).
87 self.assertEqual(data[:4] + data[8:12], b'RIFFAVI ')
88 self.assertIn(b'vidsMJPG', data[:1024])
89 self.assertIn(b'auds', data[:1024])
90
91 def test_onenote_lists_avi_mpg_and_wmv_files_as_recordings_but_not_mp4_or_mov(self):
92 page = ET.parse(FIXTURE / 'native/read/attached-video.xml').getroot()
93 kinds = {element.get('preferredName'): element.tag.split('}')[1]
94 for element in page.iter()
95 if element.tag.endswith(('}MediaFile', '}InsertedFile'))}
96 self.assertEqual(kinds['mjpeg-pcm.avi'], 'MediaFile')
97 self.assertEqual(kinds['mpeg1.mpg'], 'MediaFile')
98 self.assertEqual(kinds['wmv2.wmv'], 'MediaFile')
99 self.assertEqual(kinds['h264-aac.mp4'], 'InsertedFile')
100 self.assertEqual(kinds['h264-aac.mov'], 'InsertedFile')
101
102 def test_notes_written_while_onenote_s_recording_is_paused_are_not_linked(self):
103 page = ET.parse(FIXTURE / 'native/read/paused-while-recording.xml').getroot()
104 linked = {texts(element)[0]: element.find('one:MediaIndex', ns) is not None
105 for element in page.findall('.//one:OE', ns)
106 if element.find('one:T', ns) is not None and texts(element)[0]}
107 self.assertTrue(linked['Another note here'])
108 self.assertFalse(linked['Typed while paused'])
109 self.assertTrue(linked['After resuming'])
110
111 def test_onenote_s_recording_opens_as_recorded_after_snowbound_deletes_restores_and_links(self):
112 read = self.cold('edit')
113 _, playlist, links = recorded(read)
114 native = FIXTURE / 'native/read'
115 _, native_playlist, native_links = recorded(native)
116 self.assertEqual(playlist, native_playlist)
117 self.assertEqual(links[:len(native_links)], native_links)
118 recording, = playlist
119 self.assertEqual(links[len(native_links):], [('Linked by Snowbound', (recording, 25000), None)])
120 entry, data = payload(read, 'Recorded.wma')
121 self.assertEqual(entry['kind'], 'MediaFile')
122 self.assertTrue(data.startswith(bytes.fromhex('3026b2758e66cf11')))
123
124
125if __name__ == '__main__':
126 unittest.main()