#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-3.0-or-later """Failure-injection tests for Phase-1.0AI mapping transactions.""" 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 phase10ag_bounded_elf import LoadSegment, PF_R, PF_W, PF_X # noqa: E402 from phase10ai_mapping_model import * # noqa: E402,F403 def plan() -> MappingPlan: return MappingPlan(( LoadSegment(PF_R | PF_X, 0x4000, 0, 0x1800, 0x1800, 0x4000), LoadSegment(PF_R, 0x8000, 0x8000, 0x1000, 0x1000, 0x4000), LoadSegment(PF_R | PF_W, 0xc000, 0xc000, 0x800, 0x1800, 0x4000), ), 7) def happy(mapping: MappingPlan | None = None) -> list[FakeMappingEvent]: mapping = mapping or plan() events = [FakeMappingEvent(RESERVE_CHILD, OK, value=mapping.region_size), FakeMappingEvent(CREATE_MIRROR, OK, value=mapping.region_size)] for index, segment in enumerate(mapping.segments): if segment.file_size: events.append(FakeMappingEvent(COPY_FILE_BYTES, OK, index, segment.file_size)) if segment.memory_size > segment.file_size: events.append(FakeMappingEvent(ZERO_BSS, OK, index, segment.memory_size - segment.file_size)) events.append(FakeMappingEvent(APPLY_RELATIVE, OK, value=mapping.relative_relocations)) events.append(FakeMappingEvent(COPY_MIRROR_TO_CHILD, OK, value=mapping.region_size)) for index, segment in enumerate(mapping.segments): events.append(FakeMappingEvent(SET_FINAL_PROTECTION, OK, index, segment.flags)) events += [FakeMappingEvent(SYNC_IMAGE, OK, value=mapping.region_size), FakeMappingEvent(RELEASE_MIRROR, OK)] return events class MappingModelTests(unittest.TestCase): def test_success(self) -> None: mapping = plan() result = run_mapping(mapping, FakeMappingFacade(tuple(happy(mapping)))) self.assertTrue(result.success and result.child_region_retained) self.assertTrue(result.mirror_released) self.assertFalse(result.child_region_unmapped or result.target_mapping_performed) self.assertEqual(result.zeroed_bss_bytes, 0x1000) def test_reserve_failure_has_no_cleanup(self) -> None: mapping = plan() events = (FakeMappingEvent(RESERVE_CHILD, ERROR, value=mapping.region_size),) result = run_mapping(mapping, FakeMappingFacade(events)) self.assertFalse(result.success or result.child_region_unmapped) def test_mirror_creation_failure_unmaps_child(self) -> None: mapping = plan() events = (FakeMappingEvent(RESERVE_CHILD, OK, value=mapping.region_size), FakeMappingEvent(CREATE_MIRROR, ERROR, value=mapping.region_size), FakeMappingEvent(UNMAP_CHILD, OK, value=mapping.region_size)) result = run_mapping(mapping, FakeMappingFacade(events)) self.assertFalse(result.success) self.assertTrue(result.child_region_unmapped) def test_every_post_mirror_failure_releases_and_unmaps(self) -> None: mapping = plan() baseline = happy(mapping) for index in range(2, len(baseline)): failed = baseline[:index + 1] event = failed[index] failed[index] = FakeMappingEvent(event.operation, ERROR, event.segment_index, event.value) failed += [FakeMappingEvent(RELEASE_MIRROR, OK), FakeMappingEvent(UNMAP_CHILD, OK, value=mapping.region_size)] with self.subTest(operation=event.operation, index=index): result = run_mapping(mapping, FakeMappingFacade(tuple(failed))) self.assertFalse(result.success) self.assertTrue(result.mirror_released) self.assertTrue(result.child_region_unmapped) def test_cleanup_failure_is_hard_error(self) -> None: mapping = plan() events = happy(mapping)[:3] event = events[-1] events[-1] = FakeMappingEvent(event.operation, ERROR, event.segment_index, event.value) events.append(FakeMappingEvent(RELEASE_MIRROR, ERROR)) with self.assertRaises(MappingModelError): run_mapping(mapping, FakeMappingFacade(tuple(events))) def test_tick_budget_is_enforced_and_rolled_back(self) -> None: mapping = plan() events = happy(mapping)[:2] first = events[0] events[0] = FakeMappingEvent(first.operation, OK, value=first.value, ticks=128) events += [FakeMappingEvent(UNMAP_CHILD, OK, value=mapping.region_size)] result = run_mapping(mapping, FakeMappingFacade(tuple(events))) self.assertFalse(result.success) self.assertTrue(result.child_region_unmapped) def test_plan_rejects_wx_and_page_protection_overlap(self) -> None: with self.assertRaises(MappingModelError): MappingPlan((LoadSegment(PF_R | PF_W | PF_X, 0, 0, 1, 1, 0x4000),), 1) with self.assertRaises(MappingModelError): MappingPlan((LoadSegment(PF_R | PF_X, 0, 0, 1, 0x3000, 0x4000), LoadSegment(PF_R | PF_W, 0x3000, 0x3000, 1, 1, 0x4000)), 1) def test_wrong_binding_and_unused_events_are_rejected(self) -> None: mapping = plan() events = happy(mapping) events[2] = FakeMappingEvent(COPY_FILE_BYTES, OK, 0, 1) with self.assertRaises(MappingModelError): run_mapping(mapping, FakeMappingFacade(tuple(events))) with self.assertRaises(MappingModelError): run_mapping(mapping, FakeMappingFacade(tuple( happy(mapping) + [FakeMappingEvent(RELEASE_MIRROR, OK)]))) if __name__ == "__main__": unittest.main(argv=[__file__])