eb4177bd52
Fix ruff format check failures on files introduced by this PR: - features/steps/cli_error_handling_steps.py - features/steps/sandbox_manager_concurrency_steps.py - src/cleveragents/cli/output/_cli_output_manager.py - src/cleveragents/cli/output.py
165 lines
5.5 KiB
Python
165 lines
5.5 KiB
Python
"""Concurrency-related step definitions for sandbox manager commit_all."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
import time
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from behave import given, then, when
|
|
|
|
from cleveragents.infrastructure.sandbox.manager import SandboxManager
|
|
from cleveragents.infrastructure.sandbox.protocol import CommitResult, SandboxStatus
|
|
|
|
|
|
@dataclass
|
|
class _ConcurrencyHarness:
|
|
commit_started: threading.Event
|
|
allow_commit: threading.Event
|
|
commit_finished: threading.Event
|
|
creation_done: threading.Event
|
|
creation_error: Exception | None = None
|
|
creation_completed_before_release: bool = False
|
|
|
|
|
|
class _BlockingSandbox:
|
|
"""Test sandbox that blocks commit until allowed."""
|
|
|
|
def __init__(
|
|
self,
|
|
sandbox_id: str,
|
|
commit_started: threading.Event,
|
|
allow_commit: threading.Event,
|
|
) -> None:
|
|
self._sandbox_id = sandbox_id
|
|
self._status = SandboxStatus.CREATED
|
|
self._commit_started = commit_started
|
|
self._allow_commit = allow_commit
|
|
|
|
@property
|
|
def sandbox_id(self) -> str:
|
|
return self._sandbox_id
|
|
|
|
@property
|
|
def status(self) -> SandboxStatus:
|
|
return self._status
|
|
|
|
def commit(self) -> CommitResult:
|
|
self._commit_started.set()
|
|
if not self._allow_commit.wait(timeout=1):
|
|
raise TimeoutError("commit did not receive release signal in test harness")
|
|
self._status = SandboxStatus.COMMITTED
|
|
return CommitResult(
|
|
sandbox_id=self._sandbox_id,
|
|
success=True,
|
|
timestamp=datetime.now(),
|
|
)
|
|
|
|
def rollback(self) -> None: # pragma: no cover - not triggered in test
|
|
self._status = SandboxStatus.ROLLED_BACK
|
|
|
|
def cleanup(self) -> None: # pragma: no cover - not used in test
|
|
self._status = SandboxStatus.CLEANED_UP
|
|
|
|
|
|
@given('plan "{plan_id}" has a blocking sandbox commit harness')
|
|
def step_given_plan_has_blocking_sandbox(context: Any, plan_id: str) -> None:
|
|
manager: SandboxManager = context.manager
|
|
harness = _ConcurrencyHarness(
|
|
commit_started=threading.Event(),
|
|
allow_commit=threading.Event(),
|
|
commit_finished=threading.Event(),
|
|
creation_done=threading.Event(),
|
|
)
|
|
blocking = _BlockingSandbox(
|
|
sandbox_id="sb-block",
|
|
commit_started=harness.commit_started,
|
|
allow_commit=harness.allow_commit,
|
|
)
|
|
|
|
with manager._lock: # type: ignore[attr-defined]
|
|
manager._active_sandboxes[plan_id] = {"res-block": blocking}
|
|
|
|
context.concurrency_harness = harness
|
|
context.plan_under_test = plan_id
|
|
context.commit_results = None
|
|
context.commit_error = None
|
|
context.created_sandbox = None
|
|
|
|
|
|
@when(
|
|
'commit_all runs concurrently with sandbox creation for plan "{plan_id}" resource "{resource_id}"'
|
|
)
|
|
def step_when_commit_all_runs_concurrently(
|
|
context: Any, plan_id: str, resource_id: str
|
|
) -> None:
|
|
manager: SandboxManager = context.manager
|
|
harness: _ConcurrencyHarness = context.concurrency_harness
|
|
|
|
def _run_commit() -> None:
|
|
try:
|
|
context.commit_results = manager.commit_all(plan_id)
|
|
except Exception as exc: # pragma: no cover - unexpected path
|
|
context.commit_error = exc
|
|
finally:
|
|
harness.commit_finished.set()
|
|
|
|
def _run_creation() -> None:
|
|
try:
|
|
context.created_sandbox = manager.get_or_create_sandbox(
|
|
plan_id=plan_id,
|
|
resource_id=resource_id,
|
|
original_path="/tmp/resource",
|
|
sandbox_strategy="none",
|
|
)
|
|
except Exception as exc: # pragma: no cover - unexpected path
|
|
harness.creation_error = exc
|
|
finally:
|
|
harness.creation_done.set()
|
|
|
|
commit_thread = threading.Thread(target=_run_commit, daemon=True)
|
|
creation_thread = threading.Thread(target=_run_creation, daemon=True)
|
|
|
|
commit_thread.start()
|
|
started = harness.commit_started.wait(timeout=1)
|
|
assert started, "commit_all did not reach sandbox commit phase"
|
|
|
|
creation_thread.start()
|
|
time.sleep(0.05)
|
|
harness.creation_completed_before_release = harness.creation_done.is_set()
|
|
|
|
harness.allow_commit.set()
|
|
|
|
commit_thread.join(timeout=1)
|
|
creation_thread.join(timeout=1)
|
|
|
|
assert harness.commit_finished.is_set(), "commit_all did not finish in test window"
|
|
assert harness.creation_done.is_set(), "sandbox creation thread did not finish"
|
|
assert context.commit_error is None, (
|
|
f"Unexpected commit error: {context.commit_error}"
|
|
)
|
|
|
|
|
|
@then("the concurrent sandbox creation should wait for commit completion")
|
|
def step_then_creation_waited(context: Any) -> None:
|
|
harness: _ConcurrencyHarness = context.concurrency_harness
|
|
assert harness.creation_completed_before_release is False, (
|
|
"Sandbox creation should have been blocked until commit finished"
|
|
)
|
|
assert harness.creation_error is None, (
|
|
f"Sandbox creation failed: {harness.creation_error}"
|
|
)
|
|
assert context.created_sandbox is not None, "Sandbox creation never completed"
|
|
|
|
|
|
@then("commit_all should return {expected:d} successful result")
|
|
def step_then_commit_results_success(context: Any, expected: int) -> None:
|
|
results = context.commit_results
|
|
assert results is not None, "commit_all did not return results"
|
|
assert len(results) == expected, (
|
|
f"Expected {expected} commit results, received {len(results)}"
|
|
)
|
|
assert all(result.success for result in results), "Not all commit results succeeded"
|