Files
EscapistsReintroduced/tests/test_source_bundle.py
T
jakub cab998fb64 Combine Workshop addon and fix prison startup
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.
2026-09-20 16:42:52 +02:00

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()