#!/usr/bin/env python3 """Real addon handoff bytes, mutations and publication failures; no native engine.""" from contextlib import redirect_stdout import hashlib import io import json import os from pathlib import Path import shutil import sys import tempfile import unittest from unittest.mock import patch import zipfile sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools')) import source_bundle as bundle ROOT = Path(__file__).resolve().parents[1] class SourceBundleTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.home = Path(self.temp.name) self.root = self.home / 'source' shutil.copytree(ROOT / 'addons', self.root / 'addons') (self.root / 'docs').mkdir() for name in ('addons.json', 'vanilla-resources.json', 'acceptance.json'): shutil.copy2(ROOT / 'docs' / name, self.root / 'docs' / name) self.output = self.home / 'bundle.zip' def edit(self, path, old, new): p = self.root / path text = p.read_text() self.assertIn(old, text) p.write_text(text.replace(old, new)) def test_deterministic_bytes_ignore_source_mtime_and_mode(self): first, manifest = bundle.build(self.root) for p in (self.root / 'addons').rglob('*'): if p.is_file(): os.utime(p, (1700000000, 1700000000)) p.chmod(0o600) second, again = bundle.build(self.root) self.assertEqual(first, second) self.assertEqual(manifest, again) with zipfile.ZipFile(io.BytesIO(first)) as archive: self.assertEqual(archive.namelist(), sorted(archive.namelist())) self.assertEqual(set(archive.namelist()), set(manifest['files']) | {'BUNDLE-MANIFEST.json'}) self.assertEqual(json.loads(archive.read('BUNDLE-MANIFEST.json')), manifest) for name, digest in manifest['files'].items(): self.assertEqual(hashlib.sha256(archive.read(name)).hexdigest(), digest) self.assertTrue(all(info.date_time == (1980, 1, 1, 0, 0, 0) for info in archive.infolist())) self.assertFalse(manifest['native_build'] or manifest['release_package']) self.assertIn(b'DEVELOPMENT SOURCE BUNDLE', archive.read('BUNDLE-README.md')) self.assertEqual(set(manifest['projects']), set(bundle.PROJECTS)) def test_generated_vendor_original_profile_and_evidence_are_excluded(self): before, _ = bundle.build(self.root) for name in ('addons/core/cache/private.c', 'addons/core/vendor/asset.c', 'addons/ai/original/restricted.c', 'addons/core/.git/config', 'addons/core/evidence/raw.c', 'addons/core/runtime/account.c', 'addons/chat/profiles/profile.c', 'addons/core/.build/file.c', 'addons/core/resourceDatabase.rdb', 'addons/core/packed.pak', 'addons/core/runtime.log', 'docs/evidence/private.log'): p = self.root / name; p.parent.mkdir(parents=True, exist_ok=True); p.write_text('DO NOT COPY') after, _ = bundle.build(self.root) self.assertEqual(before, after) def test_changed_resource_changes_archive_and_manifest(self): first, manifest = bundle.build(self.root) name = 'addons/core/Scripts/Game/ERR/ERR_Actors.c' p = self.root / name; p.write_text(p.read_text() + '\n// reviewed successor edit\n') second, updated = bundle.build(self.root) self.assertNotEqual(first, second) self.assertNotEqual(manifest['files'][name], updated['files'][name]) def test_identity_and_external_dependency_refused(self): name = 'addons/core/addon.gproj' self.edit(name, '2528466DE220D91C', '1111111111111111') with self.assertRaisesRegex(ValueError, 'identity/dependency'): bundle.build(self.root) self.edit(name, '1111111111111111', '2528466DE220D91C') self.edit(name, '58D0FB3206B6F859', '5F16D7E4A1CBE075') with self.assertRaisesRegex(ValueError, 'identity/dependency'): bundle.build(self.root) def test_missing_transitive_project_refused(self): shutil.rmtree(self.root / 'addons/intel') with self.assertRaisesRegex(ValueError, 'identity/dependency'): bundle.build(self.root) def test_resource_dependency_outside_closure_refused(self): p = self.root / 'addons/core/Scripts/Game/ERR/BadReference.c' meta = next((self.root / 'addons/map').rglob('*.meta')).read_text() import re resource = re.search(r'Name "([^"\n]+)"', meta)[1] p.write_text('class ERR_Test { ResourceName asset = "' + resource + '"; }') with self.assertRaisesRegex(ValueError, 'dependency closure'): bundle.build(self.root) def test_metadata_and_unknown_asset_format_refused(self): (self.root / 'addons/core/Prefabs/ERR/Unknown.xob').write_bytes(b'vendor') with self.assertRaisesRegex(ValueError, 'unreviewed'): bundle.build(self.root) (self.root / 'addons/core/Prefabs/ERR/Unknown.xob').unlink() next((self.root / 'addons/core').rglob('*.et.meta')).unlink() with self.assertRaisesRegex(ValueError, 'metadata|unresolved'): bundle.build(self.root) def test_source_symlink_and_nonregular_refused(self): p = self.root / 'addons/core/Scripts/Game/ERR/Link.c' p.symlink_to('/etc/hostname') with self.assertRaisesRegex(ValueError, 'symlink'): bundle.build(self.root) p.unlink(); os.mkfifo(p) with self.assertRaisesRegex(ValueError, 'nonregular'): bundle.build(self.root) def test_unsafe_or_case_colliding_member_refused(self): p = self.root / 'addons/core/Scripts/Game/ERR/escape\\outside.c'; p.write_text('') with self.assertRaisesRegex(ValueError, 'unsafe'): bundle.build(self.root) p.unlink() (self.root / 'addons/core/Scripts/Game/ERR/err_actors.c').write_text('') with self.assertRaisesRegex(ValueError, 'collision'): bundle.build(self.root) def test_validation_failure_with_output_argument_leaves_no_archive(self): self.edit('addons/core/addon.gproj', '2528466DE220D91C', '1111111111111111') real_build = bundle.build with patch.object(bundle, 'build', side_effect=lambda **kw: real_build(self.root, **kw)), redirect_stdout(io.StringIO()): self.assertEqual(bundle.main(['--output', str(self.output)]), 1) self.assertFalse(self.output.exists()) self.assertEqual(list(self.home.glob('.erri-bundle-*')), []) def test_changed_tree_during_collection_refused(self): real_inventory = bundle.inventory calls = 0 def changing_inventory(root): nonlocal calls calls += 1 if calls == 2: (root / 'addons/core/Scripts/Game/ERR/Late.c').write_text('// late addition') return real_inventory(root) with patch.object(bundle, 'inventory', side_effect=changing_inventory): with self.assertRaisesRegex(ValueError, 'tree changed'): bundle.build(self.root) self.assertFalse(self.output.exists()) def test_atomic_new_output_no_overwrite_or_symlink(self): content, _ = bundle.build(self.root) bundle.write_new(self.output, content, self.root) self.assertEqual(self.output.read_bytes(), content) with self.assertRaises(ValueError): bundle.write_new(self.output, b'bad', self.root) self.assertEqual(self.output.read_bytes(), content) link = self.home / 'link.zip'; link.symlink_to(self.output) with self.assertRaises(ValueError): bundle.write_new(link, b'bad', self.root) self.assertEqual(self.output.read_bytes(), content) def test_publication_failure_removes_temporary_and_leaves_no_output(self): with patch.object(bundle.os, 'link', side_effect=OSError('publication failure')): with self.assertRaises(OSError): bundle.write_new(self.output, b'bytes', self.root) self.assertFalse(self.output.exists()) self.assertEqual(list(self.home.glob('.erri-bundle-*')), []) def test_default_preview_creates_nothing_and_release_uses_real_pending_audit(self): real_build = bundle.build with patch.object(bundle, 'build', side_effect=lambda **kw: real_build(self.root, **kw)), redirect_stdout(io.StringIO()) as out: self.assertEqual(bundle.main([]), 0) self.assertTrue(json.loads(out.getvalue())['preview']) self.assertFalse(self.output.exists()) with patch.object(bundle, 'build', side_effect=lambda **kw: real_build(self.root, **kw)), redirect_stdout(io.StringIO()) as out: self.assertEqual(bundle.main(['--mode', 'release', '--output', str(self.output)]), 1) self.assertIn('cases.twelve_players: status is pending', out.getvalue()) self.assertFalse(self.output.exists()) self.assertEqual(list(self.home.glob('.erri-bundle-*')), []) if __name__ == '__main__': unittest.main()