121 lines
5.8 KiB
Python
121 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Exhaustive ownership tests for Phase-1.0AK."""
|
|
|
|
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 phase10ak_hybrid_composition import * # noqa: E402,F403
|
|
|
|
|
|
def happy(count: int = 2) -> list[FakeEvent]:
|
|
events = [FakeEvent(CREATE_CHILD, OK), FakeEvent(RESERVE_REGION, OK),
|
|
FakeEvent(CREATE_MIRROR, OK)]
|
|
for segment in range(count):
|
|
events += [FakeEvent(CREATE_JIT_MASTER, OK, segment),
|
|
FakeEvent(MAP_EXECUTABLE, OK, segment),
|
|
FakeEvent(CREATE_JIT_ALIAS, OK, segment),
|
|
FakeEvent(MAP_HOST_ALIAS, OK, segment),
|
|
FakeEvent(MAP_REMOTE_ALIAS, OK, segment),
|
|
FakeEvent(COPY_ALIAS, OK, segment),
|
|
FakeEvent(UNMAP_REMOTE_ALIAS, OK, segment),
|
|
FakeEvent(UNMAP_HOST_ALIAS, OK, segment),
|
|
FakeEvent(CLOSE_JIT_ALIAS, OK, segment),
|
|
FakeEvent(CLOSE_JIT_MASTER, OK, segment)]
|
|
return events + [FakeEvent(FINALIZE_IMAGE, OK), FakeEvent(RELEASE_MIRROR, OK)]
|
|
|
|
|
|
def cleanup_for(prefix: list[FakeEvent]) -> list[FakeEvent]:
|
|
acquired = {name: set() for name in ("masters", "aliases", "host", "remote")}
|
|
child = region = mirror = False
|
|
for event in prefix:
|
|
if event.result != OK:
|
|
break
|
|
if event.operation == CREATE_CHILD: child = True
|
|
elif event.operation == RESERVE_REGION: region = True
|
|
elif event.operation == CREATE_MIRROR: mirror = True
|
|
elif event.operation == CREATE_JIT_MASTER: acquired["masters"].add(event.segment)
|
|
elif event.operation == CREATE_JIT_ALIAS: acquired["aliases"].add(event.segment)
|
|
elif event.operation == MAP_HOST_ALIAS: acquired["host"].add(event.segment)
|
|
elif event.operation == MAP_REMOTE_ALIAS: acquired["remote"].add(event.segment)
|
|
elif event.operation == UNMAP_REMOTE_ALIAS: acquired["remote"].remove(event.segment)
|
|
elif event.operation == UNMAP_HOST_ALIAS: acquired["host"].remove(event.segment)
|
|
elif event.operation == CLOSE_JIT_ALIAS: acquired["aliases"].remove(event.segment)
|
|
elif event.operation == CLOSE_JIT_MASTER: acquired["masters"].remove(event.segment)
|
|
elif event.operation == RELEASE_MIRROR: mirror = False
|
|
result: list[FakeEvent] = []
|
|
for key, operation in (("remote", UNMAP_REMOTE_ALIAS),
|
|
("host", UNMAP_HOST_ALIAS),
|
|
("aliases", CLOSE_JIT_ALIAS),
|
|
("masters", CLOSE_JIT_MASTER)):
|
|
result += [FakeEvent(operation, OK, item)
|
|
for item in sorted(acquired[key], reverse=True)]
|
|
if mirror: result.append(FakeEvent(RELEASE_MIRROR, OK))
|
|
if region: result.append(FakeEvent(UNMAP_REGION, OK))
|
|
if child: result.append(FakeEvent(KILL_AND_REAP_CHILD, OK))
|
|
return result
|
|
|
|
|
|
class HybridCompositionTests(unittest.TestCase):
|
|
def test_success_releases_every_temporary_resource(self) -> None:
|
|
outcome = run_composition(CompositionPlan(2), FakeFacade(tuple(happy())))
|
|
self.assertTrue(outcome.success and outcome.child_alive and outcome.image_retained)
|
|
self.assertEqual(outcome.resources_open, 0)
|
|
self.assertFalse(outcome.target_action_performed or outcome.firmware_behavior_proven)
|
|
|
|
def test_every_forward_failure_terminates_fail_closed(self) -> None:
|
|
baseline = happy()
|
|
for index, original in enumerate(baseline):
|
|
failed = baseline[:index] + [FakeEvent(original.operation, ERROR,
|
|
original.segment)]
|
|
failed += cleanup_for(failed)
|
|
with self.subTest(index=index, operation=original.operation):
|
|
outcome = run_composition(CompositionPlan(2), FakeFacade(tuple(failed)))
|
|
self.assertFalse(outcome.success or outcome.child_alive)
|
|
self.assertEqual(outcome.fail_closed_termination, index > 0)
|
|
self.assertEqual(outcome.resources_open, 0)
|
|
|
|
def test_cleanup_failure_is_contained_by_child_termination(self) -> None:
|
|
failed = happy()[:9]
|
|
original = failed[-1]
|
|
failed[-1] = FakeEvent(original.operation, ERROR, original.segment)
|
|
cleanup = cleanup_for(failed)
|
|
cleanup[0] = FakeEvent(cleanup[0].operation, ERROR, cleanup[0].segment)
|
|
outcome = run_composition(CompositionPlan(2), FakeFacade(tuple(failed + cleanup)))
|
|
self.assertEqual(outcome.classification,
|
|
"OFFLINE_FAIL_CLOSED_AFTER_CLEANUP_FAILURE")
|
|
self.assertFalse(outcome.child_alive)
|
|
|
|
def test_failed_termination_is_a_hard_error(self) -> None:
|
|
events = [FakeEvent(CREATE_CHILD, OK), FakeEvent(RESERVE_REGION, ERROR),
|
|
FakeEvent(KILL_AND_REAP_CHILD, ERROR)]
|
|
with self.assertRaises(CompositionError):
|
|
run_composition(CompositionPlan(1), FakeFacade(tuple(events)))
|
|
|
|
def test_bounds_wrong_order_deadline_and_unused_events(self) -> None:
|
|
for value in (0, 9, True):
|
|
with self.subTest(value=value), self.assertRaises(CompositionError):
|
|
CompositionPlan(value)
|
|
events = happy(1)
|
|
events[0] = FakeEvent(CREATE_CHILD, OK, ticks=256)
|
|
events = events[:2] + [FakeEvent(KILL_AND_REAP_CHILD, OK)]
|
|
outcome = run_composition(CompositionPlan(1), FakeFacade(tuple(events)))
|
|
self.assertFalse(outcome.success)
|
|
with self.assertRaises(CompositionError):
|
|
run_composition(CompositionPlan(1), FakeFacade(tuple(
|
|
happy(1) + [FakeEvent(KILL_AND_REAP_CHILD, OK)])))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(argv=[__file__])
|