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