Files
temp/features/steps/cow_sandbox_coverage_boost_steps.py
2026-02-16 21:35:54 +00:00

351 lines
13 KiB
Python

"""Step definitions for copy-on-write sandbox coverage boost.
Covers uncovered lines and branches in copy_on_write.py:
- SandboxCreationError re-raise (lines 140-142) when original path is a file
- get_path / commit / rollback with _sandbox_path = None
- OSError handlers in commit (lines 252-254) and rollback (lines 311-313)
- cleanup with _sandbox_path = None (branch 341:347)
- cleanup when parent dir already removed (branch 344:347)
- commit dst_dir creation branch (branch 242:244)
- commit deleted-file-not-in-original branch (branch 249:247)
All steps use the ``cowcb`` prefix to avoid collisions with existing steps.
"""
from __future__ import annotations
import os
import shutil
import tempfile
from behave import given, then, when
from behave.runner import Context
from cleveragents.infrastructure.sandbox.copy_on_write import CopyOnWriteSandbox
from cleveragents.infrastructure.sandbox.protocol import (
SandboxCommitError,
SandboxCreationError,
SandboxRollbackError,
SandboxStateError,
SandboxStatus,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _init_cowcb_test_dir() -> str:
"""Create a temporary directory with some test files."""
test_dir = tempfile.mkdtemp(prefix="cowcb-test-dir-")
with open(os.path.join(test_dir, "existing.txt"), "w") as f:
f.write("original content")
with open(os.path.join(test_dir, "to_delete.txt"), "w") as f:
f.write("will be deleted")
os.makedirs(os.path.join(test_dir, "data"), exist_ok=True)
with open(os.path.join(test_dir, "data", "file.txt"), "w") as f:
f.write("nested file")
return test_dir
def _cleanup_dir(path: str) -> None:
"""Remove a directory tree if it exists, ignoring errors."""
if path and os.path.exists(path):
shutil.rmtree(path, ignore_errors=True)
# ---------------------------------------------------------------------------
# Given
# ---------------------------------------------------------------------------
@given("a cowcb temporary file instead of a directory")
def step_cowcb_temp_file(ctx: Context) -> None:
"""Create a temporary regular file (not a directory) to use as original_path."""
fd, path = tempfile.mkstemp(prefix="cowcb-file-")
os.write(fd, b"not a directory")
os.close(fd)
ctx.cowcb_file_path = path
ctx.cowcb_sandbox = None
ctx.cowcb_error = None
ctx.cowcb_commit_result = None
# Register cleanup
if not hasattr(ctx, "_cleanup_handlers"):
ctx._cleanup_handlers = []
ctx._cleanup_handlers.append(
lambda: os.unlink(path) if os.path.exists(path) else None
)
@given("a cowcb test directory is initialised")
def step_cowcb_init_dir(ctx: Context) -> None:
ctx.cowcb_test_dir = _init_cowcb_test_dir()
ctx.cowcb_sandbox = None
ctx.cowcb_error = None
ctx.cowcb_commit_result = None
if not hasattr(ctx, "_cleanup_handlers"):
ctx._cleanup_handlers = []
ctx._cleanup_handlers.append(lambda: _cleanup_dir(ctx.cowcb_test_dir))
@given("a cowcb sandbox with status CREATED and sandbox_path None")
def step_cowcb_created_none_path(ctx: Context) -> None:
"""Create a sandbox and force internal state: status=CREATED, _sandbox_path=None."""
test_dir = _init_cowcb_test_dir()
ctx.cowcb_test_dir = test_dir
sandbox = CopyOnWriteSandbox(resource_id="res-cowcb", original_path=test_dir)
# Force internal state to simulate the edge case
sandbox._status = SandboxStatus.CREATED
sandbox._sandbox_path = None
ctx.cowcb_sandbox = sandbox
ctx.cowcb_error = None
if not hasattr(ctx, "_cleanup_handlers"):
ctx._cleanup_handlers = []
ctx._cleanup_handlers.append(lambda: _cleanup_dir(test_dir))
@given("a cowcb sandbox with status ACTIVE and sandbox_path None")
def step_cowcb_active_none_path(ctx: Context) -> None:
"""Create a sandbox and force internal state: status=ACTIVE, _sandbox_path=None."""
test_dir = _init_cowcb_test_dir()
ctx.cowcb_test_dir = test_dir
sandbox = CopyOnWriteSandbox(resource_id="res-cowcb", original_path=test_dir)
# Force internal state to simulate the edge case
sandbox._status = SandboxStatus.ACTIVE
sandbox._sandbox_path = None
ctx.cowcb_sandbox = sandbox
ctx.cowcb_error = None
if not hasattr(ctx, "_cleanup_handlers"):
ctx._cleanup_handlers = []
ctx._cleanup_handlers.append(lambda: _cleanup_dir(test_dir))
@given("a cowcb sandbox with status PENDING and sandbox_path None")
def step_cowcb_pending_none_path(ctx: Context) -> None:
"""Create a sandbox in PENDING state with _sandbox_path=None (the default)."""
test_dir = _init_cowcb_test_dir()
ctx.cowcb_test_dir = test_dir
sandbox = CopyOnWriteSandbox(resource_id="res-cowcb", original_path=test_dir)
# PENDING is the default; _sandbox_path is already None
ctx.cowcb_sandbox = sandbox
ctx.cowcb_error = None
if not hasattr(ctx, "_cleanup_handlers"):
ctx._cleanup_handlers = []
ctx._cleanup_handlers.append(lambda: _cleanup_dir(test_dir))
@given('a cowcb sandbox is created for plan "{plan_id}"')
def step_cowcb_create_sandbox(ctx: Context, plan_id: str) -> None:
"""Create a sandbox normally (CREATED state)."""
ctx.cowcb_sandbox = CopyOnWriteSandbox(
resource_id="res-cowcb",
original_path=ctx.cowcb_test_dir,
)
ctx.cowcb_sandbox.create(plan_id)
# Register cleanup for the sandbox temp directory
sandbox_path = ctx.cowcb_sandbox._sandbox_path
if sandbox_path:
parent = os.path.dirname(sandbox_path)
ctx._cleanup_handlers.append(lambda: _cleanup_dir(parent))
@given('a cowcb sandbox is created and activated for plan "{plan_id}"')
def step_cowcb_create_and_activate(ctx: Context, plan_id: str) -> None:
"""Create a sandbox and activate it by calling get_path."""
ctx.cowcb_sandbox = CopyOnWriteSandbox(
resource_id="res-cowcb",
original_path=ctx.cowcb_test_dir,
)
ctx.cowcb_sandbox.create(plan_id)
# Activate by resolving a path
ctx.cowcb_sandbox.get_path("existing.txt")
assert ctx.cowcb_sandbox.status == SandboxStatus.ACTIVE
# Register cleanup for the sandbox temp directory
sandbox_path = ctx.cowcb_sandbox._sandbox_path
if sandbox_path:
parent = os.path.dirname(sandbox_path)
ctx._cleanup_handlers.append(lambda: _cleanup_dir(parent))
# ---------------------------------------------------------------------------
# When
# ---------------------------------------------------------------------------
@when('a cowcb sandbox is created on that file for plan "{plan_id}"')
def step_cowcb_create_on_file(ctx: Context, plan_id: str) -> None:
"""Attempt to create a sandbox with a file as original_path."""
ctx.cowcb_sandbox = CopyOnWriteSandbox(
resource_id="res-cowcb",
original_path=ctx.cowcb_file_path,
)
try:
ctx.cowcb_sandbox.create(plan_id)
except SandboxCreationError as exc:
ctx.cowcb_error = exc
@when('cowcb get_path is called with "{path}"')
def step_cowcb_get_path(ctx: Context, path: str) -> None:
try:
ctx.cowcb_sandbox.get_path(path)
except SandboxStateError as exc:
ctx.cowcb_error = exc
@when("cowcb commit is called")
def step_cowcb_commit(ctx: Context) -> None:
try:
ctx.cowcb_commit_result = ctx.cowcb_sandbox.commit()
except (SandboxStateError, SandboxCommitError) as exc:
ctx.cowcb_error = exc
@when("cowcb rollback is called")
def step_cowcb_rollback(ctx: Context) -> None:
try:
ctx.cowcb_sandbox.rollback()
except (SandboxStateError, SandboxRollbackError) as exc:
ctx.cowcb_error = exc
@when("cowcb cleanup is called")
def step_cowcb_cleanup(ctx: Context) -> None:
ctx.cowcb_sandbox.cleanup()
@when("the cowcb original directory is removed before commit")
def step_cowcb_remove_original_before_commit(ctx: Context) -> None:
"""Modify a file in the sandbox and then patch shutil.copy2 to raise OSError
so the commit sync loop fails when trying to copy the changed file back."""
from unittest.mock import patch as _mock_patch
# First, modify a file in the sandbox so _compute_diff detects a change
sandbox_path = ctx.cowcb_sandbox._sandbox_path
if sandbox_path:
modified = os.path.join(sandbox_path, "existing.txt")
if os.path.exists(modified):
with open(modified, "w") as f:
f.write("modified content for OSError test")
patcher = _mock_patch(
"cleveragents.infrastructure.sandbox.copy_on_write.shutil.copy2",
side_effect=OSError("mocked copy2 failure"),
)
patcher.start()
if not hasattr(ctx, "_cleanup_handlers"):
ctx._cleanup_handlers = []
ctx._cleanup_handlers.append(patcher.stop)
@when("the cowcb original directory is removed before rollback")
def step_cowcb_remove_original_before_rollback(ctx: Context) -> None:
"""Remove the original directory so that shutil.copytree in rollback triggers OSError."""
shutil.rmtree(ctx.cowcb_test_dir)
@when("the cowcb sandbox parent directory is removed")
def step_cowcb_remove_sandbox_parent(ctx: Context) -> None:
"""Remove the sandbox's parent temp directory before cleanup."""
sandbox_path = ctx.cowcb_sandbox._sandbox_path
if sandbox_path:
parent = os.path.dirname(sandbox_path)
if os.path.exists(parent):
shutil.rmtree(parent, ignore_errors=True)
@when('a cowcb file "{rel_path}" is created in the sandbox')
def step_cowcb_create_file_in_sandbox(ctx: Context, rel_path: str) -> None:
"""Create a new file at a relative path inside the sandbox copy."""
sandbox_path = ctx.cowcb_sandbox._sandbox_path
assert sandbox_path is not None
file_path = os.path.join(sandbox_path, rel_path)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w") as f:
f.write("coverage boost content")
@when('the cowcb file "{filename}" is deleted from both sandbox and original')
def step_cowcb_delete_from_both(ctx: Context, filename: str) -> None:
"""Delete a file from both sandbox and original so commit's delete loop
encounters a file that doesn't exist in the original."""
sandbox_path = ctx.cowcb_sandbox._sandbox_path
assert sandbox_path is not None
# Delete from sandbox
sandbox_file = os.path.join(sandbox_path, filename)
if os.path.exists(sandbox_file):
os.remove(sandbox_file)
# Delete from original
original_file = os.path.join(ctx.cowcb_test_dir, filename)
if os.path.exists(original_file):
os.remove(original_file)
# ---------------------------------------------------------------------------
# Then
# ---------------------------------------------------------------------------
@then("a cowcb SandboxCreationError should be raised")
def step_cowcb_check_creation_error(ctx: Context) -> None:
assert ctx.cowcb_error is not None, (
"Expected SandboxCreationError but none occurred"
)
assert isinstance(ctx.cowcb_error, SandboxCreationError), (
f"Expected SandboxCreationError, got {type(ctx.cowcb_error).__name__}: {ctx.cowcb_error}"
)
@then('a cowcb SandboxStateError should be raised with message "{msg}"')
def step_cowcb_check_state_error_msg(ctx: Context, msg: str) -> None:
assert ctx.cowcb_error is not None, "Expected SandboxStateError but none occurred"
assert isinstance(ctx.cowcb_error, SandboxStateError), (
f"Expected SandboxStateError, got {type(ctx.cowcb_error).__name__}: {ctx.cowcb_error}"
)
assert msg in str(ctx.cowcb_error), (
f"Expected '{msg}' in error message, got: {ctx.cowcb_error}"
)
@then("a cowcb SandboxCommitError should be raised")
def step_cowcb_check_commit_error(ctx: Context) -> None:
assert ctx.cowcb_error is not None, "Expected SandboxCommitError but none occurred"
assert isinstance(ctx.cowcb_error, SandboxCommitError), (
f"Expected SandboxCommitError, got {type(ctx.cowcb_error).__name__}: {ctx.cowcb_error}"
)
@then("a cowcb SandboxRollbackError should be raised")
def step_cowcb_check_rollback_error(ctx: Context) -> None:
assert ctx.cowcb_error is not None, (
"Expected SandboxRollbackError but none occurred"
)
assert isinstance(ctx.cowcb_error, SandboxRollbackError), (
f"Expected SandboxRollbackError, got {type(ctx.cowcb_error).__name__}: {ctx.cowcb_error}"
)
@then('the cowcb sandbox status should be "{status}"')
def step_cowcb_check_status(ctx: Context, status: str) -> None:
expected = SandboxStatus(status)
assert ctx.cowcb_sandbox.status == expected, (
f"Expected status {expected}, got {ctx.cowcb_sandbox.status}"
)
@then("the cowcb commit should succeed")
def step_cowcb_commit_success(ctx: Context) -> None:
assert ctx.cowcb_error is None, f"Expected no error but got: {ctx.cowcb_error}"
assert ctx.cowcb_commit_result is not None, "Expected a commit result"
assert ctx.cowcb_commit_result.success is True, "Expected commit to succeed"
@then('the cowcb file "{rel_path}" should exist in the original directory')
def step_cowcb_file_in_original(ctx: Context, rel_path: str) -> None:
file_path = os.path.join(ctx.cowcb_test_dir, rel_path)
assert os.path.exists(file_path), (
f"File {rel_path} not found in original directory at {file_path}"
)