#!/usr/bin/env python3 """Run isolated native occupancy, JSON persistence, recovery and shutdown checks.""" import argparse import json from pathlib import Path import shutil import subprocess import sys ROOT = Path(__file__).resolve().parents[3] p = argparse.ArgumentParser(description=__doc__) p.add_argument('--work', type=Path, required=True, help='New disposable directory; must not exist') p.add_argument('--compat', type=Path, required=True, help='Existing private test Proton prefix') p.add_argument('--external-addons', type=Path, required=True) a = p.parse_args() work = a.work.resolve() if work.exists(): p.error('--work must not exist; preserve evidence from earlier runs') work.mkdir(parents=True) source, profile = work/'source', work/'profile' for name in ['addons', 'tools']: shutil.copytree(ROOT/name, source/name) (source/'docs').mkdir() shutil.copy2(ROOT/'docs/external-addons.json', source/'docs/external-addons.json') def replace(path, before, after): text = path.read_text() if text.count(before) != 1: raise ValueError(f'Fixture insertion point changed in {path.name}') path.write_text(text.replace(before, after)) scripts = source/'addons/core/Scripts/Game/ERR/00_Core' shutil.copy2(Path(__file__).with_name('ERR_TelemetryCheckpointTest.c'), scripts/'ERR_TelemetryCheckpointTest.c') # Modded classes can only access fields/methods of earlier layers. Test accessors # follow telemetry; the existing native-end fixture is moved after that layer. replace(scripts/'ERR_TelemetryLifecycle.c', ' void ERR_RefreshTelemetry()', ' bool ERR_TestTelemetryBefore() { return m_ERRTelemetry && m_ERRTelemetry.ERR_TestOccupiedRecovery(); }\n' ' bool ERR_TestTelemetryAfter() { return m_ERRTelemetry && m_ERRTelemetry.ERR_TestFinished(); }\n' ' void ERR_RefreshTelemetry()') probe = scripts/'ERR_NativeEndProbe.c' replace(probe, 'm_ERREndData = SCR_GameModeEndData.CreateSimple', 'if (!ERR_EndCheck(ERR_TestTelemetryBefore(), "occupied telemetry snapshot and recovery")) return;\n' ' m_ERREndData = SCR_GameModeEndData.CreateSimple') replace(probe, 'EndGameMode(m_ERREndData);', 'EndGameMode(m_ERREndData);\n' ' if (!ERR_EndCheck(ERR_TestTelemetryAfter(), "native shutdown records one defeat")) return;') probe.rename(scripts/'ERR_ZTelemetryNativeEndProbe.c') runner = source/'tools/headless_workbench.py' replace(runner, 'if a.roadblock_probe_count is not None: command', "command += ['-errTelemetryTest', '1']\nif a.roadblock_probe_count is not None: command") # Use two explicit process launches: Workbench need not auto-reload a hosted game. replace(runner, "a.project != 'core' or a.rounds != 2 or a.timeout < 120", "a.project != 'core' or a.rounds != 1 or a.timeout < 120") replace(runner, "if any((profile/name).exists() or (profile/name).is_symlink() for name in fixture_names): p.error('native-end fixture paths must not preexist; preserve prior evidence before retrying')", '# This isolated fixture intentionally preserves the first instance marker.') (profile/'profile').mkdir(parents=True) (profile/'profile/EscapistsTelemetry.json').write_text( '{"endpoint":"https://telemetry.invalid/api/mission-events",' '"token":"local-test-only","serverName":"Isolated restart test"}\n') command = [sys.executable, str(runner), '--compat', str(a.compat.resolve()), '--profile', str(profile), '--external-addons', str(a.external_addons.resolve()), '--smoke', 'native-end-preparing', '--project', 'core', '--rounds', '1', '--timeout', '120'] sys.path.insert(0, str(ROOT/'tools')) from native_report import analyze for _ in range(2): old_logs = set((profile/'logs').glob('logs_*')) result = subprocess.run(command, check=False) new_logs = set((profile/'logs').glob('logs_*')) - old_logs assert len(new_logs) == 1, 'Expected exactly one fresh native session' session = new_logs.pop() evidence = json.loads((session/'runner.json').read_text()) report = analyze((session/'console.log').read_text(errors='replace'), session/'console.log') assert result.returncode in (0, 1) assert evidence['result'] == 'smoke-completed' and evidence['cleanup_confirmed'] assert evidence['source_unchanged_after_run'] and evidence['external_addons_unchanged_after_run'] assert report['smoke_completions'] == 1 and not report['script_or_runtime_failures'] assert not report['missing_end_screen'] # The generic runner also grades all optional asset/backend errors. Preserve # its complete report; this fixture grades telemetry, not DLC/asset integrity. report['telemetry_assertions_passed'] = True (session/'telemetry-check.json').write_text(json.dumps(report, indent=2)+'\n') if report['engine_errors']: print(f"Telemetry checks passed; {len(report['engine_errors'])} asset/backend errors retained in {session/'telemetry-check.json'}", flush=True) state_path = profile/'profile/EscapistsTelemetryState.json' events = [json.loads(payload) for payload in json.loads(state_path.read_text())['state']['pending']] finished = [event for event in events if event['type'] == 'mission_finished'] assert len(finished) == 2 and len({event['missionId'] for event in finished}) == 2 assert all(event['outcome'] == 'defeat' and event['phase'] == 6 and event['connectedPlayers'] > 0 for event in finished) restarts = [event for event in events if event['type'] == 'mission_restarted'] assert len(restarts) == 1 and restarts[0]['previousOutcome'] == 'defeat' assert restarts[0]['previousMissionId'] in {event['missionId'] for event in finished} subprocess.run(['node', '--input-type=module', '-e', "import {readFileSync} from 'node:fs'; import {validateEvent} from " + json.dumps((ROOT/'services/telemetry-worker/src/validation.js').as_uri()) + ";" + "const {state}=JSON.parse(readFileSync(process.argv[1],'utf8'));" + "for(const payload of state.pending) validateEvent(JSON.parse(payload));", str(state_path)], check=True) print('PASS: two native process instances, real occupied shutdowns, durable recovery, no duplicate results and receiver-valid native payloads')