Add bounded aircraft control and verify current packaged runtime closure

This commit is contained in:
2026-09-19 13:31:51 +02:00
parent 4125018d04
commit a880e0485c
695 changed files with 79317 additions and 44 deletions
+129
View File
@@ -2,6 +2,7 @@
import contextlib
import importlib.util
import io
import re
from pathlib import Path
import sys
import tempfile
@@ -43,6 +44,134 @@ class DedicatedNativeTests(unittest.TestCase):
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': ('FA804F411BDDF2C6', '{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'/project/'addon.gproj').read_text()
self.assertIn('GUID "'+guid+'"', gproj)
resource = header.split('}', 1)[1]
self.assertIn('Name "'+header+'"', (root/'addons'/project/(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')
# Selecting Chat activates the existing declared closure, not invented
# runtime addons or an explicit list that could omit dependencies.
chat = (root/'addons/chat/addon.gproj').read_text().split('Dependencies', 1)[1]
dependencies = set(re.findall(r'"([A-F0-9]{16})"', chat))
expected_dependencies = {'58D0FB3206B6F859'}
for project in ('core', 'ai', 'intel', 'map'):
expected_dependencies.add(re.search(r'GUID "([A-F0-9]{16})"', (root/'addons'/project/'addon.gproj').read_text())[1])
self.assertEqual(dependencies, expected_dependencies)
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', 'ai', 'intel', 'map', 'chat')):
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')
with self.assertRaisesRegex(ValueError, 'ai'):
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', 'ai/resourceDatabase.rdb', False),
('chat', 'map/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', 'FA804F411BDDF2C6'))
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')