#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-3.0-or-later """Failure-injection tests for the Phase-1.0AF lifecycle model.""" from __future__ import annotations import argparse from pathlib import Path import sys import unittest PARSER = argparse.ArgumentParser() PARSER.add_argument("--root", type=Path, required=True) ROOT = PARSER.parse_args().root sys.path.insert(0, str(ROOT / "tools")) from phase10af_bigapp_lifecycle_model import * # noqa: E402,F403 def happy() -> list[FakeEvent]: return [FakeEvent(CHECK_NO_BIGAPP, NONE), FakeEvent(ATTACH_PARENT, OK), FakeEvent(ARM_FORK, OK), FakeEvent(CONTINUE_PARENT, OK), FakeEvent(LAUNCH_FIXED_TITLE, OK), FakeEvent(AWAIT_UNIQUE_CHILD, CHILD, child_id=4242), FakeEvent(DETACH_PARENT, OK), FakeEvent(ARM_EXEC, OK), FakeEvent(CONTINUE_CHILD, OK), FakeEvent(AWAIT_EXEC, OK), FakeEvent(REPLACE_EXACT_PAYLOAD, OK), FakeEvent(RESTORE_MUTATIONS, OK), FakeEvent(DETACH_CHILD, OK), FakeEvent(EMIT_RESULT, OK)] class LifecycleTests(unittest.TestCase): def test_success(self) -> None: result = run_lifecycle(FakeLifecycleFacade(tuple(happy()))) self.assertTrue(result.success) self.assertEqual(result.child_id, 4242) self.assertTrue(result.parent_detached and result.child_detached) self.assertTrue(result.mutations_restored) self.assertFalse(result.child_terminated or result.existing_bigapp_killed) def test_existing_bigapp_fails_without_kill(self) -> None: result = run_lifecycle(FakeLifecycleFacade((FakeEvent(CHECK_NO_BIGAPP, EXISTS),))) self.assertFalse(result.success) self.assertFalse(result.existing_bigapp_killed) self.assertEqual(result.child_id, 0) def test_parent_stage_failures_detach(self) -> None: for index in range(2, 5): events = happy()[:index + 1] events[index] = FakeEvent(events[index].operation, ERROR) events = events[:index + 1] + [FakeEvent(DETACH_PARENT, OK)] with self.subTest(index=index): result = run_lifecycle(FakeLifecycleFacade(tuple(events))) self.assertFalse(result.success) self.assertTrue(result.parent_detached) def test_child_stage_failures_terminate_only_child(self) -> None: for index in range(7, 10): events = happy()[:index + 1] events[index] = FakeEvent(events[index].operation, ERROR) events = events[:index + 1] + [FakeEvent(TERMINATE_CHILD, OK)] with self.subTest(index=index): result = run_lifecycle(FakeLifecycleFacade(tuple(events))) self.assertFalse(result.success) self.assertEqual(result.child_id, 4242) self.assertTrue(result.child_terminated) def test_replace_failure_restores_then_terminates(self) -> None: events = happy()[:11] events[10] = FakeEvent(REPLACE_EXACT_PAYLOAD, ERROR) events += [FakeEvent(RESTORE_MUTATIONS, OK), FakeEvent(TERMINATE_CHILD, OK)] result = run_lifecycle(FakeLifecycleFacade(tuple(events))) self.assertFalse(result.success) self.assertTrue(result.mutations_restored and result.child_terminated) self.assertLess(result.trace.index("RESTORE_MUTATIONS:OK"), result.trace.index("TERMINATE_CHILD:OK")) def test_timeout_is_failure(self) -> None: events = happy()[:6] events[5] = FakeEvent(AWAIT_UNIQUE_CHILD, TIMEOUT) events += [FakeEvent(DETACH_PARENT, OK)] result = run_lifecycle(FakeLifecycleFacade(tuple(events))) self.assertFalse(result.success) def test_main_tick_budget(self) -> None: events = happy() events[0] = FakeEvent(CHECK_NO_BIGAPP, NONE, ticks=64) events = events[:2] + [FakeEvent(DETACH_PARENT, OK)] with self.assertRaises(LifecycleModelError): run_lifecycle(FakeLifecycleFacade(tuple(events))) def test_cleanup_failure_is_hard_error(self) -> None: events = happy()[:3] events[2] = FakeEvent(ARM_FORK, ERROR) events += [FakeEvent(DETACH_PARENT, ERROR)] with self.assertRaises(LifecycleModelError): run_lifecycle(FakeLifecycleFacade(tuple(events))) def test_unused_or_wrong_order_is_rejected(self) -> None: result = run_lifecycle(FakeLifecycleFacade((FakeEvent(ATTACH_PARENT, OK),))) self.assertFalse(result.success) with self.assertRaises(LifecycleModelError): run_lifecycle(FakeLifecycleFacade(tuple(happy() + [FakeEvent(EMIT_RESULT, OK)]))) if __name__ == "__main__": unittest.main(argv=[__file__])