Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
172 lines
8.9 KiB
Python
172 lines
8.9 KiB
Python
#!/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', 'external-addons.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/00_Core/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_project_refused(self):
|
|
(self.root / 'addons/core/addon.gproj').unlink()
|
|
with self.assertRaisesRegex(ValueError, 'identity/dependency'): bundle.build(self.root)
|
|
|
|
def test_internal_module_resource_is_visible(self):
|
|
p = self.root / 'addons/core/Scripts/Game/ERR/00_Core/BadReference.c'
|
|
meta = next((self.root / 'addons/core').rglob('*.meta')).read_text()
|
|
import re
|
|
resource = re.search(r'Name "([^"\n]+)"', meta)[1]
|
|
p.write_text('class ERR_Test { ResourceName asset = "' + resource + '"; }')
|
|
bundle.build(self.root) # Internal modules share the same resource namespace.
|
|
|
|
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/00_Core/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/00_Core/escape\\outside.c'; p.write_text('')
|
|
with self.assertRaisesRegex(ValueError, 'unsafe'): bundle.build(self.root)
|
|
p.unlink()
|
|
(self.root / 'addons/core/Scripts/Game/ERR/00_Core/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/00_Core/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()
|