Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
144 lines
9.1 KiB
Python
144 lines
9.1 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_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()
|