#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-3.0-or-later """Failure-injection tests for Phase-1.0AM bounded copy/restore.""" 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 phase10am_bounded_copy_model import * # noqa: E402,F403 def prefix() -> list[FakeCopyEvent]: return [FakeCopyEvent(BACKUP_AUTHID, OK), FakeCopyEvent(BACKUP_CAPS, OK), FakeCopyEvent(SET_PRIV_AUTHID, OK), FakeCopyEvent(SET_PRIV_CAPS, OK)] class BoundedCopyTests(unittest.TestCase): def test_exact_multichunk_success_restores_both_fields(self) -> None: events = prefix() + [FakeCopyEvent(COPY_CHUNK, OK, 4, MORE), FakeCopyEvent(COPY_CHUNK, OK, 6, COMPLETE), FakeCopyEvent(RESTORE_CAPS, OK), FakeCopyEvent(RESTORE_AUTHID, OK)] result = run_copy(CopyPlan(100, 200, 10), FakeCopyFacade(tuple(events))) self.assertTrue(result.success) self.assertEqual(result.copied, 10) self.assertTrue(result.child_alive) self.assertFalse(result.target_copy_performed) self.assertTrue(result.service_available) def test_partial_error_is_reported_and_child_is_reaped(self) -> None: events = prefix() + [FakeCopyEvent(COPY_CHUNK, OK, 4, MORE), FakeCopyEvent(COPY_CHUNK, ERROR, 0, MORE), FakeCopyEvent(RESTORE_CAPS, OK), FakeCopyEvent(RESTORE_AUTHID, OK), FakeCopyEvent(KILL_AND_REAP_CHILD, OK)] result = run_copy(CopyPlan(0, 32, 10), FakeCopyFacade(tuple(events))) self.assertEqual(result.classification, "OFFLINE_PARTIAL_COPY_CONTAINED") self.assertEqual(result.copied, 4) self.assertFalse(result.child_alive) def test_zero_and_oversized_progress_never_complete(self) -> None: for progress in (0, 11): events = prefix() + [FakeCopyEvent(COPY_CHUNK, OK, progress, MORE), FakeCopyEvent(RESTORE_CAPS, OK), FakeCopyEvent(RESTORE_AUTHID, OK)] with self.subTest(progress=progress): result = run_copy(CopyPlan(0, 32, 10), FakeCopyFacade(tuple(events))) self.assertFalse(result.success) self.assertEqual(result.copied, 0) def test_both_restores_are_attempted_and_failure_contains_service(self) -> None: events = prefix() + [FakeCopyEvent(COPY_CHUNK, ERROR, 0, MORE), FakeCopyEvent(RESTORE_CAPS, ERROR), FakeCopyEvent(RESTORE_AUTHID, ERROR), FakeCopyEvent(KILL_AND_REAP_CHILD, OK), FakeCopyEvent(TERMINATE_SERVICE, OK)] result = run_copy(CopyPlan(0, 32, 10), FakeCopyFacade(tuple(events))) self.assertEqual(result.restore_failure_bits, 3) self.assertFalse(result.child_alive or result.service_available) def test_caps_set_failure_still_restores_authid(self) -> None: events = prefix()[:3] + [FakeCopyEvent(SET_PRIV_CAPS, ERROR), FakeCopyEvent(RESTORE_AUTHID, OK)] result = run_copy(CopyPlan(0, 32, 10), FakeCopyFacade(tuple(events))) self.assertEqual(result.classification, "OFFLINE_COPY_REJECTED_BEFORE_MUTATION") self.assertTrue(result.child_alive and result.service_available) def test_restore_or_child_termination_failure_is_hard(self) -> None: events = prefix() + [FakeCopyEvent(COPY_CHUNK, OK, 1, MORE), FakeCopyEvent(RESTORE_CAPS, OK), FakeCopyEvent(RESTORE_AUTHID, OK), FakeCopyEvent(KILL_AND_REAP_CHILD, ERROR)] with self.assertRaises(CopyModelError): run_copy(CopyPlan(0, 32, 10), FakeCopyFacade(tuple(events))) def test_bounds_deadline_status_and_arithmetic(self) -> None: for args in ((0, 0, 0), (MAX_U64, 0, 2), (0, MAX_U64, 2)): with self.subTest(args=args), self.assertRaises(CopyModelError): CopyPlan(*args) events = prefix() events[0] = FakeCopyEvent(BACKUP_AUTHID, OK, ticks=MAX_TICKS) result = run_copy(CopyPlan(0, 32, 10), FakeCopyFacade(tuple(events[:2]))) self.assertFalse(result.success) incomplete = prefix() + [FakeCopyEvent(COPY_CHUNK, OK, 9, COMPLETE), FakeCopyEvent(RESTORE_CAPS, OK), FakeCopyEvent(RESTORE_AUTHID, OK), FakeCopyEvent(KILL_AND_REAP_CHILD, OK)] result = run_copy(CopyPlan(0, 32, 10), FakeCopyFacade(tuple(incomplete))) self.assertFalse(result.success) if __name__ == "__main__": unittest.main(argv=[__file__])