File
Blob: scripts/test_flash_backup.py
| 1 | """Backup integrity and failure behavior without serial I/O.""" |
| 2 | import json |
| 3 | from pathlib import Path |
| 4 | import subprocess |
| 5 | import tempfile |
| 6 | import unittest |
| 7 | from unittest.mock import patch |
| 8 | |
| 9 | import flash_backup as backup |
| 10 | |
| 11 | |
| 12 | class BackupTests(unittest.TestCase): |
| 13 | def setUp(self): |
| 14 | self.temporary = tempfile.TemporaryDirectory() |
| 15 | self.addCleanup(self.temporary.cleanup) |
| 16 | self.root = Path(self.temporary.name) |
| 17 | self.size = patch.object(backup, "FLASH_BYTES", 64) |
| 18 | self.size.start() |
| 19 | self.addCleanup(self.size.stop) |
| 20 | |
| 21 | def run_tool(self, command, **kwargs): |
| 22 | self.assertTrue(kwargs["check"]) |
| 23 | self.assertNotIn("write-flash", command) |
| 24 | self.assertNotIn("erase-flash", command) |
| 25 | if "read-flash" in command: |
| 26 | Path(command[-1]).write_bytes(b"x" * 64) |
| 27 | |
| 28 | def test_verified_image_is_preserved_and_rechecked(self): |
| 29 | image = backup.create_backup(self.root, "test-port", self.run_tool) |
| 30 | self.assertEqual(backup.require_backup(self.root), image) |
| 31 | self.assertEqual(image.stat().st_mode & 0o777, 0o600) |
| 32 | record = self.root / backup.RECORD_NAME |
| 33 | self.assertEqual(record.stat().st_mode & 0o777, 0o600) |
| 34 | self.assertTrue(json.loads(record.read_text())["verified_against_device_flash"]) |
| 35 | with self.assertRaisesRegex(ValueError, "already exists"): |
| 36 | backup.create_backup(self.root, "another-port", lambda *a, **k: self.fail("Must not contact hardware")) |
| 37 | image.write_bytes(b"y" * 64) |
| 38 | with self.assertRaisesRegex(ValueError, "No valid saved backup"): |
| 39 | backup.require_backup(self.root) |
| 40 | |
| 41 | def test_failed_verification_publishes_no_backup(self): |
| 42 | def failing(command, **kwargs): |
| 43 | self.run_tool(command, **kwargs) |
| 44 | if "verify-flash" in command: |
| 45 | raise subprocess.CalledProcessError(1, command) |
| 46 | with self.assertRaises(subprocess.CalledProcessError): |
| 47 | backup.create_backup(self.root, "test-port", failing) |
| 48 | self.assertFalse((self.root / backup.IMAGE_NAME).exists()) |
| 49 | self.assertFalse((self.root / backup.RECORD_NAME).exists()) |
| 50 | |
| 51 | def test_partial_read_is_not_verified(self): |
| 52 | def partial(command, **kwargs): |
| 53 | self.assertIn("read-flash", command) |
| 54 | Path(command[-1]).write_bytes(b"short") |
| 55 | with self.assertRaisesRegex(ValueError, "incomplete"): |
| 56 | backup.create_backup(self.root, "test-port", partial) |
| 57 | self.assertFalse((self.root / backup.RECORD_NAME).exists()) |
| 58 | |
| 59 | def test_missing_or_malformed_record_is_rejected(self): |
| 60 | for contents in [None, "not json", "[]", '{"verified_against_device_flash": true}']: |
| 61 | if contents is not None: |
| 62 | (self.root / backup.RECORD_NAME).write_text(contents) |
| 63 | with self.assertRaisesRegex(ValueError, "No valid saved backup"): |
| 64 | backup.require_backup(self.root) |
| 65 | |
| 66 | |
| 67 | if __name__ == "__main__": |
| 68 | unittest.main() |