Retain aircraft through native landing and qualify bounded input diagnostics
This commit is contained in:
@@ -114,7 +114,9 @@ class NativePackageTests(unittest.TestCase):
|
||||
(folder/'external.pak').symlink_to(self.source/'addons/core/addon.gproj')
|
||||
with self.assertRaises(ValueError): n.package_inventory(folder)
|
||||
|
||||
def native_double(self, authored, timeout=30, mutate=False, failure=None, native_code=0, write_status=True, launcher_code=0):
|
||||
def native_double(self, authored, timeout=30, mutate=False, failure=None, native_code=0, write_status=True, launcher_code=0, silent_workbench=False, exit_after_init=False, cleanup_proof=None, prefix_remains_active=False):
|
||||
if cleanup_proof is None:
|
||||
cleanup_proof = {'cleanup_confirmed': True, 'idle_verified': True, 'errors': [], 'orphan_cleanup': {'attempted': False}}
|
||||
child = Mock(); child.poll.return_value = launcher_code
|
||||
def spawn(*args, **kwargs):
|
||||
self.assertNotIn('WINEPREFIX', kwargs['env'])
|
||||
@@ -124,7 +126,7 @@ class NativePackageTests(unittest.TestCase):
|
||||
(folder/'console.log').write_text('SYNTHETIC NATIVE PROCESS DOUBLE\n'+(failure or ''))
|
||||
(self.session/'packages/core/data.pak').write_bytes(b'SYNTHETIC TEST ONLY')
|
||||
if write_status:
|
||||
native_command = n.command_for(self.steam, self.session, 'core')
|
||||
native_command = n.command_for(self.steam, self.session, 'core', silent_workbench, exit_after_init)
|
||||
native_command = native_command[native_command.index('--')+1:]
|
||||
(self.session/'evidence/core/native-exit.json').write_text(json.dumps({'command': native_command, 'exit_code': native_code}))
|
||||
if mutate:
|
||||
@@ -132,11 +134,17 @@ class NativePackageTests(unittest.TestCase):
|
||||
return child
|
||||
def check_output(args, **kwargs):
|
||||
return '[]' if 'sink-inputs' in args else '987'
|
||||
with patch.object(n, 'assert_no_game'), patch.object(n, 'assert_prefix_idle'), \
|
||||
patch.object(n, 'cleanup') as cleanup, patch.object(n.subprocess, 'Popen', side_effect=spawn), \
|
||||
probes = 0
|
||||
def idle(*args):
|
||||
nonlocal probes
|
||||
probes += 1
|
||||
if prefix_remains_active and probes > 2:
|
||||
raise ValueError('synthetic adopted helper remains after native exit')
|
||||
with patch.object(n, 'assert_no_game'), patch.object(n, 'assert_prefix_idle', side_effect=idle), \
|
||||
patch.object(n, 'cleanup_owned_run', return_value=cleanup_proof) as cleanup, patch.object(n.subprocess, 'Popen', side_effect=spawn), \
|
||||
patch.object(n.subprocess, 'check_output', side_effect=check_output), patch.object(n.subprocess, 'run') as process:
|
||||
result = n.run_project(self.steam, self.compat, self.session, 'core', timeout, authored)
|
||||
cleanup.assert_called_once()
|
||||
result = n.run_project(self.steam, self.compat, self.session, 'core', timeout, authored, silent_workbench, exit_after_init)
|
||||
cleanup.assert_called_once_with(child, Path(n.command_for(self.steam, self.session, 'core')[n.command_for(self.steam, self.session, 'core').index('--')+1]), unittest.mock.ANY, self.compat)
|
||||
self.assertTrue(any(call.args[0] == ['pactl', 'unload-module', '987'] for call in process.call_args_list))
|
||||
return result
|
||||
|
||||
@@ -179,7 +187,7 @@ class NativePackageTests(unittest.TestCase):
|
||||
probes += 1
|
||||
if probes > 2: raise ValueError('synthetic owned prefix child remains')
|
||||
with patch.object(n, 'assert_no_game'), patch.object(n, 'assert_prefix_idle', side_effect=idle), \
|
||||
patch.object(n, 'cleanup') as cleanup, patch.object(n.subprocess, 'Popen', return_value=child), \
|
||||
patch.object(n, 'cleanup_owned_run', return_value={'cleanup_confirmed': False, 'idle_verified': False, 'errors': ['synthetic owned prefix child remains']}) as cleanup, patch.object(n.subprocess, 'Popen', return_value=child), \
|
||||
patch.object(n.subprocess, 'check_output', side_effect=lambda args, **kw: '[]' if 'sink-inputs' in args else '987'), \
|
||||
patch.object(n.subprocess, 'run'), patch.object(n.time, 'monotonic', side_effect=itertools.count(0, 6)), patch.object(n.time, 'sleep'):
|
||||
report = n.run_project(self.steam, self.compat, self.session, 'core', 100, authored)
|
||||
@@ -225,14 +233,14 @@ class NativePackageTests(unittest.TestCase):
|
||||
copy.assert_not_called()
|
||||
self.assertFalse(fresh.exists())
|
||||
|
||||
def orchestration_double(self, reports, continuation=True, idle=None):
|
||||
def orchestration_double(self, reports, continuation=True, idle=None, exit_after_init=False):
|
||||
import contextlib
|
||||
import io
|
||||
import uuid
|
||||
fresh = Path('/tmp')/('erri-package-orchestration-'+uuid.uuid4().hex)
|
||||
self.addCleanup(lambda: shutil.rmtree(fresh, ignore_errors=True))
|
||||
args = argparse.Namespace(steam=self.steam, compat=self.compat, session=fresh,
|
||||
project='chat', timeout=30, continue_on_failure=continuation)
|
||||
project='chat', timeout=30, continue_on_failure=continuation, exit_after_init=exit_after_init)
|
||||
with patch.object(n, 'assert_no_game'), patch.object(n, 'assert_prefix_idle', side_effect=idle), \
|
||||
patch.object(n.shutil, 'which', return_value='/mock'), \
|
||||
patch.object(n, 'run_project', side_effect=reports) as run, \
|
||||
@@ -387,6 +395,141 @@ class NativePackageTests(unittest.TestCase):
|
||||
self.assertIn('-packAddon', silent)
|
||||
self.assertIn('-noBackend', silent)
|
||||
|
||||
def test_exit_after_init_changes_only_documented_lifecycle_flag(self):
|
||||
ordinary = n.command_for(self.steam, self.session, 'core')
|
||||
experiment = n.command_for(self.steam, self.session, 'core', exit_after_init=True)
|
||||
self.assertEqual(experiment, ordinary + ['-exitAfterInit'])
|
||||
self.assertNotIn('-wbSilent', experiment)
|
||||
self.assertNotIn('-run', experiment)
|
||||
with self.assertRaisesRegex(ValueError, 'only one'):
|
||||
n.command_for(self.steam, self.session, 'core', True, True)
|
||||
with patch.object(n, 'run', return_value=1) as run:
|
||||
base = ['--session', '/tmp/not-launched', '--compat', str(self.compat)]
|
||||
n.main(base)
|
||||
self.assertFalse(run.call_args.args[0].silent_workbench)
|
||||
self.assertFalse(run.call_args.args[0].exit_after_init)
|
||||
n.main(base + ['--silent-workbench'])
|
||||
self.assertTrue(run.call_args.args[0].silent_workbench)
|
||||
self.assertFalse(run.call_args.args[0].exit_after_init)
|
||||
n.main(base + ['--exit-after-init'])
|
||||
self.assertTrue(run.call_args.args[0].exit_after_init)
|
||||
self.assertFalse(run.call_args.args[0].silent_workbench)
|
||||
run.reset_mock()
|
||||
with self.assertRaises(SystemExit) as refused:
|
||||
n.main(base + ['--silent-workbench', '--exit-after-init'])
|
||||
self.assertEqual(refused.exception.code, 2)
|
||||
run.assert_not_called()
|
||||
|
||||
def test_exit_after_init_forwards_across_dependency_closure(self):
|
||||
code, summary, calls = self.orchestration_double(
|
||||
[self.completed_report(True) for _ in range(5)], exit_after_init=True)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(len(calls), 5)
|
||||
self.assertTrue(all(call.args[-2:] == (False, True) for call in calls))
|
||||
self.assertFalse(summary['release_qualified'])
|
||||
|
||||
def test_exit_after_init_records_exact_command_and_retains_shutdown_errors(self):
|
||||
authored = n.copy_payload(self.source, self.session)
|
||||
report = self.native_double(authored, launcher_code=None, exit_after_init=True,
|
||||
failure='RESOURCES (E): synthetic shutdown UI leak')
|
||||
launch = json.loads((self.session/'evidence/core/launch.json').read_text())
|
||||
self.assertEqual(launch['native_command'][-1], '-exitAfterInit')
|
||||
self.assertTrue(launch['exit_after_init_experiment'])
|
||||
self.assertTrue(report['exit_after_init_experiment'])
|
||||
self.assertFalse(report['silent_workbench_experiment'])
|
||||
status = json.loads((self.session/'evidence/core/native-exit.json').read_text())
|
||||
self.assertEqual(status['command'], launch['native_command'])
|
||||
self.assertEqual(report['native_exit_code'], 0)
|
||||
self.assertEqual(report['completion_observed'], 'native-child-exited')
|
||||
self.assertFalse(report['candidate_created'])
|
||||
self.assertTrue(report['errors'])
|
||||
self.assertFalse(report['release_qualified'])
|
||||
console = self.session/'profiles/core/logs/logs_test/console.log'
|
||||
self.assertIn('synthetic shutdown UI leak', console.read_text())
|
||||
self.assertEqual(report['logs'][str(console)]['sha256'], n.digest(console))
|
||||
|
||||
def test_exit_after_init_without_recorded_native_exit_still_refuses(self):
|
||||
authored = n.copy_payload(self.source, self.session)
|
||||
report = self.native_double(authored, exit_after_init=True, write_status=False)
|
||||
self.assertFalse(report['candidate_created'])
|
||||
self.assertIsNone(report['native_exit_code'])
|
||||
|
||||
def test_exit_after_init_timeout_keeps_error_and_cleanup_rules(self):
|
||||
authored = n.copy_payload(self.source, self.session)
|
||||
report = self.native_double(authored, exit_after_init=True, timeout=0)
|
||||
self.assertEqual(report['completion_observed'], 'timeout')
|
||||
self.assertFalse(report['candidate_created'])
|
||||
self.assertTrue(report['cleanup_confirmed'])
|
||||
|
||||
def test_shared_cleanup_report_preserves_partial_refusal_and_native_errors(self):
|
||||
authored = n.copy_payload(self.source, self.session)
|
||||
proof = {'cleanup_confirmed': False, 'idle_verified': False,
|
||||
'errors': ['private helper cleanup: ambiguous live process'],
|
||||
'orphan_cleanup': {'attempted': True, 'report': {'cleared': False, 'term_sent': [123]}}}
|
||||
report = self.native_double(authored, launcher_code=None,
|
||||
failure='RESOURCES (E): retained UI leak', cleanup_proof=proof)
|
||||
self.assertEqual(report['cleanup_report'], proof)
|
||||
self.assertFalse(report['cleanup_confirmed'])
|
||||
self.assertFalse(report['candidate_created'])
|
||||
self.assertIn('ambiguous live process', report['runner_error'])
|
||||
self.assertTrue(any('retained UI leak' in error for error in report['errors']))
|
||||
self.assertEqual(report['native_exit_code'], 0)
|
||||
self.assertEqual(report['completion_observed'], 'native-child-exited')
|
||||
|
||||
def test_successful_adopted_cleanup_cannot_upgrade_timeout(self):
|
||||
authored = n.copy_payload(self.source, self.session)
|
||||
proof = {'cleanup_confirmed': True, 'idle_verified': True, 'errors': [],
|
||||
'orphan_cleanup': {'attempted': True, 'report': {'cleared': True, 'term_sent': [123], 'kill_sent': []}}}
|
||||
report = self.native_double(authored, timeout=0, cleanup_proof=proof)
|
||||
self.assertEqual(report['cleanup_report'], proof)
|
||||
self.assertTrue(report['cleanup_confirmed'])
|
||||
self.assertEqual(report['completion_observed'], 'timeout')
|
||||
self.assertFalse(report['candidate_created'])
|
||||
|
||||
def test_post_run_cleanup_does_not_reclassify_prefix_active_native_exit(self):
|
||||
import itertools
|
||||
authored = n.copy_payload(self.source, self.session)
|
||||
proof = {'cleanup_confirmed': True, 'idle_verified': True, 'errors': [],
|
||||
'orphan_cleanup': {'attempted': True, 'report': {'cleared': True, 'term_sent': [123]}}}
|
||||
with patch.object(n.time, 'monotonic', side_effect=itertools.count(0, 2)), patch.object(n.time, 'sleep'):
|
||||
report = self.native_double(authored, timeout=100, launcher_code=None,
|
||||
cleanup_proof=proof, prefix_remains_active=True,
|
||||
failure='RESOURCES (E): synthetic shutdown leak')
|
||||
self.assertEqual(report['native_exit_code'], 0)
|
||||
self.assertEqual(report['completion_observed'], 'native-or-launcher-exited-prefix-active')
|
||||
self.assertTrue(report['cleanup_confirmed'])
|
||||
self.assertFalse(report['candidate_created'])
|
||||
self.assertTrue(report['errors'])
|
||||
self.assertEqual(report['cleanup_report'], proof)
|
||||
|
||||
def test_packaging_uses_exact_shared_cleanup_adapter_and_scoping(self):
|
||||
import headless_game as game
|
||||
import private_prefix_cleanup as private
|
||||
self.assertIs(n.cleanup_owned_run, game.cleanup_owned_run)
|
||||
for compat, expected in ((private.COMPAT, ['normal', 'adopted', 'idle']),
|
||||
(self.compat, ['normal', 'idle'])):
|
||||
events = []
|
||||
with patch.object(game, 'cleanup', side_effect=lambda *args: events.append('normal')), \
|
||||
patch.object(private, 'cleanup_private_prefix', side_effect=lambda *args: events.append('adopted') or {'cleared': True}), \
|
||||
patch.object(game, 'assert_prefix_idle', side_effect=lambda *args: events.append('idle')):
|
||||
report = n.cleanup_owned_run(Mock(), private.INSTALLED_PROTON, {}, compat)
|
||||
self.assertEqual(events, expected)
|
||||
self.assertTrue(report['cleanup_confirmed'])
|
||||
self.assertEqual(report['orphan_cleanup']['attempted'], compat == private.COMPAT)
|
||||
|
||||
def test_failed_native_launch_does_not_call_post_run_cleanup(self):
|
||||
authored = n.copy_payload(self.source, self.session)
|
||||
with patch.object(n, 'assert_no_game'), patch.object(n, 'assert_prefix_idle'), \
|
||||
patch.object(n.subprocess, 'check_output', return_value='987'), \
|
||||
patch.object(n.subprocess, 'run'), \
|
||||
patch.object(n.subprocess, 'Popen', side_effect=OSError('synthetic launch refusal')), \
|
||||
patch.object(n, 'cleanup_owned_run') as cleanup:
|
||||
report = n.run_project(self.steam, self.compat, self.session, 'core', 30, authored)
|
||||
cleanup.assert_not_called()
|
||||
self.assertIsNone(report['cleanup_report'])
|
||||
self.assertEqual(report['completion_observed'], 'runner-failed')
|
||||
self.assertFalse(report['candidate_created'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user