Files
EscapistsReintroduced/tests/test_dedicated_native.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

191 lines
11 KiB
Python

#!/usr/bin/env python3
import contextlib
import importlib.util
import io
import re
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import patch
spec = importlib.util.spec_from_file_location('dedicated_native', Path(__file__).parents[1] / 'tools/dedicated_native.py')
runner = importlib.util.module_from_spec(spec)
spec.loader.exec_module(runner)
class DedicatedNativeTests(unittest.TestCase):
def probe(self, namespace='isolated', interfaces=None, routes=None, ipv6=''):
interfaces = interfaces if interfaces is not None else [(1, 'lo')]
routes = routes if routes is not None else 'Iface Destination Gateway\n'
def content(path, *args):
return io.StringIO(ipv6 if path.endswith('ipv6_route') else routes)
with patch('os.readlink', return_value=namespace), patch('socket.if_nameindex', return_value=interfaces), patch('builtins.open', side_effect=content), patch.object(sys, 'argv', ['probe', 'host', '/server']), patch('os.execv') as launch, contextlib.redirect_stdout(io.StringIO()):
try:
exec(runner.PROBE, {})
except AssertionError:
self.assertFalse(launch.called)
return False
launch.assert_called_once_with('/server', ['/server'])
return True
def test_isolation_gate_executes_only_private_loopback(self):
self.assertTrue(self.probe())
self.assertFalse(self.probe(namespace='host'))
self.assertFalse(self.probe(interfaces=[(1, 'lo'), (2, 'eth0')]))
self.assertFalse(self.probe(routes='Iface Destination\neth0 00000000\n'))
self.assertFalse(self.probe(ipv6='0000 eth0\n'))
self.assertTrue(self.probe(ipv6='0000 lo\n'))
def test_vanilla_control_has_no_successor_or_custom_systems(self):
args=runner.engine_args('vanilla-everon')
self.assertNotIn('-addons',args)
self.assertNotIn('-addonsDir',args)
self.assertNotIn('-worldSystemsConfig',args)
self.assertEqual(args[-2:],['-server','Missions/21_GM_Eden.conf'])
self.assertEqual(args[0],'/tmp/runtime/ArmaReforgerServer')
def test_exact_core_and_chat_header_and_diagnostic_selection(self):
self.assertEqual(runner.engine_args('header'), runner.engine_args('header', 'core'))
expected = {
'core': ('2528466DE220D91C', '{AD62322D5213C440}Missions/ERR_Everon_Base.conf'),
'chat': ('2528466DE220D91C', '{C1E20667DCE324ED}Missions/ERR_Everon.conf'),
}
root = Path(__file__).parents[1]
for project, (guid, header) in expected.items():
# Keep the CLI identities tied to the actual authored resources.
gproj = (root/'addons/core'/'addon.gproj').read_text()
self.assertIn('GUID "'+guid+'"', gproj)
resource = header.split('}', 1)[1]
self.assertIn('Name "'+header+'"', (root/'addons/core'/(resource+'.meta')).read_text())
for route in ('header', 'diag-world'):
args = runner.engine_args(route, project)
self.assertEqual(args[args.index('-addonsDir')+1], '/tmp/source/addons')
self.assertEqual(args[args.index('-addons')+1], guid)
self.assertEqual(args.count('-addons'), 1)
self.assertEqual(args[args.index('-profile')+1], '/tmp/work/profile')
self.assertEqual(args[args.index('-bindIP')+1], '127.0.0.1')
self.assertIn('-noBackend', args)
self.assertIn('-noSound', args)
if route == 'header':
self.assertEqual(args[0], '/tmp/runtime/ArmaReforgerServer')
self.assertEqual(args[args.index('-server')+1], header)
self.assertNotIn('-worldSystemsConfig', args)
else:
self.assertEqual(args[0], '/tmp/runtime/ArmaReforgerServerDiag')
self.assertEqual(args[args.index('-server')+1], '{B6C516F86302C679}Worlds/ERR_Everon.ent')
self.assertEqual(args[args.index('-worldSystemsConfig')+1], '{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf')
definition = (root/'addons/core/addon.gproj').read_text()
dependencies = set(re.findall(r'"([A-F0-9]{16})"', re.search(r'Dependencies\s*\{([^}]+)', definition)[1]))
self.assertEqual(dependencies, {'58D0FB3206B6F859', '595F2BF2F44836FB', '629B2BA37EFFD577', '5E524E4FEECCA92B'})
def test_invalid_project_route_combinations_fail_before_files_or_processes(self):
for route, project in [('vanilla-everon', 'chat'), ('unknown', 'core'), ('header', 'unknown')]:
with self.subTest(route=route, project=project), self.assertRaises(ValueError):
runner.engine_args(route, project)
argv = ['dedicated_native.py', '--source', '/tmp/not-read-source',
'--runtime', '/tmp/not-read-runtime', '--archive', '/tmp/not-created',
'--route', 'vanilla-everon', '--project', 'chat']
stderr = io.StringIO()
with patch.object(sys, 'argv', argv), patch.object(Path, 'resolve') as resolve, patch.object(Path, 'mkdir') as mkdir, patch.object(runner.subprocess, 'run') as run, patch.object(runner.subprocess, 'Popen') as launch, contextlib.redirect_stderr(stderr):
with self.assertRaises(SystemExit) as error:
runner.main()
self.assertEqual(error.exception.code, 2)
resolve.assert_not_called()
mkdir.assert_not_called()
run.assert_not_called()
launch.assert_not_called()
self.assertIn('vanilla-everon cannot be combined with --project chat', stderr.getvalue())
def addon_input(self, root, names=('core',)):
repository = Path(__file__).parents[1]
for addon in names:
folder = root/'addons'/addon
folder.mkdir(parents=True)
(folder/'addon.gproj').write_bytes((repository/'addons'/addon/'addon.gproj').read_bytes())
# Synthetic presence-only files, never supplied to a real engine.
(folder/'resourceDatabase.rdb').write_bytes(b'nonempty-presence-test')
(folder/'data.pak').write_bytes(b'nonempty-presence-test')
def test_preflight_accepts_packed_input_without_loose_scripts(self):
with tempfile.TemporaryDirectory() as directory:
source = Path(directory)
self.addon_input(source)
for route in ('header', 'diag-world'):
for project in ('core', 'chat'):
runner.preflight_addons(source, route, project)
self.assertFalse(list(source.rglob('*.c')))
# Core-only means no companion RDB is required.
with tempfile.TemporaryDirectory() as directory:
source = Path(directory)
self.addon_input(source, ('core',))
runner.preflight_addons(source, 'header')
runner.preflight_addons(source, 'header', 'chat')
runner.preflight_addons(Path('/not-a-real-addon-input'), 'vanilla-everon')
def test_missing_core_dependency_and_empty_database_refuse_before_archive_or_process(self):
for project, relative, empty in [
('core', 'core/resourceDatabase.rdb', False),
('chat', 'core/addon.gproj', False),
('chat', 'core/resourceDatabase.rdb', False),
('chat', 'core/resourceDatabase.rdb', True),
]:
with self.subTest(project=project, relative=relative, empty=empty), tempfile.TemporaryDirectory() as directory:
root = Path(directory); source=root/'source'; archive=root/'archive'
self.addon_input(source)
target = source/'addons'/relative
if empty: target.write_bytes(b'')
else: target.unlink()
argv = ['dedicated_native.py', '--source', str(source), '--archive', str(archive),
'--runtime', str(root/'runtime'), '--project', project]
stderr = io.StringIO()
with patch.object(sys, 'argv', argv), patch.object(runner, 'manifest') as manifest, patch.object(runner.subprocess, 'run') as preflight, patch.object(runner.subprocess, 'Popen') as launch, contextlib.redirect_stderr(stderr):
with self.assertRaises(SystemExit) as error: runner.main()
self.assertEqual(error.exception.code, 2)
manifest.assert_not_called(); preflight.assert_not_called(); launch.assert_not_called()
self.assertFalse(archive.exists())
self.assertIn(relative, stderr.getvalue())
def test_preflight_refuses_linked_directory_linked_database_and_wrong_identity(self):
with tempfile.TemporaryDirectory() as directory:
source = Path(directory)/'source'; self.addon_input(source)
database = source/'addons/core/resourceDatabase.rdb'
database.unlink(); database.symlink_to(source/'addons/core/data.pak')
with self.assertRaisesRegex(ValueError, 'regular file'):
runner.preflight_addons(source, 'header')
database.unlink(); database.mkdir()
with self.assertRaisesRegex(ValueError, 'regular file'):
runner.preflight_addons(source, 'header')
database.rmdir(); database.write_bytes(b'presence-only')
project = source/'addons/core/addon.gproj'
project.write_text(project.read_text().replace('2528466DE220D91C', '1111111111111111'))
with self.assertRaisesRegex(ValueError, 'identity mismatch'):
runner.preflight_addons(source, 'header')
with tempfile.TemporaryDirectory() as directory:
root = Path(directory); self.addon_input(root/'real')
source=root/'source'; source.mkdir(); (source/'addons').symlink_to(root/'real/addons')
with self.assertRaisesRegex(ValueError, 'regular addons directory'):
runner.preflight_addons(source, 'header')
def test_manifest_detects_mutation_and_refuses_links(self):
with tempfile.TemporaryDirectory() as directory:
root=Path(directory); target=root/'a.c'; target.write_text('first')
old=runner.manifest(root); target.write_text('second')
self.assertNotEqual(old,runner.manifest(root))
(root/'linked').symlink_to(target)
with self.assertRaises(ValueError): runner.manifest(root)
def test_routes_do_not_mix_public_config_or_client_flags(self):
for route in ['header','diag-world','vanilla-everon']:
args=runner.engine_args(route)
self.assertNotIn('-config',args)
self.assertNotIn('-client',args)
self.assertIn('-noBackend',args)
self.assertNotIn('-rpl-validation-rdb-disable',args)
normal=runner.engine_args('header')
self.assertEqual(normal[0],'/tmp/runtime/ArmaReforgerServer')
self.assertTrue(normal[normal.index('-server')+1].endswith('ERR_Everon_Base.conf'))
self.assertNotIn('-worldSystemsConfig',normal)
self.assertIn('-worldSystemsConfig',runner.engine_args('diag-world'))
if __name__ == '__main__': unittest.main()