Fix inventory layout integration and qualify ordinary UI and radio guidance
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_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.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_vanilla_game_control.py','test_archive_game_control.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_progression_identity.py','test_discovery_map_position.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']:
|
||||
for name in ['test_radio_waiting_text.py', 'test_inventory_control_runner.py', 'test_inventory_layout.py', 'test_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.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_vanilla_game_control.py','test_archive_game_control.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_progression_identity.py','test_discovery_map_position.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()
|
||||
|
||||
+25
-2
@@ -751,10 +751,33 @@ def input_observer_evidence(content, nonce):
|
||||
if closes and (not opens or records.index(closes[0]) <= records.index(opens[0])):
|
||||
errors.append('inventory close lacks prior observed open')
|
||||
captures = matching('CAPTURE_REQUEST')
|
||||
for suffix, transition in [('baseline', ready), ('open', opens), ('closed', closes)]:
|
||||
for suffix, transition, following in [('baseline', ready, opens), ('open', opens, closes), ('closed', closes, finished)]:
|
||||
requested = [r for r in captures if r['event'] == 'CAPTURE_REQUEST '+suffix]
|
||||
if len(requested) > 1 or (requested and (not transition or records.index(requested[0]) <= records.index(transition[0]) or requested[0]['sample'] != transition[0]['sample'])):
|
||||
if len(requested) > 1 or (requested and len(transition) != 1):
|
||||
errors.append('capture request lacks its unique transition')
|
||||
continue
|
||||
if not requested:
|
||||
continue # superseded/invalid pending captures are legitimately skipped
|
||||
capture, begin = requested[0], transition[0]
|
||||
capture_index, begin_index = records.index(capture), records.index(begin)
|
||||
current = by_index.get(capture['sample'])
|
||||
if (capture_index <= begin_index or capture['real_ms']-begin['real_ms'] < 300 or
|
||||
not current or current['inventory'] is not (suffix == 'open')):
|
||||
errors.append('delayed capture lacks matching native state and minimum delay')
|
||||
if following and capture_index >= records.index(following[0]):
|
||||
errors.append('delayed capture crossed its transition interval')
|
||||
if queued and not 0 <= capture['real_ms']-queued[0]['real_ms'] < 45000:
|
||||
errors.append('delayed capture outside observation window')
|
||||
interval = [sample for sample in samples if begin['sample'] <= sample['sample'] <= capture['sample']]
|
||||
if any(sample['inventory'] is not (suffix == 'open') for sample in interval):
|
||||
errors.append('delayed capture native inventory state changed')
|
||||
if suffix == 'baseline':
|
||||
baseline = by_index.get(begin['sample'])
|
||||
if not baseline or any(abs(sample['forward']) > .01 or
|
||||
math.sqrt(sum(value*value for value in sample['input'])) > .01 or
|
||||
math.sqrt(sum(value*value for value in sample['velocity'])) > .05 or
|
||||
math.dist(sample['origin'], baseline['origin']) > .05 for sample in interval):
|
||||
errors.append('delayed baseline capture lost stationary state')
|
||||
# Native movement proof belongs to the observer, independently of whether
|
||||
# an external helper claims to have sent keys. Restrict it to READY→open.
|
||||
movement = {'action_observed': False, 'physical_motion_observed': False,
|
||||
|
||||
@@ -285,7 +285,7 @@ def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time
|
||||
check()
|
||||
pulse('Tab', 0.05)
|
||||
wait_for('INVENTORY_OPEN', 4)
|
||||
open_until = clock() + .5
|
||||
open_until = clock() + 3.0
|
||||
while clock() < open_until:
|
||||
sleep(min(.05, open_until - clock()))
|
||||
check()
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reproduce stock inventory UI behavior in a diagnostic-only vanilla-dependent addon.
|
||||
Never loads successor addons or edits installed/original content. Native errors remain failed.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from native_report import analyze
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = ROOT/'tests/native/inventory_control'
|
||||
HELPERS = ('headless_workbench.py','headless_game.py','private_prefix_cleanup.py','native_provenance.py','source_manifest.py','native_report.py','archive_native.py')
|
||||
EXPECTED_ERROR = 'OverlayWidget Frame0 has invalid slot GridWidgetSlot for slot manipulation function used, expected: LayoutSlot'
|
||||
|
||||
|
||||
def fixture_inventory(folder):
|
||||
if folder.is_symlink() or not folder.is_dir():
|
||||
raise ValueError('Control fixture must be a regular directory')
|
||||
result = {}
|
||||
for path in sorted(folder.rglob('*')):
|
||||
if path.is_symlink():
|
||||
raise ValueError('Control fixture must not contain symlinks')
|
||||
if path.is_file():
|
||||
if path.suffix == '.gproj' and path != folder/'addon.gproj':
|
||||
raise ValueError('Control fixture must contain only its root addon project')
|
||||
if path.suffix not in {'.c','.gproj','.et','.ent','.layer','.meta','.conf','.md'}:
|
||||
raise ValueError('Unexpected control fixture input: '+str(path))
|
||||
result[str(path.relative_to(folder))] = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
elif not path.is_dir():
|
||||
raise ValueError('Control fixture must contain only regular source files/directories')
|
||||
if not result or 'addon.gproj' not in result:
|
||||
raise ValueError('Missing diagnostic fixture')
|
||||
project = (folder/'addon.gproj').read_text()
|
||||
guids = re.findall(r'\bGUID\s+"([A-F0-9]{16})"', project)
|
||||
dependencies = re.findall(r'\bDependencies\s*\{([^{}]*)\}', project)
|
||||
if (len(guids) != 1 or len(re.findall(r'\bGUID\b', project)) != 1
|
||||
or len(dependencies) != 1 or len(re.findall(r'\bDependencies\b', project)) != 1
|
||||
or not re.fullmatch(r'\s*"58D0FB3206B6F859"\s*', dependencies[0])):
|
||||
raise ValueError('Control must depend only on installed vanilla')
|
||||
guid = guids[0]
|
||||
if guid in {'5614BBCCBB55ED1C','58D0FB3206B6F859','2528466DE220D91C','5192138CB9D5B911','D31039C7C53E348A','FA804F411BDDF2C6','9AAEE9041D79809E'}:
|
||||
raise ValueError('Control must use its own addon identity')
|
||||
return result,guid
|
||||
|
||||
|
||||
def harness_source(source):
|
||||
replacements = {
|
||||
'ERR_ValidateBaseWorldPlugin':'ERRIC_InventoryControlPlugin',
|
||||
'Worlds/ERR_Everon.ent':'Worlds/ERRIC_Inventory.ent',
|
||||
"'-errPlay', '1'":"'-errPlay', '1', '-erricInventoryControl', '1'",
|
||||
}
|
||||
for old,new in replacements.items():
|
||||
if old not in source:
|
||||
raise ValueError('Known hidden harness entry changed: '+old)
|
||||
source = source.replace(old,new)
|
||||
return source
|
||||
|
||||
|
||||
def control_evidence(content,guid):
|
||||
report = analyze(content, 'console.log')
|
||||
blocks = []
|
||||
for section in content.split('Loaded addons:')[1:]:
|
||||
block = []
|
||||
for line in section.splitlines()[1:]:
|
||||
match = re.search(r"ENGINE\s*:\s*gproj: .*? guid: '([A-F0-9]{16})'\s*$", line)
|
||||
if not match: break
|
||||
block.append(match[1])
|
||||
blocks.append(block)
|
||||
guids = blocks[0] if blocks else []
|
||||
isolation = bool(blocks) and all(len(block)==3 and set(block)=={'5614BBCCBB55ED1C','58D0FB3206B6F859',guid} for block in blocks)
|
||||
errors = report['engine_errors']
|
||||
target_errors = [error for error in errors if re.search(r'\bGUI\s+\(E\)', error) and EXPECTED_ERROR in error]
|
||||
other_errors = [error for error in errors if error not in target_errors]
|
||||
target_pair = (isolation and report['smoke_completions'] == 1
|
||||
and not report['script_or_runtime_failures'] and not report['missing_end_screen']
|
||||
and len(target_errors) == 2)
|
||||
reproduced = (isolation and report['smoke_completions']==1 and not report['script_or_runtime_failures'] and not report['missing_end_screen']
|
||||
and len(errors)==2 and all(EXPECTED_ERROR in error for error in errors))
|
||||
return {'loaded_addon_guids':guids,'loaded_addon_blocks':blocks,'only_vanilla_and_control_loaded':isolation,
|
||||
'target_gui_error_count':len(target_errors),'target_gui_pair_observed':target_pair,
|
||||
'other_engine_errors':other_errors,
|
||||
'stock_slot_error_reproduced':reproduced,'native_clean':not errors and not report['script_or_runtime_failures'],
|
||||
'native_report':report,'successor_or_release_qualified':False}
|
||||
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--compat',type=Path,default=Path('/tmp/erri-headless-compat'))
|
||||
p.add_argument('--profile',type=Path,default=Path('/tmp/erri-inventory-control-profile'))
|
||||
p.add_argument('--timeout',type=int,default=150)
|
||||
p.add_argument('--archive-name',required=True)
|
||||
a=p.parse_args()
|
||||
if a.compat != Path('/tmp/erri-headless-compat') or a.compat.resolve()!=a.compat:
|
||||
p.error('Only the established disposable test prefix is supported')
|
||||
if not 90<=a.timeout<=300 or not re.fullmatch(r'inventory-control-[a-z0-9-]+',a.archive_name):
|
||||
p.error('Use a90..300 second bound and a unique inventory-control-* archive name')
|
||||
dest=ROOT/'docs/evidence'/a.archive_name
|
||||
if dest.exists() or dest.is_symlink():p.error('Archive must not preexist')
|
||||
profile=a.profile
|
||||
if profile != profile.resolve() or not profile.is_relative_to('/tmp') or profile==Path('/tmp') or profile.is_relative_to(a.compat) or a.compat.is_relative_to(profile):
|
||||
p.error('Use a separate canonical disposable /tmp profile')
|
||||
marker=profile/'.erri-inventory-control-profile.json';marker_value={'kind':'erri-vanilla-inventory-control','version':1}
|
||||
if profile.exists() and (not marker.is_file() or marker.is_symlink() or json.loads(marker.read_text())!=marker_value):
|
||||
p.error('Existing profile is not owned by this control')
|
||||
original,guid=fixture_inventory(FIXTURE)
|
||||
bundle=Path(tempfile.mkdtemp(prefix='erri-inventory-control-run-'))
|
||||
(bundle/'addons').mkdir();shutil.copytree(FIXTURE,bundle/'addons/core');(bundle/'tools').mkdir()
|
||||
for name in HELPERS:shutil.copyfile(ROOT/'tools'/name,bundle/'tools'/name)
|
||||
runner=bundle/'tools/headless_workbench.py';runner.write_text(harness_source(runner.read_text()))
|
||||
profile.mkdir(parents=True,exist_ok=True);marker.write_text(json.dumps(marker_value)+'\n')
|
||||
tool_hashes={name:hashlib.sha256((bundle/'tools'/name).read_bytes()).hexdigest() for name in HELPERS}
|
||||
command=[sys.executable,str(runner),'--compat',str(a.compat),'--profile',str(profile),'--project','core','--smoke','victory','--rounds','1','--timeout',str(a.timeout)]
|
||||
before=set((profile/'logs').glob('logs_*')) if (profile/'logs').exists() else set()
|
||||
with (bundle/'control-launch.log').open('w') as output:
|
||||
result=subprocess.run(command,stdout=output,stderr=subprocess.STDOUT)
|
||||
fresh=set((profile/'logs').glob('logs_*'))-before
|
||||
if len(fresh)!=1:
|
||||
print((bundle/'control-launch.log').read_text(),end='');raise RuntimeError('Control did not produce exactly one session; retained bundle '+str(bundle))
|
||||
session=next(iter(fresh));runner_report=json.loads((session/'runner.json').read_text())
|
||||
if runner_report.get('cleanup_confirmed') is not True or runner_report.get('source_unchanged_after_run') is not True:
|
||||
raise RuntimeError('Control cleanup/source seal incomplete; retained bundle '+str(bundle))
|
||||
subprocess.run([sys.executable,str(bundle/'tools/archive_native.py'),str(session),a.archive_name],check=False)
|
||||
staged=bundle/'docs/evidence'/a.archive_name
|
||||
if not staged.is_dir():raise RuntimeError('Native archive was not created; retained bundle '+str(bundle))
|
||||
for capture in sorted([*session.glob('erric-*.png'), *session.glob('erric-*.bmp')]):
|
||||
if capture.is_file() and not capture.is_symlink(): shutil.copyfile(capture,staged/capture.name)
|
||||
after,_=fixture_inventory(FIXTURE)
|
||||
if original!=after:raise RuntimeError('Authored control fixture changed during run')
|
||||
review=control_evidence((session/'console.log').read_text(errors='replace'),guid)
|
||||
review.update(scope='Scripted stock-character inventory control with vanilla plus one diagnostic addon only; no normal-input, successor gameplay or release qualification.',
|
||||
native_runner_exit=result.returncode,session=str(session),bundle=str(bundle),command=command,
|
||||
fixture_sha256=original,harness_sha256=tool_hashes,cleanup_confirmed=True,source_unchanged_after_run=True)
|
||||
(staged/'control-review.json').write_text(json.dumps(review,indent=2)+'\n')
|
||||
shutil.copyfile(bundle/'control-launch.log',staged/'control-launch.log.txt')
|
||||
# Preserve the actual remapped harness which launched this isolated input.
|
||||
(staged/'harness.py.txt').write_bytes(runner.read_bytes())
|
||||
(staged/'control-extra-sha256.json').write_text(json.dumps({name:hashlib.sha256((staged/name).read_bytes()).hexdigest() for name in ('control-review.json','control-launch.log.txt','harness.py.txt')},indent=2)+'\n')
|
||||
shutil.copytree(staged,dest)
|
||||
print(json.dumps({'archive':str(dest),**review},indent=2))
|
||||
return 0 if review['stock_slot_error_reproduced'] else 1
|
||||
|
||||
|
||||
if __name__=='__main__':raise SystemExit(main())
|
||||
Reference in New Issue
Block a user