import os import shutil import subprocess import tempfile import unittest from pathlib import Path class EngineBuildTests(unittest.TestCase): def setUp(self): self.temporary = tempfile.TemporaryDirectory() self.root = Path(self.temporary.name) engine = Path(__file__).resolve().parents[1] for name in ['engine-fingerprint.sh', 'ensure-engine.sh']: shutil.copyfile(engine / name, self.root / name) for name in ['engine.py', 'exports.py', 'requirements.txt', 'requirements.lock.txt', 'personal-expense-template.xlsx', 'helper.entitlements']: (self.root / name).write_text('fixture\n') self.binary = self.root / 'dist/receipt-engine-helper.app/Contents/MacOS/receipt-engine-helper' (self.root / 'build-engine.sh').write_text('''#!/bin/bash set -euo pipefail cd "$(dirname "$0")" echo build >> builds.log mkdir -p dist/receipt-engine-helper.app/Contents/MacOS printf 'fixture engine\\n' > dist/receipt-engine-helper.app/Contents/MacOS/receipt-engine-helper chmod +x dist/receipt-engine-helper.app/Contents/MacOS/receipt-engine-helper SOURCE_HASH="$(bash engine-fingerprint.sh)" BINARY_HASH="$(shasum -a 256 dist/receipt-engine-helper.app/Contents/MacOS/receipt-engine-helper | cut -d ' ' -f 1)" printf '%s\\n%s\\n' "$SOURCE_HASH" "$BINARY_HASH" > dist/receipt-engine-helper.source.sha256 ''') def tearDown(self): self.temporary.cleanup() def ensure(self): return subprocess.run(['bash', str(self.root / 'ensure-engine.sh')], capture_output=True, text=True, timeout=20) def build_count(self): return len((self.root / 'builds.log').read_text().splitlines()) def test_missing_engine_builds_and_unchanged_engine_is_reused(self): self.assertEqual(self.ensure().returncode, 0) self.assertEqual(self.build_count(), 1) self.assertEqual(self.ensure().returncode, 0) self.assertEqual(self.build_count(), 1) def test_changed_sources_rebuild_even_when_timestamp_is_unchanged(self): self.assertEqual(self.ensure().returncode, 0) source = self.root / 'exports.py' original = source.stat() source.write_text('new grouping implementation\n') os.utime(source, ns=(original.st_atime_ns, original.st_mtime_ns)) self.assertEqual(self.ensure().returncode, 0) self.assertEqual(self.build_count(), 2) def test_replaced_binary_rebuilds(self): self.assertEqual(self.ensure().returncode, 0) self.binary.write_text('old engine\n') self.assertEqual(self.ensure().returncode, 0) self.assertEqual(self.build_count(), 2) def test_missing_stamp_rebuilds(self): self.assertEqual(self.ensure().returncode, 0) (self.root / 'dist/receipt-engine-helper.source.sha256').unlink() self.assertEqual(self.ensure().returncode, 0) self.assertEqual(self.build_count(), 2) def test_failed_rebuild_does_not_accept_old_engine(self): self.assertEqual(self.ensure().returncode, 0) (self.root / 'build-engine.sh').write_text('exit 17\n') self.assertEqual(self.ensure().returncode, 17) if __name__ == '__main__': unittest.main()