Files

219 lines
12 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_atlas_model_requires_source_and_imported_resource(self):
bundle.build(self.root)
(self.root / bundle.ATLAS_ROOT / 'ERR_Atlas.fbx').unlink()
with self.assertRaisesRegex(ValueError, 'atlas model lacks source/runtime/metadata'):
bundle.build(self.root)
def test_atlas_binary_admission_does_not_allow_unrelated_models(self):
folder = self.root / bundle.ATLAS_ROOT
(folder / 'ERR_Atlas.xob').write_bytes(b'not a compiled mesh')
with self.assertRaisesRegex(ValueError, 'invalid atlas model signature'):
bundle.build(self.root)
(folder / 'Unreviewed.xob').write_bytes(b'FORMxxxxXOB9')
with self.assertRaisesRegex(ValueError, 'unreviewed addon source/resource format'):
bundle.build(self.root)
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_character_action_patch_is_the_only_allowed_vanilla_override(self):
_, manifest = bundle.build(self.root)
patch = 'addons/core/Prefabs/Characters/Core/Character_Base.et'
self.assertIn(patch, manifest['files'])
# The exception cannot be moved under a different resource path.
changed = 'addons/core/Prefabs/Characters/Core/Unreviewed.et'
(self.root / patch).rename(self.root / changed)
(self.root / (patch + '.meta')).rename(self.root / (changed + '.meta'))
self.edit(changed + '.meta', 'Character_Base.et', 'Unreviewed.et')
with self.assertRaisesRegex(ValueError, 'reuses a vanilla identity'):
bundle.build(self.root)
# Nor can the allowlisted path take an unrelated vanilla resource GUID.
(self.root / changed).rename(self.root / patch)
(self.root / (changed + '.meta')).rename(self.root / (patch + '.meta'))
self.edit(patch + '.meta', 'Unreviewed.et', 'Character_Base.et')
vanilla = json.loads((self.root / 'docs/vanilla-resources.json').read_text())
other = next(g for g in vanilla.values() if g not in bundle.VANILLA_OVERRIDES)
self.edit(patch + '.meta', '37578B1666981FCE', other)
with self.assertRaisesRegex(ValueError, 'reuses a vanilla identity'):
bundle.build(self.root)
def test_escaped_audio_graph_references_are_checked(self):
name = 'addons/core/Scripts/Game/ERR/10_AI/ERR_SoldierSpeechAudio.c'
bundle.build(self.root)
self.edit(name, 'D4A1555B02A77EDA', '1111111111111111')
with self.assertRaisesRegex(ValueError, 'unresolved or nonvanilla.*Vehicles_Occlusion'):
bundle.build(self.root)
self.edit(name, '1111111111111111', 'D4A1555B02A77EDA')
self.edit(name, 'Vehicles_Occlusion_OutsideVehicleSource.sig', 'Missing.sig')
with self.assertRaisesRegex(ValueError, 'unresolved or nonvanilla.*Missing.sig'):
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()