"""Exercise release pinning, drift detection, replacement and recovery without network.""" import importlib.util import json from pathlib import Path import shutil import subprocess import tempfile import unittest HOME = Path(__file__).resolve().parents[1] spec = importlib.util.spec_from_file_location('bootstrap', HOME / 'bootstrap.py') bootstrap = importlib.util.module_from_spec(spec) spec.loader.exec_module(bootstrap) class BootstrapTests(unittest.TestCase): def setUp(self): self.temporary = tempfile.TemporaryDirectory() self.addCleanup(self.temporary.cleanup) self.root = Path(self.temporary.name) self.source = self.root / 'engineering' self.source.mkdir() self.git('init', '-q') self.git('config', 'user.email', 'test@example.invalid') self.git('config', 'user.name', 'Engineering test') (self.source / 'resources').mkdir() (self.source / 'resources/tool').write_text('version one\n') self.commit = self.release('v1.0.0') self.project = self.root / 'consumer' self.project.mkdir() self.lock = {'repository': str(self.source), 'tag': 'v1.0.0', 'commit': self.commit} self.save_lock() def git(self, *args): return subprocess.check_output(['git', '-C', str(self.source), *args], text=True, stderr=subprocess.DEVNULL).strip() def release(self, tag): self.git('add', '.') self.git('commit', '-qm', tag) self.git('tag', tag) return self.git('rev-parse', 'HEAD') def save_lock(self): (self.project / 'engineering.lock.json').write_text(json.dumps(self.lock)) def test_absent_generated_and_repeat_are_deterministic(self): bootstrap.materialize(self.project) first = bootstrap.files(self.project / 'generated') bootstrap.materialize(self.project) self.assertEqual(first, bootstrap.files(self.project / 'generated')) bootstrap.materialize(self.project, True) def test_drift_includes_modified_added_and_missing_files(self): bootstrap.materialize(self.project) target = self.project / 'generated/tool' for operation in ('modify', 'remove', 'add'): with self.subTest(operation=operation): bootstrap.materialize(self.project) if operation == 'modify': target.write_text('hand edit') elif operation == 'remove': target.unlink() else: (target.parent / 'extra').write_text('untracked') before = bootstrap.files(target.parent) with self.assertRaisesRegex(ValueError, 'differ'): bootstrap.materialize(self.project, True) self.assertEqual(before, bootstrap.files(target.parent)) def test_update_removes_obsolete_files_and_preserves_project_files(self): bootstrap.materialize(self.project) (self.project / 'pom.xml').write_text('owned by project') (self.source / 'resources/tool').unlink() (self.source / 'resources/new').write_text('new release') self.lock.update(tag='v1.1.0', commit=self.release('v1.1.0')) self.save_lock() bootstrap.materialize(self.project) self.assertFalse((self.project / 'generated/tool').exists()) self.assertEqual('owned by project', (self.project / 'pom.xml').read_text()) def test_moved_tag_fails_before_replacing_existing_content(self): bootstrap.materialize(self.project) before = bootstrap.files(self.project / 'generated') (self.source / 'resources/tool').write_text('different commit') self.release('v1.1.0') self.git('tag', '-f', 'v1.0.0') with self.assertRaisesRegex(ValueError, 'tag moved'): bootstrap.materialize(self.project) self.assertEqual(before, bootstrap.files(self.project / 'generated')) def test_symlink_output_is_rejected(self): (self.project / 'generated').symlink_to(self.source / 'resources', target_is_directory=True) with self.assertRaisesRegex(ValueError, 'symlink'): bootstrap.materialize(self.project) if __name__ == '__main__': unittest.main()