rein-aharness/tests/test_execution_cancel.py
custodian-sync f01b765668 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-09-04:
  - update .custodian-brief.md for rein-aharness

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a06ba0-10aa-7ea0-b20a-4f3fac39efe9
2026-09-04 11:00:20 +02:00

113 lines
3 KiB
Python

from __future__ import annotations
import threading
import time
from unittest.mock import MagicMock
import pytest
from rein_aharness.execution_cancel import (
ExecutionCancel,
ExecutionCancelled,
active_cancel,
using_cancel,
)
def test_cancel_is_one_shot_and_bounded() -> None:
cancel = ExecutionCancel()
first = cancel.cancel("lease-loss")
second = cancel.cancel("timeout")
assert first == second == "lease-loss"
assert cancel.cancelled is True
assert cancel.reason == "lease-loss"
assert cancel.evidence() == {"cancelled": "true", "reason": "lease-loss"}
def test_unknown_reason_is_normalized() -> None:
cancel = ExecutionCancel()
cancel.cancel("provider exploded with secrets")
assert cancel.reason == "cancelled"
def test_check_raises_only_after_cancel() -> None:
cancel = ExecutionCancel()
cancel.check()
cancel.cancel("timeout")
with pytest.raises(ExecutionCancelled, match="timeout") as excinfo:
cancel.check()
assert excinfo.value.reason == "timeout"
def test_register_process_is_killed_on_cancel() -> None:
proc = MagicMock()
proc.poll.return_value = None
cancel = ExecutionCancel()
cancel.register_process(proc)
cancel.cancel("lease-loss")
proc.kill.assert_called_once()
def test_already_cancelled_register_kills_immediately() -> None:
proc = MagicMock()
proc.poll.return_value = None
cancel = ExecutionCancel()
cancel.cancel("signal")
cancel.register_process(proc)
proc.kill.assert_called_once()
def test_exited_process_is_not_killed() -> None:
proc = MagicMock()
proc.poll.return_value = 0
cancel = ExecutionCancel()
cancel.register_process(proc)
cancel.cancel("timeout")
proc.kill.assert_not_called()
def test_stop_callback_failure_does_not_block_cancel() -> None:
cancel = ExecutionCancel()
cancel.register_stop(lambda: (_ for _ in ()).throw(RuntimeError("stop failed")))
assert cancel.cancel("signal") == "signal"
def test_concurrent_cancel_invokes_stop_once() -> None:
seen = []
cancel = ExecutionCancel()
cancel.register_stop(lambda: seen.append("stop"))
threads = [
threading.Thread(target=cancel.cancel, args=("lease-loss",)) for _ in range(8)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
assert seen == ["stop"]
def test_using_cancel_exposes_active_cancel() -> None:
cancel = ExecutionCancel()
assert active_cancel() is None
with using_cancel(cancel):
assert active_cancel() is cancel
cancel.cancel("timeout")
assert active_cancel() is cancel
assert active_cancel() is None
def test_wait_returns_after_cancel() -> None:
cancel = ExecutionCancel()
started = threading.Event()
def _cancel_soon() -> None:
started.wait(timeout=1)
time.sleep(0.01)
cancel.cancel("signal")
thread = threading.Thread(target=_cancel_soon)
thread.start()
started.set()
assert cancel.wait(timeout=1) is True
thread.join()