97 lines
6.0 KiB
Python
97 lines
6.0 KiB
Python
#!/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')
|