Files
EscapistsReintroduced/tests/test_external_addons.py
T

152 lines
9.7 KiB
Python

#!/usr/bin/env python3
"""Exercise exact dependency closure, resource ownership and isolated profile inputs."""
import ast
import importlib.util
import os
import re
import copy
import hashlib
import json
from pathlib import Path
import shutil
import sys
import tempfile
import unittest
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
TOOLS=Path(os.environ.get('ERR_OPTIONAL_IDENTITY_TOOLS',Path(__file__).resolve().parents[1]/'tools'))
sys.path.insert(0,str(TOOLS))
import resource_references as refs
import external_addons as ext
ROOT=Path(os.environ.get('ERR_REPO',Path(__file__).resolve().parents[1]))
class ExternalAddonsTests(unittest.TestCase):
def setUp(self):
self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup)
self.home=Path(self.temp.name);self.source=self.home/'installed';self.source.mkdir()
self.addons=self.home/'profile'/'addons';self.addons.mkdir(parents=True)
self.cache=self.home/'profile'/'cache.json';self.value=ext.policy(ROOT)
for guid,deps in ext.ALLOWED.items():
folder=self.source/guid;folder.mkdir()
manifest='GameProject { GUID "'+guid+'" Dependencies { '+' '.join('"'+d+'"' for d in sorted(deps))+' } }\n'
(folder/'addon.gproj').write_text(manifest)
self.value['addons'][guid]['project_sha256']=hashlib.sha256(manifest.encode()).hexdigest()
data=b'RDB synthetic fixture\0'
for path,entry in self.value['resources'].items():
if entry['addon']==guid:data+=path.encode()+b'\0\x06\0\0\0\0\0'+bytes.fromhex(entry['guid'])[::-1]
(folder/'resourceDatabase.rdb').write_bytes(data);(folder/'data.pak').write_bytes(b'fixture '+guid.encode())
def test_resource_identity_requires_visible_owner(self):
for path,item in self.value['resources'].items():
self.assertTrue(ext.resource_allowed(self.value,{item['addon']},item['guid'],path))
self.assertFalse(ext.resource_allowed(self.value,{'core'},item['guid'],path))
self.assertFalse(ext.resource_allowed(self.value,{item['addon']},'0000000000000000',path))
self.assertEqual(ext.direct_for('core'),set(ext.DIRECT));self.assertEqual(ext.direct_for('chat'),set())
def test_policy_rejects_original_transitive_dependency(self):
root=self.home/'root';(root/'docs').mkdir(parents=True)
value=copy.deepcopy(self.value);value['addons'][ext.DIRECT[0]]['dependencies'].append('5F16D7E4A1CBE075')
(root/'docs/external-addons.json').write_text(json.dumps(value))
with self.assertRaisesRegex(ValueError,'transitive'):ext.policy(root)
def test_only_exact_authorized_closure_is_exposed_and_hashed(self):
unrelated=self.source/'original';unrelated.mkdir();(unrelated/'addon.gproj').write_text('GameProject { GUID "5F16D7E4A1CBE075" }')
records=ext.prepare_profile(self.addons,self.source,self.value,self.cache)
self.assertEqual(set(records),set(ext.ALLOWED))
self.assertEqual({p.name for p in self.addons.iterdir()},{'ERRI_'+g for g in ext.ALLOWED})
self.assertTrue(all(p.is_symlink() for p in self.addons.iterdir()))
self.assertTrue(ext.verify_profile(records,self.source,self.value,self.cache,self.addons))
self.assertEqual(ext.prepare_profile(self.addons,self.source,self.value,self.cache),records)
(self.source/ext.DIRECT[0]/'data.pak').write_bytes(b'mutated runtime')
self.assertFalse(ext.verify_profile(records,self.source,self.value,self.cache,self.addons))
def test_missing_and_duplicate_identity_fail(self):
guid=ext.DIRECT[0];shutil.copytree(self.source/guid,self.source/'duplicate')
with self.assertRaisesRegex(ValueError,'Duplicate'):ext.installed(self.source,self.value)
shutil.rmtree(self.source/'duplicate');shutil.rmtree(self.source/guid)
with self.assertRaisesRegex(ValueError,'Missing installed'):ext.installed(self.source,self.value)
def test_manifest_and_database_mutations_fail(self):
guid=ext.DIRECT[0];manifest=self.source/guid/'addon.gproj';old=manifest.read_text();manifest.write_text(old+'// changed')
with self.assertRaisesRegex(ValueError,'manifest changed'):ext.installed(self.source,self.value)
manifest.write_text(old)
resource=next(iter(self.value['resources'].values()));(self.source/resource['addon']/'resourceDatabase.rdb').write_bytes(b'wrong GUID')
with self.assertRaisesRegex(ValueError,'resource identity'):ext.installed(self.source,self.value)
def test_unrelated_profile_addon_and_wrong_link_fail(self):
(self.addons/'original').mkdir()
with self.assertRaisesRegex(ValueError,'Unrelated'):ext.prepare_profile(self.addons,self.source,self.value,self.cache)
(self.addons/'original').rmdir();guid=ext.DIRECT[0]
(self.addons/('ERRI_'+guid)).symlink_to(self.source/ext.DIRECT[1],target_is_directory=True)
with self.assertRaisesRegex(ValueError,'Unexpected private'):ext.prepare_profile(self.addons,self.source,self.value,self.cache)
def test_workshop_preview_media_is_hashed_without_accepting_loose_code(self):
folder=self.source/ext.DIRECT[0]
(folder/'preview0_576x324.jpg').write_bytes(b'preview fixture')
(folder/'preview0_576x324.jpg_2.3.7_manifest.json').write_text('{}')
records=ext.prepare_profile(self.addons,self.source,self.value,self.cache)
self.assertTrue(ext.verify_profile(records,self.source,self.value,self.cache,self.addons))
(folder/'unreviewed.c').write_text('class Surprise {}')
with self.assertRaisesRegex(ValueError,'Unreviewed external packed input'):ext.installed(self.source,self.value)
def test_symlinked_runtime_file_fails(self):
pak=self.source/ext.DIRECT[0]/'data.pak';pak.unlink();pak.symlink_to(self.source/ext.DIRECT[1]/'data.pak')
with self.assertRaisesRegex(ValueError,'regular file'):ext.installed(self.source,self.value)
class OptionalIdentityTests(unittest.TestCase):
def setUp(self):
self.name=refs.OPTIONAL_FILE
self.text=(ROOT/self.name).read_text()
self.guid='574AA64002B8E308';self.resource='Prefabs/Characters/REAPER_SpectatorCameraHelper.et'
def rejected_reference(self,name,text):
self.assertIn((self.guid,self.resource),list(refs.hard_resource_references(name,text)))
def test_exact_comparison_only_and_every_other_reference_retained(self):
all_refs=re.findall(r'\{([A-F0-9]{16})\}([^"\n]+)',self.text)
expected=[pair for pair in all_refs if pair!=(self.guid,self.resource)]
self.assertEqual(list(refs.hard_resource_references(self.name,self.text)),expected)
self.assertIn(('37578B1666981FCE','Prefabs/Characters/Core/Character_Base.et'),expected)
self.rejected_reference('addons/core/Scripts/Game/Other.c',self.text)
for changed in [
self.text+'\n', # even a benign source edit needs renewed audit
self.text.replace('return prefab ==', 'Resource.Load(prefab); return prefab =='),
self.text.replace('return prefab ==', 'return Resource.Load(').replace(refs.OPTIONAL_RESOURCE+'";',refs.OPTIONAL_RESOURCE+'");'),
self.text+'\nResource.Load("'+refs.OPTIONAL_RESOURCE+'");',
self.text+'\nSpawn("'+refs.OPTIONAL_RESOURCE+'");',
self.text+'\nResourceName extra = "'+refs.OPTIONAL_RESOURCE+'";',
self.text.replace(refs.OPTIONAL_METHOD,'/*'+refs.OPTIONAL_METHOD+'*/'),
self.text.replace('CameraSlot(BaseContainer slot)','DifferentMethod(BaseContainer slot)'),
self.text.replace('if (!ModsPresent()) return false;',''),
]: self.rejected_reference(self.name,changed)
unknown=self.text.replace(self.guid,'574AA64002B8E309')
self.assertIn(('574AA64002B8E309',self.resource),list(refs.hard_resource_references(self.name,unknown)))
# The optional addon never enters either resource policy or dependency closure.
value=ext.policy(ROOT)
self.assertNotIn('5EB139459EBF5C16',ext.ALLOWED)
self.assertFalse(ext.resource_allowed(value,set(ext.ALLOWED),self.guid,self.resource))
def repository_resource_loop(self,name,text):
# Execute the actual dependency-scanner loop without running the full suite.
source=Path(os.environ.get('ERR_OPTIONAL_CHECK_SOURCE',TOOLS/'check.py')).read_text()
tree=ast.parse(source)
loops=[node for node in ast.walk(tree) if isinstance(node,ast.For) and isinstance(node.target,ast.Tuple)
and [getattr(n,'id',None) for n in node.target.elts]==['g','path']]
self.assertEqual(len(loops),1)
code=compile(ast.fix_missing_locations(ast.Module(body=loops,type_ignores=[])),'actual_repository_resource_loop','exec')
env=dict(re=re,hard_resource_references=refs.hard_resource_references,p=ROOT/name,ROOT=ROOT,s=text,
resources={},owners={},vanilla=json.loads((ROOT/'docs/vanilla-resources.json').read_text()),
external=ext.policy(ROOT),visibility={'core':{'core',*ext.ALLOWED}},resource_allowed=ext.resource_allowed)
exec(code,env)
def test_actual_repository_resource_loop(self):
self.repository_resource_loop(self.name,self.text)
for name,text in [(self.name,self.text+'\nResource.Load("'+refs.OPTIONAL_RESOURCE+'");'),
('addons/core/Scripts/Game/Other.c',self.text),
(self.name,self.text.replace('return prefab ==','return Resource.Load(').replace(refs.OPTIONAL_RESOURCE+'";',refs.OPTIONAL_RESOURCE+'");'))]:
with self.assertRaisesRegex(AssertionError,'Unresolved/mismatched'):
self.repository_resource_loop(name,text)
def test_actual_bundle_validator(self):
path=Path(os.environ.get('ERR_OPTIONAL_BUNDLE_SOURCE',TOOLS/'source_bundle.py'))
spec=importlib.util.spec_from_file_location('optional_test_source_bundle',path)
bundle=importlib.util.module_from_spec(spec);spec.loader.exec_module(bundle)
payload={name:(ROOT/name).read_bytes() for name in bundle.inventory(ROOT)}
before=ext.policy(ROOT)
bundle.validate(payload,ROOT)
self.assertEqual(before,ext.policy(ROOT))
for changed in [self.text+'\nResource.Load("'+refs.OPTIONAL_RESOURCE+'");',
self.text+'\nSpawn("'+refs.OPTIONAL_RESOURCE+'");']:
altered=dict(payload);altered[self.name]=changed.encode()
with self.assertRaisesRegex(ValueError,'unresolved or nonvanilla'):
bundle.validate(altered,ROOT)
if __name__=='__main__':unittest.main()