Advance standalone admission, finite artillery and native validation
This commit is contained in:
+1
-1
@@ -59,7 +59,7 @@ def check():
|
||||
assert set(mapped)==set(inventory['script_paths']), 'Original inventory has untracked or unknown entries'
|
||||
print(f'PASS: {len(mapped)} original script entries tracked across {len(feature_ids)} unfinished responsibilities')
|
||||
print(f'PASS: {len(ids)} isolated addons, acyclic dependencies, {len(resources)} unique resources, exact vanilla references')
|
||||
for name in ['test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_prison_placement_policy.py', 'test_recovery_policy.py','test_time_policy.py','test_environment_policy.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']:
|
||||
for name in ['test_powered_flight_policy.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py','test_time_policy.py','test_environment_policy.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']:
|
||||
subprocess.run([sys.executable,str(ROOT/'tests'/name)],check=True)
|
||||
print('All repository checks passed. Native compilation and multiplayer tests are separate requirements.')
|
||||
if __name__=='__main__':check()
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bounded Linux server diagnostic: isolated network, frozen sources, no clients."""
|
||||
import argparse
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
PROBE = '''import json,os,socket,sys
|
||||
state={"netns":os.readlink("/proc/self/ns/net"),"interfaces":socket.if_nameindex(),"routes":open("/proc/net/route").read(),"ipv6Routes":open("/proc/net/ipv6_route").read()}
|
||||
print("ERR ISOLATION "+json.dumps(state),flush=True)
|
||||
assert state["netns"] != sys.argv[1], "network namespace was not isolated"
|
||||
assert [name for _,name in state["interfaces"]] == ["lo"], "non-loopback interface present"
|
||||
assert len(state["routes"].splitlines()) == 1, "IPv4 route present"
|
||||
assert all(line.split()[-1] == "lo" for line in state["ipv6Routes"].splitlines()), "non-loopback IPv6 route present"
|
||||
if len(sys.argv)>2: os.execv(sys.argv[2],sys.argv[2:])
|
||||
'''
|
||||
|
||||
|
||||
def manifest(source):
|
||||
result = {}
|
||||
for path in sorted(source.rglob('*')):
|
||||
if path.is_symlink():
|
||||
raise ValueError('Linked source entry is not allowed: ' + str(path))
|
||||
if path.is_file():
|
||||
result[str(path.relative_to(source))] = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
return result
|
||||
|
||||
|
||||
def sandbox(runtime, source, run):
|
||||
return ['bwrap', '--unshare-net', '--unshare-pid', '--unshare-ipc', '--unshare-uts',
|
||||
'--die-with-parent', '--cap-drop', 'ALL', '--clearenv',
|
||||
'--ro-bind', '/', '/', '--tmpfs', '/home', '--tmpfs', '/tmp', '--tmpfs', '/run',
|
||||
'--proc', '/proc', '--dev', '/dev', '--ro-bind', str(runtime), '/tmp/runtime',
|
||||
'--bind', str(source), '/tmp/source', '--bind', str(run), '/tmp/work',
|
||||
'--setenv', 'HOME', '/tmp/work/home', '--setenv', 'TMPDIR', '/tmp/work/tmp',
|
||||
'--setenv', 'XDG_CONFIG_HOME', '/tmp/work/home/.config',
|
||||
'--setenv', 'XDG_DATA_HOME', '/tmp/work/home/.local/share',
|
||||
'--setenv', 'XDG_CACHE_HOME', '/tmp/work/home/.cache',
|
||||
'--setenv', 'PATH', '/usr/bin:/bin', '--setenv', 'LANG', 'C.UTF-8',
|
||||
'--chdir', '/tmp/runtime']
|
||||
|
||||
|
||||
def engine_args(route):
|
||||
args = ['/tmp/runtime/ArmaReforgerServer' + ('Diag' if route == 'diag-world' else ''),
|
||||
'-profile', '/tmp/work/profile', '-noBackend', '-noSound', '-noSplash', '-noThrow',
|
||||
'-bindIP', '127.0.0.1', '-bindPort', '2001', '-maxFPS', '30', '-logStats', '5000']
|
||||
if route == 'vanilla-everon':
|
||||
return args + ['-server', 'Missions/21_GM_Eden.conf']
|
||||
args += ['-addonsDir', '/tmp/source/addons', '-addons', '2528466DE220D91C']
|
||||
if route == 'header':
|
||||
return args + ['-server', '{AD62322D5213C440}Missions/ERR_Everon_Base.conf']
|
||||
return args + ['-server', '{B6C516F86302C679}Worlds/ERR_Everon.ent',
|
||||
'-worldSystemsConfig', '{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf']
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--source', type=Path, required=True)
|
||||
parser.add_argument('--runtime', type=Path, default=Path('/tmp/erri-dedicated-runtime/server'))
|
||||
parser.add_argument('--archive', type=Path, required=True)
|
||||
parser.add_argument('--route', choices=['header', 'diag-world', 'vanilla-everon'], default='header')
|
||||
parser.add_argument('--seconds', type=int, default=90)
|
||||
args = parser.parse_args()
|
||||
if not 1 <= args.seconds <= 110:
|
||||
parser.error('Runtime bound must be 1..110 seconds (plus at most 10 seconds owned cleanup)')
|
||||
source, runtime, archive = args.source.resolve(), args.runtime.resolve(), args.archive.resolve()
|
||||
if not source.is_relative_to('/tmp') or not runtime.is_relative_to('/tmp') or not archive.is_relative_to('/tmp'):
|
||||
parser.error('Sources, runtime and archive must be isolated /tmp paths')
|
||||
if archive == source or source in archive.parents or archive in source.parents or archive == runtime or runtime in archive.parents or archive in runtime.parents:
|
||||
parser.error('Source, archive and runtime must not overlap')
|
||||
if archive.exists() or archive.is_symlink():
|
||||
parser.error('Archive must be new')
|
||||
origin = runtime.parent / 'runtime-origin.json'
|
||||
if not origin.is_file() or json.loads(origin.read_text()).get('app_id') != 1874900:
|
||||
parser.error('Stable private runtime origin missing')
|
||||
binary = runtime / Path(engine_args(args.route)[0]).name
|
||||
with binary.open('rb') as stream:
|
||||
elf = stream.read(20)
|
||||
if binary.is_symlink() or len(elf) != 20 or elf[:6] != b'\x7fELF\x02\x01' or int.from_bytes(elf[18:20], 'little') != 62:
|
||||
parser.error('Expected real native x86-64 ELF server')
|
||||
archive.mkdir(mode=0o700, parents=True)
|
||||
(archive / 'runner.py').write_bytes(Path(__file__).read_bytes())
|
||||
for child in ['home', 'profile', 'tmp']:
|
||||
(archive / child).mkdir()
|
||||
# Reject links before copytree follows them into the isolated source snapshot.
|
||||
manifest(source)
|
||||
frozen = archive / 'source'
|
||||
shutil.copytree(source, frozen, ignore=shutil.ignore_patterns('.git', '__pycache__', 'evidence', 'generated'))
|
||||
initial = manifest(frozen)
|
||||
(archive / 'source-manifest.json').write_text(json.dumps(initial, indent=2)+'\n')
|
||||
base = sandbox(runtime, frozen, archive)
|
||||
netns = os.readlink('/proc/self/ns/net')
|
||||
check = base + ['/usr/bin/python3', '-c', PROBE, netns]
|
||||
preflight = subprocess.run(check, capture_output=True, text=True, timeout=15)
|
||||
(archive / 'isolation-preflight.json').write_text(json.dumps({'command':check,'exit':preflight.returncode,'stdout':preflight.stdout,'stderr':preflight.stderr}, indent=2)+'\n')
|
||||
if preflight.returncode:
|
||||
print('Isolation refused; no server launched:', archive)
|
||||
return 2
|
||||
command = base + ['/usr/bin/python3', '-c', PROBE, netns] + engine_args(args.route)
|
||||
record = {'command': command, 'engine': engine_args(args.route), 'seconds':args.seconds,
|
||||
'binarySHA256':hashlib.sha256(binary.read_bytes()).hexdigest(),
|
||||
'serverLaunched':False,'realPlayers':0,'multiplayerProven':False,
|
||||
'scope':'vanilla Everon control; no successor loaded' if args.route == 'vanilla-everon' else 'successor empty-server startup'}
|
||||
(archive / 'command.json').write_text(json.dumps(record, indent=2)+'\n')
|
||||
lock_fd = os.open('/tmp/erri-dedicated-native-diagnostic.lock', os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW, 0o600)
|
||||
with os.fdopen(lock_fd, 'a') as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
with (archive / 'stdout.log').open('wb') as log:
|
||||
process = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT, start_new_session=True)
|
||||
record['serverLaunched'] = True
|
||||
record['supervisorPid'] = process.pid
|
||||
record['startedUTC'] = time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime())
|
||||
try:
|
||||
record['exit'] = process.wait(timeout=args.seconds)
|
||||
record['timeout'] = False
|
||||
except subprocess.TimeoutExpired:
|
||||
record['timeout'] = True
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
try: process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
process.wait(timeout=5)
|
||||
record['exit'] = process.returncode
|
||||
record['finishedUTC'] = time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime())
|
||||
(archive / 'result.json').write_text(json.dumps(record, indent=2)+'\n')
|
||||
(archive / 'source-manifest-after.json').write_text(json.dumps(manifest(frozen), indent=2)+'\n')
|
||||
print('Diagnostic archived; inspect actual compilation/header/systems/errors:',archive)
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare reviewable private dedicated-server inputs; never launch or download."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from source_manifest import addon_manifest
|
||||
|
||||
APP_ID = '1874900'
|
||||
HEADERS = {'core': 'Missions/ERR_Everon_Base.conf', 'chat': 'Missions/ERR_Everon.conf'}
|
||||
SYSTEMS = '{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf'
|
||||
WORLD = '{B6C516F86302C679}Worlds/ERR_Everon.ent'
|
||||
|
||||
|
||||
def digest(path):
|
||||
value = hashlib.sha256()
|
||||
with path.open('rb') as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
|
||||
value.update(chunk)
|
||||
return value.hexdigest()
|
||||
|
||||
|
||||
def selected_scenario(source, project):
|
||||
addon = source / 'addons' / project
|
||||
text = (addon / 'addon.gproj').read_text()
|
||||
guid = re.search(r'\bGUID "([A-F0-9]{16})"', text)
|
||||
if not guid:
|
||||
raise ValueError('Selected addon has no exact GUID')
|
||||
relative = HEADERS[project]
|
||||
header = addon / relative
|
||||
meta = header.with_suffix('.conf.meta').read_text()
|
||||
resource = re.search(r'\bName "(\{[A-F0-9]{16}\}[^"\n]+)"', meta)
|
||||
if not resource or resource[1][18:] != relative:
|
||||
raise ValueError('Selected header resource identity/path mismatch')
|
||||
data = header.read_text()
|
||||
for key, expected in [('World', WORLD), ('SystemsConfig', SYSTEMS)]:
|
||||
match = re.search(r'\b' + key + r' "([^"\n]+)"', data)
|
||||
if not match or match[1] != expected:
|
||||
raise ValueError('Selected header has unexpected ' + key)
|
||||
return {'project': project, 'addon_guid': guid[1], 'header': resource[1],
|
||||
'world': WORLD, 'systems_config': SYSTEMS,
|
||||
'addon_directory': str(source / 'addons')}
|
||||
|
||||
|
||||
def inspect_runtime(runtime):
|
||||
binary = runtime / 'server' / 'ArmaReforgerServer'
|
||||
manifest = runtime / 'server' / 'steamapps' / ('appmanifest_' + APP_ID + '.acf')
|
||||
result = {'runtime_directory': str(runtime), 'binary': str(binary),
|
||||
'app_id': APP_ID, 'platform': 'linux', 'branch': 'public',
|
||||
'binary_present': binary.is_file(), 'binary_elf_x86_64': False,
|
||||
'binary_executable': False, 'steam_build_id': None,
|
||||
'install_complete': False, 'engine_version': None,
|
||||
'native_runtime_executed': False}
|
||||
if manifest.is_file():
|
||||
values = dict(re.findall(r'"(appid|buildid|StateFlags)"\s+"([^"]*)"', manifest.read_text()))
|
||||
if values.get('appid') != APP_ID:
|
||||
raise ValueError('Runtime manifest is not the stable dedicated-server app')
|
||||
result.update(steam_build_id=values.get('buildid'),
|
||||
install_complete=values.get('StateFlags') == '4',
|
||||
manifest_sha256=digest(manifest))
|
||||
if binary.is_file():
|
||||
if binary.is_symlink():
|
||||
raise ValueError('Private runtime binary must not be a symlink to another installation')
|
||||
with binary.open('rb') as stream:
|
||||
header = stream.read(20)
|
||||
result['binary_elf_x86_64'] = len(header) == 20 and header[:6] == b'\x7fELF\x02\x01' and int.from_bytes(header[18:20], 'little') == 62
|
||||
result['binary_executable'] = os.access(binary, os.X_OK)
|
||||
result['binary_sha256'] = digest(binary)
|
||||
return result
|
||||
|
||||
|
||||
def build_plan(source, runtime, project='core', port=2001):
|
||||
source, runtime = source.resolve(), runtime.resolve()
|
||||
if not 1024 <= port <= 65535:
|
||||
raise ValueError('Use an unprivileged port in 1024..65535')
|
||||
if source == runtime or source in runtime.parents or runtime in source.parents:
|
||||
raise ValueError('Runtime must be separate from the authored source tree')
|
||||
manifest = addon_manifest(source)
|
||||
scenario = selected_scenario(source, project)
|
||||
installation = inspect_runtime(runtime)
|
||||
config = {
|
||||
'bindAddress': '127.0.0.1', 'bindPort': port,
|
||||
'publicAddress': '127.0.0.1', 'publicPort': port,
|
||||
'game': {'name': 'Rereintroduced private validation', 'scenarioId': scenario['header'],
|
||||
'maxPlayers': 2, 'visible': False,
|
||||
'gameProperties': {'fastValidation': True, 'battlEye': True}, 'mods': []},
|
||||
'operating': {'disableCrashReporter': True},
|
||||
}
|
||||
blockers = []
|
||||
if not (installation['install_complete'] and installation['binary_elf_x86_64'] and installation['binary_executable']):
|
||||
blockers.append('Stable native Linux dedicated-server installation is missing or incomplete.')
|
||||
blockers.extend([
|
||||
'Server binary dependencies and exact engine-version compatibility have not been validated by a native run.',
|
||||
'Unpublished local-addon loading with authenticated server configuration/header selection needs native verification; do not combine -config and -addons.',
|
||||
'Loopback server configuration must be reviewed before any listener is started.',
|
||||
'Two genuine accepted native client identities/controllers are not available as evidence; copied profiles and synthetic actors do not qualify.',
|
||||
'Existing ordinary runner refuses concurrent game instances; it is not a multi-client supervisor.',
|
||||
])
|
||||
result = {'schema': 1, 'scope': 'no-launch dedicated preparation only', 'launch_allowed': False,
|
||||
'listener_started': False, 'genuine_clients_verified': 0, 'multiplayer_passed': False,
|
||||
'source_directory': str(source), 'scenario': scenario, 'runtime': installation,
|
||||
'network': {'bind': '127.0.0.1', 'port': port, 'public_listing': False,
|
||||
'rcon_configured': False, 'a2s_configured': False},
|
||||
'source_manifest_sha256': hashlib.sha256((json.dumps(manifest, indent=2, sort_keys=True) + '\n').encode()).hexdigest(),
|
||||
'disk_free_bytes': shutil.disk_usage(runtime if runtime.exists() else runtime.parent).free,
|
||||
'blockers': blockers,
|
||||
'evidence_required': [
|
||||
'Server and both clients: exact version, role, source/addon manifest and complete sealed raw logs.',
|
||||
'Server: native dedicated mode, selected header and actual MissionSystems config, two distinct accepted connections/current controlled prisoners.',
|
||||
'Client1 and Client2: separate native ownership/local controller and actual replicated state; record no account credentials.',
|
||||
'Both clients: JIP, same-account reconnect/reservation expiry, death/unconsciousness, spectator and extraction lifecycle.',
|
||||
'No synthetic-client substitution and no multiplayer/FPS claim from headless startup alone.',
|
||||
]}
|
||||
return result, config, manifest
|
||||
|
||||
|
||||
def write_plan(destination, result, config, manifest):
|
||||
destination = destination.absolute()
|
||||
runtime = Path(result['runtime']['runtime_directory'])
|
||||
if destination.is_symlink() or destination.exists():
|
||||
raise ValueError('Plan directory must be new; existing evidence is never overwritten')
|
||||
if not destination.parent.is_dir() or destination.parent.resolve() != runtime:
|
||||
raise ValueError('Plan directory must be a new direct child of the private runtime')
|
||||
marker = runtime / 'runtime-origin.json'
|
||||
if not marker.is_file() or marker.is_symlink():
|
||||
raise ValueError('Private runtime origin marker is required before writing plans')
|
||||
origin = json.loads(marker.read_text())
|
||||
if origin.get('app_id') != int(APP_ID) or origin.get('purpose') != 'private anonymous Reforger dedicated server preparation; no server launched':
|
||||
raise ValueError('Runtime origin marker is not the authorized private preparation')
|
||||
destination.mkdir(mode=0o700)
|
||||
for filename, value in [('preflight.json', result), ('server-config.review.json', config), ('source-manifest.json', manifest)]:
|
||||
(destination / filename).write_text(json.dumps(value, indent=2, sort_keys=True) + '\n')
|
||||
# A human-readable plan is deliberately not an executable launch script.
|
||||
(destination / 'REVIEW.txt').write_text('NO SERVER STARTED. DO NOT LAUNCH BEFORE CONFIGURATION REVIEW.\n'
|
||||
'server-config.review.json binds loopback and preserves native fast validation/BattlEye.\n'
|
||||
'Local unpublished-addon/config compatibility, engine dependencies/version and two genuine clients remain unverified.\n'
|
||||
'Do not add -addons to a -config command or treat this plan as runtime/MP evidence.\n')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--source', type=Path, required=True)
|
||||
parser.add_argument('--runtime', type=Path, default=Path('/tmp/erri-dedicated-runtime'))
|
||||
parser.add_argument('--project', choices=HEADERS, default='core')
|
||||
parser.add_argument('--port', type=int, default=2001)
|
||||
parser.add_argument('--write-plan', type=Path, help='Write review files into a NEW direct child of the private runtime; never launch')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
result, config, manifest = build_plan(args.source, args.runtime, args.project, args.port)
|
||||
if args.write_plan:
|
||||
write_plan(args.write_plan, result, config, manifest)
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return 0 # success means inspection completed, never launch/MP qualification
|
||||
except (OSError, ValueError) as error:
|
||||
parser.error(str(error))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -138,13 +138,15 @@ def game_evidence(content, project=None):
|
||||
non_workbench = bool(defines) and all(not {'WORKBENCH', 'ENF_WB'}.intersection(value.split(',')) for value in defines)
|
||||
headers = re.findall(r'ERR ORDINARY: header verified project=(core|chat) systems=MissionSystems', content)
|
||||
header_verified = bool(headers) and (project is None or project in headers)
|
||||
ready = header_verified and 'Rereintroduced [mission]: phase 0 -> 1' in content
|
||||
participants = re.findall(r'ERR ORDINARY: local participant verified project=(core|chat) phase=prison', content)
|
||||
participant_verified = bool(participants) and (project is None or project in participants)
|
||||
ready = header_verified and participant_verified and 'Rereintroduced [mission]: phase 0 -> 1' in content
|
||||
unexpected_fixture = 'ERR SMOKE ' in content
|
||||
failed = (bool(report['engine_errors'] or report['script_or_runtime_failures'])
|
||||
or 'Rereintroduced: scenario preparation failed' in content
|
||||
or 'Can\'t compile "Game"' in content or unexpected_fixture
|
||||
or any({'WORKBENCH', 'ENF_WB'}.intersection(value.split(',')) for value in defines))
|
||||
return {**report, 'scenario_header_verified': header_verified, 'verified_header_projects': headers, 'game_module_defines': defines, 'workbench_undefined': non_workbench,
|
||||
return {**report, 'local_participant_verified': participant_verified, 'scenario_header_verified': header_verified, 'verified_header_projects': headers, 'game_module_defines': defines, 'workbench_undefined': non_workbench,
|
||||
'normal_mission_ready': ready, 'unexpected_smoke_fixture': unexpected_fixture,
|
||||
'failed': failed, 'ready': non_workbench and ready and not failed,
|
||||
'scope': 'ordinary local game startup only; no player input, full escape, multiplayer or FPS proof'}
|
||||
|
||||
@@ -11,7 +11,7 @@ p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam')
|
||||
p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam')
|
||||
p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile')
|
||||
p.add_argument('--smoke', choices=['patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory')
|
||||
p.add_argument('--smoke', choices=['powered-flight', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory')
|
||||
p.add_argument('--station-teardown-diagnostics', action='store_true', help='Opt-in bounded native deletion diagnostics; preserves all validation errors')
|
||||
p.add_argument('--rounds', type=int, default=2)
|
||||
p.add_argument('--timeout', type=int, default=360)
|
||||
@@ -20,6 +20,9 @@ a = p.parse_args()
|
||||
steam, compat, profile = a.steam.resolve(), a.compat.resolve(), a.profile.resolve()
|
||||
if not 1 <= a.rounds <= 10 or not 60 <= a.timeout <= 1800:
|
||||
p.error('Use 1..10 rounds and a 60..1800 second timeout')
|
||||
if a.smoke in {'mortar-expiry', 'mortar-transfer', 'mortar-fire'} and (a.project != 'chat' or a.rounds != 1 or a.timeout < 400):
|
||||
p.error('mortar-expiry/mortar-transfer/mortar-fire requires --project chat --rounds 1 --timeout at least 400')
|
||||
if a.smoke == 'powered-flight' and (a.project != 'core' or a.rounds != 1 or a.timeout < 420): p.error('powered-flight requires Core, one round, and at least 420 seconds')
|
||||
if a.smoke == 'environment-restart':
|
||||
if a.project != 'core' or a.rounds != 2: p.error('environment-restart requires --project core --rounds 2')
|
||||
fixture_names = ['erri-environment-restart.json', 'erri-environment-restart.json.bak', 'erri-environment-restart.json.tmp', 'erri-environment-restart-round.json']
|
||||
@@ -56,10 +59,10 @@ for proc in Path('/proc').iterdir():
|
||||
command = ['gamescope', '--backend', 'headless', '-W', '1280', '-H', '720', '-w', '1280', '-h', '720', '-r', '30', '--', str(proton), 'run', str(wb/'ArmaReforgerWorkbenchSteamDiag.exe'), '-gproj', 'Z:'+str(ROOT/'addons'/a.project/'addon.gproj'), '-addonsDir', 'Z:'+str(ROOT/'addons'), '-profile', profile_arg, '-logconsole', '-wbModule=WorldEditor', '-load', 'Worlds/ERR_Everon.ent', '-plugin=ERR_ValidateWorldPlugin', '-errPlay', '1', '-errSmoke', '1', '-errSmokeOutcome', a.smoke, '-noThrow', '-disableCrashReporter']
|
||||
if a.station_teardown_diagnostics: command += ['-errStationTeardownDiag', '1']
|
||||
if a.project == 'core':
|
||||
if a.smoke not in {'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture')
|
||||
if a.smoke not in {'powered-flight', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture')
|
||||
command[command.index('-plugin=ERR_ValidateWorldPlugin')] = '-plugin=ERR_ValidateBaseWorldPlugin'
|
||||
command[command.index('-errSmoke')] = '-errBaseSmoke'
|
||||
if a.smoke in {'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core')
|
||||
if a.smoke in {'powered-flight', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core')
|
||||
if a.smoke in {'startup-failure', 'site-survey', 'transport-survey', 'extraction-startup-failure', 'extraction-boundary-failure'} and (a.project != 'core' or a.rounds != 1): p.error('This diagnostic requires --project core --rounds 1')
|
||||
env = os.environ.copy()
|
||||
env.update(STEAM_COMPAT_DATA_PATH=str(compat), STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam), WINEPREFIX=str(compat/'pfx'))
|
||||
|
||||
Reference in New Issue
Block a user