| 1 | import base64 |
| 2 | from datetime import datetime, timezone |
| 3 | import hashlib |
| 4 | from pathlib import Path |
| 5 | import runpy |
| 6 | import tempfile |
| 7 | import unittest |
| 8 | |
| 9 | release = runpy.run_path(str(Path(__file__).resolve().parent / 'release.py')) |
| 10 | |
| 11 | |
| 12 | def utc(text): |
| 13 | return datetime.fromisoformat(text).replace(tzinfo=timezone.utc) |
| 14 | |
| 15 | |
| 16 | class ReleaseTest(unittest.TestCase): |
| 17 | def test_a_build_counts_the_commits_of_its_day_in_los_angeles(self): |
| 18 | # 07:34 UTC on the 29th is 00:34 in Los Angeles; 05:00 UTC on the 30th is 22:00 on the 29th. |
| 19 | commits = [utc('2026-09-30T05:00:00'), utc('2026-09-29T19:23:00'), utc('2026-09-29T07:34:00'), |
| 20 | utc('2026-09-29T06:29:00'), utc('2026-09-28T12:43:00')] |
| 21 | self.assertEqual(release['derive'](commits[0], commits), ('2026-09-29', 3)) |
| 22 | self.assertEqual(release['derive'](commits[3], commits[3:]), ('2026-09-28', 2)) |
| 23 | self.assertEqual(release['name'](('2026-09-29', 10)), '2026-09-29-r10') |
| 24 | self.assertEqual(release['parse']('2026-09-29-r10'), ('2026-09-29', 10)) |
| 25 | self.assertEqual(release['folder'](('2026-09-29', 10)), '2026-09-29.r10') |
| 26 | |
| 27 | def test_a_commit_counts_once_by_its_prefix_or_once_per_bullet(self): |
| 28 | entries = release['entries'] |
| 29 | self.assertEqual(entries('fix(update): accept an archive of exactly its size\n\nureq refuses.\n'), |
| 30 | [('fix', 'Accept an archive of exactly its size')]) |
| 31 | self.assertEqual(entries('docs: center the row'), [('other', 'Center the row')]) |
| 32 | self.assertEqual(entries('Initial import'), [('other', 'Initial import')]) |
| 33 | self.assertEqual(entries('feat!: macOS icon'), [('feature', 'macOS icon')]) |
| 34 | self.assertEqual( |
| 35 | entries('feat: icon, builds\n\nIntro.\n\n- macOS ships an icon so Tahoe\n shows it.\n' |
| 36 | '* pinch zoom.\n\nAssisted-by: claude-opus-5.5\n'), |
| 37 | [('feature', 'macOS ships an icon so Tahoe shows it'), ('feature', 'Pinch zoom')]) |
| 38 | |
| 39 | def test_changes_start_after_the_build_before_and_carry_their_commits_versions(self): |
| 40 | commits = {'a': ([], utc('2026-09-30T10:00:00'), 'fix: published first'), |
| 41 | 'b': (['a'], utc('2026-09-30T11:00:00'), 'feat: pinch zoom'), |
| 42 | 'c': (['b'], utc('2026-09-30T12:00:00'), 'fix: two\n\n- one\n- two\n')} |
| 43 | self.assertEqual(release['changes'](commits, 'c', 'a'), [ |
| 44 | {'version': '2026-09-30-r2', 'kind': 'feature', 'title': 'Pinch zoom'}, |
| 45 | {'version': '2026-09-30-r3', 'kind': 'fix', 'title': 'One'}, |
| 46 | {'version': '2026-09-30-r3', 'kind': 'fix', 'title': 'Two'}]) |
| 47 | self.assertEqual(release['changes'](commits, 'c', 'b'), [ |
| 48 | {'version': '2026-09-30-r3', 'kind': 'fix', 'title': 'One'}, |
| 49 | {'version': '2026-09-30-r3', 'kind': 'fix', 'title': 'Two'}]) |
| 50 | |
| 51 | def test_builds_are_the_published_folders_oldest_first(self): |
| 52 | with tempfile.TemporaryDirectory() as published: |
| 53 | for entry in ['2026-10-02.r34', '2026-09-30.r2', '2026-10-02.r7', '.2026-10-03.r1.partial', 'latest']: |
| 54 | (Path(published) / entry).mkdir() |
| 55 | (Path(published) / 'latest.json').touch() |
| 56 | self.assertEqual(release['builds'](Path(published)), |
| 57 | [('2026-09-30', 2), ('2026-10-02', 7), ('2026-10-02', 34)]) |
| 58 | |
| 59 | def test_latest_only_moves_forward(self): |
| 60 | latest = {'macos-aarch64': '2026-09-29-r9', 'linux-x86_64': '2026-09-30-r1'} |
| 61 | moved = release['newest'](latest, {'macos-aarch64': {}, 'linux-x86_64': {}, 'macos-10.6': {}}, |
| 62 | ('2026-09-29', 10)) |
| 63 | self.assertEqual(moved, {'macos-aarch64': '2026-09-29-r10', 'linux-x86_64': '2026-09-30-r1', |
| 64 | 'macos-10.6': '2026-09-29-r10'}) |
| 65 | self.assertEqual(release['newest'](latest, {'macos-aarch64': {}}, ('2026-09-29', 9)), latest) |
| 66 | |
| 67 | def test_minisig_signs_the_files_blake2b_then_that_with_its_comment(self): |
| 68 | minisign, scope = release['minisign'], release['minisign'].__globals__ |
| 69 | messages = [] |
| 70 | |
| 71 | def sign(paths): |
| 72 | messages.extend(Path(path).read_bytes() for path in paths) |
| 73 | return [bytes([len(messages)]).hex() * 64 for _ in paths] |
| 74 | |
| 75 | real, scope['sign'] = scope['sign'], sign |
| 76 | try: |
| 77 | with tempfile.TemporaryDirectory() as folder: |
| 78 | file = Path(folder) / 'snowbound-linux-x86_64' |
| 79 | file.write_bytes(b'an executable') |
| 80 | minisign([file]) |
| 81 | untrusted, signature, trusted, global_signature = Path(f'{file}.minisig').read_text().splitlines() |
| 82 | finally: |
| 83 | scope['sign'] = real |
| 84 | key_id = base64.b64decode(release['MINISIGN'].read_text().splitlines()[1])[2:10] |
| 85 | self.assertTrue(untrusted.startswith('untrusted comment: ')) |
| 86 | self.assertEqual(base64.b64decode(signature), b'ED' + key_id + bytes([1]) * 64) |
| 87 | self.assertRegex(trusted, r'^trusted comment: timestamp:\d+\tfile:snowbound-linux-x86_64\thashed$') |
| 88 | self.assertEqual(base64.b64decode(global_signature), bytes([2]) * 64) |
| 89 | self.assertEqual(messages, [hashlib.blake2b(b'an executable').digest(), |
| 90 | bytes([1]) * 64 + trusted.removeprefix('trusted comment: ').encode()]) |
| 91 | |
| 92 | def test_build_macos_signs_each_mac_app(self): |
| 93 | build_mac, scope = release['build_mac'], release['build_mac'].__globals__ |
| 94 | commands = [] |
| 95 | |
| 96 | def record(command, **_): |
| 97 | commands.append(list(map(str, command))) |
| 98 | if command[0] == 'ditto': |
| 99 | Path(command[-1]).touch() |
| 100 | |
| 101 | run, scope['run'] = scope['run'], record |
| 102 | try: |
| 103 | def signing(platform, developer_id, notarize): |
| 104 | commands.clear() |
| 105 | with tempfile.TemporaryDirectory() as stage: |
| 106 | build_mac(platform, Path(stage), developer_id, notarize, Path(stage) / 'symbols.zip') |
| 107 | build = commands[0] |
| 108 | return (build[build.index(f'{stage}/Snowbound.dSYM') + 1:], |
| 109 | [command[1] for command in commands[1:]]) |
| 110 | |
| 111 | self.assertEqual(signing('macos-aarch64', True, ['--key-id', 'K']), |
| 112 | (['--sign', 'developer-id', '--sign-identity', release['IDENTITY'], '--arch', 'aarch64'], |
| 113 | ['-c', '-c', 'notarytool', 'stapler', '-c'])) |
| 114 | self.assertEqual(signing('macos-x86_64', True, ['--key-id', 'K']), |
| 115 | (['--sign', 'developer-id', '--sign-identity', release['IDENTITY'], '--arch', 'x86_64'], |
| 116 | ['-c', '-c', 'notarytool', 'stapler', '-c'])) |
| 117 | self.assertEqual(signing('macos-aarch64', False, None), (['--sign', 'ad-hoc', '--arch', 'aarch64'], ['-c', '-c'])) |
| 118 | self.assertEqual(signing('macos-10.6', True, ['--key-id', 'K']), (['--snow-leopard'], ['-c', '-c'])) |
| 119 | finally: |
| 120 | scope['run'] = run |
| 121 | |
| 122 | |
| 123 | if __name__ == '__main__': |
| 124 | unittest.main() |