From 2e6fcceb05ed2e507b454a3d522cfdafc93a5fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Tue, 10 Nov 2026 16:48:17 +0500 Subject: [PATCH] CI: drop the launcher bridge test (the bridge was removed) tools/test_launcher_bridge.py tested crates/coa-core/src/launcher_bridge.py, which the Rust launcher replaced; check.yml and release.yml still ran it, so the release build failed. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/check.yml | 1 - .github/workflows/release.yml | 1 - tools/test_launcher_bridge.py | 223 ---------------------------------- 3 files changed, 225 deletions(-) delete mode 100644 tools/test_launcher_bridge.py diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 20d4693..c2309b4 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -26,5 +26,4 @@ jobs: - run: node tools/check-i18n.mjs - run: npm run build - run: node --experimental-strip-types --test tools/*.test.mjs - - run: python -B -m unittest discover -s tools -p test_launcher_bridge.py - run: cargo test --workspace --locked diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d53e7ed..7df8ba6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,7 +74,6 @@ jobs: - name: Tests run: cargo test --workspace --locked -- --test-threads=1 - run: node --experimental-strip-types --test tools/*.test.mjs - - run: python -B -m unittest discover -s tools -p test_launcher_bridge.py - name: Build and sign (publish only when requested) id: tauri uses: tauri-apps/tauri-action@v0 diff --git a/tools/test_launcher_bridge.py b/tools/test_launcher_bridge.py deleted file mode 100644 index 94c1637..0000000 --- a/tools/test_launcher_bridge.py +++ /dev/null @@ -1,223 +0,0 @@ -import pathlib -import runpy -import sys -import tempfile -import unittest -import time -from unittest.mock import patch - -BRIDGE = pathlib.Path(__file__).resolve().parents[1] / 'crates/coa-core/src/launcher_bridge.py' - - -class LauncherBridgeTests(unittest.TestCase): - def launch(self, action, body): - with tempfile.TemporaryDirectory() as directory: - script = pathlib.Path(directory) / 'Scripts/manage.py' - script.parent.mkdir() - script.write_text(body, encoding='utf8') - original = runpy.run_path - def capture(*args, **kwargs): - self.launcher = original(*args, **kwargs) - return self.launcher - with patch.object(sys, 'argv', ['bridge', str(script), action]), patch.object(runpy, 'run_path', side_effect=capture): - return original(str(BRIDGE)) - - def shutdown_fixture(self, *, console_error='RuntimeError', stopped=True, foreign=False, same=True, modern=False, terminate_error=False): - return f''' -from pathlib import Path -from contextlib import nullcontext -calls = [] -world = {{'pid': 123, 'created': 456, 'exe': str(Path(__file__).resolve().parents[1] / {'"foreign.exe"' if foreign else '"Core/worldserver.exe"'})}} -def wait_ready(*args): pass -def start_mysql(): raise AssertionError('must not start') -def main(): raise AssertionError('must not prepare') -def settings(): return {{}} -def control_lock(): return nullcontext() -def process(name): return world if name == 'world' else None -def same_process(record): return {same!r} -def ra(command, config): - calls.append('console') - {'raise '+console_error+'("Start this repack worldserver first")' if console_error else 'return "Graceful shutdown"'} -def terminate(record): - assert record is world - calls.append('terminate') - {'raise RuntimeError("identity changed")' if terminate_error else 'pass'} -def wait_stopped(record, seconds): - assert record is world - calls.append('wait') - return {stopped!r} -def stop_all(config): - record = process('world') - if record: - if {modern!r}: - try: ra('server shutdown 1s 0', config) - except (OSError, RuntimeError): terminate(record) - else: - ra('server shutdown 1s 0', config) - if not wait_stopped(record, 60): raise RuntimeError('world still alive') - calls.append('mysql-shutdown') -''' - - def test_unavailable_console_in_restored_legacy_launcher_stops_owned_world_before_mysql(self): - for error in ['RuntimeError', 'OSError']: - with self.subTest(error=error): - result = self.launch('stop-all', self.shutdown_fixture(console_error=error)) - self.assertEqual(result['launcher']['calls'], ['console', 'terminate', 'wait', 'wait', 'mysql-shutdown']) - - def test_force_stop_terminates_only_the_owned_world_before_mysql(self): - body = self.shutdown_fixture(console_error=None) - body = body.replace("return world if name == 'world' else None", "return world if name == 'world' and 'terminate' not in calls else None") - result = self.launch('force-stop-all', body) - self.assertEqual(result['launcher']['calls'], ['terminate', 'wait', 'mysql-shutdown']) - - def test_force_stop_rejects_foreign_or_reused_pids_and_a_world_that_stays_alive(self): - for options, message in [({'foreign': True}, 'ownership could not be verified'), - ({'same': False}, 'ownership could not be verified'), - ({'stopped': False}, 'database was left running')]: - with self.subTest(options=options): - with self.assertRaisesRegex(Exception, message): - self.launch('force-stop-all', self.shutdown_fixture(**options)) - self.assertNotIn('mysql-shutdown', self.launcher['calls']) - - def test_healthy_console_keeps_graceful_shutdown(self): - result = self.launch('stop-all', self.shutdown_fixture(console_error=None)) - self.assertEqual(result['launcher']['calls'], ['console', 'wait', 'mysql-shutdown']) - - def test_foreign_or_reused_process_is_never_terminated_even_by_a_modern_launcher_catch(self): - for modern in [False, True]: - for identity in [{'foreign': True}, {'same': False}]: - with self.subTest(modern=modern, **identity): - with self.assertRaisesRegex(Exception, 'ownership could not be verified'): - self.launch('stop-all', self.shutdown_fixture(modern=modern, **identity)) - self.assertEqual(self.launcher['calls'], ['console']) - - def test_world_that_stays_alive_blocks_database_shutdown(self): - for modern in [False, True]: - with self.subTest(modern=modern): - with self.assertRaisesRegex(Exception, 'database was left running'): - self.launch('stop-all', self.shutdown_fixture(stopped=False, modern=modern)) - self.assertEqual(self.launcher['calls'], ['console', 'terminate', 'wait']) - - def test_termination_identity_failure_leaves_mysql_untouched(self): - for modern in [False, True]: - with self.subTest(modern=modern): - with self.assertRaisesRegex(Exception, 'could not be stopped safely'): - self.launch('stop-all', self.shutdown_fixture(terminate_error=True, modern=modern)) - self.assertEqual(self.launcher['calls'], ['console', 'terminate']) - - def test_slow_mysql_gets_time_to_finish_but_other_waits_are_unchanged(self): - result = self.launch('start-mysql', ''' -calls = [] -def wait_ready(name, port, seconds=180): - calls.append((name, seconds)) - if name == 'mysql' and seconds < 162: - raise RuntimeError('mysql is still starting') -def start_mysql(): - wait_ready('mysql', 12345, 60) -def main(): - start_mysql() - wait_ready('world', 23456, 180) - wait_ready('mysql', 12345, 900) -''') - calls = result['launcher']['calls'] - self.assertEqual(calls[0], ('mysql', 300)) - self.assertEqual(calls[2], ('mysql', 900)) - self.assertEqual(calls[1][0], 'world') - self.assertAlmostEqual(calls[1][1], 1500, delta=1) - - def world_start(self, *, game_at=420, console_at=480, dies_at=None, requested=180): - clock = [0.0] - def sleep(seconds): clock[0] += seconds - body = f''' -import time -calls = [] -dies_at = {dies_at!r} -def wait_ready(name, port, seconds=180): - until = time.monotonic() + seconds - while time.monotonic() < until: - if dies_at is not None and time.monotonic() >= dies_at: - raise RuntimeError('world stopped during startup') - if time.monotonic() >= ({game_at!r} if port == 1 else {console_at!r}): - calls.append(port) - return - time.sleep(0.5) - raise RuntimeError('world is still starting') -def start_mysql(): pass -def settings(): return {{}} -def process(name): return {{'pid': 1}} -def ra(command, config): return 'server info ready' -def main(): - wait_ready('world', 1, {requested}) - wait_ready('world', 2, {requested}) -''' - with patch('time.monotonic', side_effect=lambda: clock[0]), patch('time.sleep', side_effect=sleep): - result = self.launch('start-all', body) - return clock[0], result['launcher']['calls'] - - def test_open_ports_do_not_finish_startup_until_commands_are_processed(self): - clock = [0.0] - def sleep(seconds): clock[0] += seconds - body = """ -import time -calls=[] -def wait_ready(*args): pass -def start_mysql(): pass -def settings(): return {} -def process(name): return {'pid': 1} -def ra(command, config): - calls.append(command) - if time.monotonic() < 5: raise TimeoutError('main loop is loading') - return 'ready' -def main(): - wait_ready('world',1) - wait_ready('world',2) -""" - with patch('time.monotonic', side_effect=lambda: clock[0]), patch('time.sleep', side_effect=sleep): - result=self.launch('start-all',body) - self.assertEqual(clock[0],5) - self.assertEqual(set(result['launcher']['calls']),{'server info'}) - - def test_world_death_after_ports_open_is_a_startup_failure(self): - body=""" -def wait_ready(*args): pass -def start_mysql(): pass -def settings(): return {} -def process(name): return None -def main(): pass -""" - with self.assertRaisesRegex(RuntimeError,'stopped before its console accepted'): - self.launch('start-world',body) - - def test_legacy_coa_world_waits_for_both_ports_after_three_minutes(self): - elapsed, calls = self.world_start() - self.assertEqual((elapsed, calls), (480, [1, 2])) - - def test_console_wait_shares_the_world_deadline(self): - with self.assertRaisesRegex(RuntimeError, 'still starting'): - self.world_start(game_at=1490, console_at=1510) - self.assertEqual(self.launcher['calls'], [1]) - - def test_world_exit_is_reported_without_waiting_out_the_extended_budget(self): - with self.assertRaisesRegex(RuntimeError, 'stopped during startup'): - self.world_start(dies_at=12) - self.assertEqual(self.launcher['calls'], []) - - def test_existing_longer_world_timeout_is_preserved(self): - self.assertEqual(self.world_start(game_at=1600, console_at=1700, requested=1800), (1700, [1, 2])) - - def test_stop_dispatch_does_not_prepare_or_start_anything(self): - result = self.launch('stop-all', ''' -from contextlib import nullcontext -calls = [] -def wait_ready(*args): raise AssertionError('must not wait') -def start_mysql(): raise AssertionError('must not start') -def main(): raise AssertionError('must not prepare') -def settings(): return {'mysqlPort': 12345} -def control_lock(): return nullcontext() -def stop_all(config): calls.append(config['mysqlPort']) -''') - self.assertEqual(result['launcher']['calls'], [12345]) - - -if __name__ == '__main__': - unittest.main()