forked from HAL9000/cleveragents-core
31472b5413
Add Behave feature/step pairs that exercise uncovered branches across handlers, LSP, CLI, and service layers to reach the coverage gate. ISSUES CLOSED: #1232
247 lines
9.1 KiB
Python
247 lines
9.1 KiB
Python
"""Step definitions for CopyOnWriteSandbox coverage round 3.
|
|
|
|
Targets the remaining uncovered lines in copy_on_write.py:
|
|
- Lines 263-265: backup_directory raises during commit → cleanup backup_dir and re-raise
|
|
- Lines 297-302: safe_restore fails during commit error recovery → log warning, preserve backup
|
|
- Lines 430-432: cleanup() removes _pre_commit_backup when it is not None
|
|
|
|
All steps use the ``cowcov3`` prefix to avoid collisions with existing
|
|
``cow``, ``cowcb``, ``cowcov``, and ``cowcov2`` step prefixes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from unittest.mock import patch
|
|
|
|
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,
|
|
SandboxStatus,
|
|
)
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Helpers
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
def _cowcov3_make_test_dir() -> str:
|
|
"""Create a temporary directory with a seed file for testing."""
|
|
d = tempfile.mkdtemp(prefix="cowcov3-test-dir-")
|
|
with open(os.path.join(d, "existing.txt"), "w") as f:
|
|
f.write("original content")
|
|
return d
|
|
|
|
|
|
def _cowcov3_cleanup_dir(path: str) -> None:
|
|
"""Remove a directory if it exists."""
|
|
if path and os.path.exists(path):
|
|
shutil.rmtree(path, ignore_errors=True)
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Given
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
@given("a cowcov3 test directory is set up")
|
|
def step_cowcov3_test_dir(ctx: Context) -> None:
|
|
"""Create a temporary test directory and initialise context attributes."""
|
|
ctx.cowcov3_test_dir = _cowcov3_make_test_dir()
|
|
ctx.cowcov3_sandbox = None
|
|
ctx.cowcov3_error = None
|
|
ctx.cowcov3_backup_dir_cleaned = None
|
|
ctx.cowcov3_log_records: list[logging.LogRecord] = []
|
|
ctx.add_cleanup(lambda: _cowcov3_cleanup_dir(ctx.cowcov3_test_dir))
|
|
|
|
|
|
@given("a cowcov3 sandbox is created and has changes")
|
|
def step_cowcov3_sandbox_with_changes(ctx: Context) -> None:
|
|
"""Create a sandbox, activate it, and make a change so commit detects diffs."""
|
|
sandbox = CopyOnWriteSandbox("cowcov3-res", ctx.cowcov3_test_dir)
|
|
sandbox.create("cowcov3-plan")
|
|
# Activate by calling get_path, then modify a file so compute_diff sees changes
|
|
path = sandbox.get_path("existing.txt")
|
|
with open(path, "w") as f:
|
|
f.write("modified content")
|
|
ctx.cowcov3_sandbox = sandbox
|
|
ctx.add_cleanup(lambda: sandbox.cleanup())
|
|
|
|
|
|
@given("cowcov3 backup_directory is patched to raise an OSError")
|
|
def step_cowcov3_patch_backup_directory(ctx: Context) -> None:
|
|
"""Patch backup_directory to raise so lines 263-265 execute."""
|
|
patcher = patch(
|
|
"cleveragents.infrastructure.sandbox.copy_on_write.backup_directory",
|
|
side_effect=OSError("disk full"),
|
|
)
|
|
patcher.start()
|
|
ctx.add_cleanup(patcher.stop)
|
|
|
|
|
|
@given("cowcov3 copy phase is patched to fail after backup succeeds")
|
|
def step_cowcov3_patch_copy_phase(ctx: Context) -> None:
|
|
"""Patch shutil.copy2 in the copy_on_write module to fail.
|
|
|
|
This makes the file-copy loop (line 275) raise AFTER backup_directory
|
|
has already succeeded, so _pre_commit_backup is set when we enter
|
|
the except block at line 283.
|
|
"""
|
|
patcher = patch(
|
|
"cleveragents.infrastructure.sandbox.copy_on_write.shutil.copy2",
|
|
side_effect=OSError("copy failed"),
|
|
)
|
|
patcher.start()
|
|
ctx.add_cleanup(patcher.stop)
|
|
|
|
|
|
@given("cowcov3 safe_restore is patched to raise an OSError")
|
|
def step_cowcov3_patch_safe_restore(ctx: Context) -> None:
|
|
"""Patch safe_restore to fail so lines 297-302 execute."""
|
|
patcher = patch(
|
|
"cleveragents.infrastructure.sandbox.copy_on_write.safe_restore",
|
|
side_effect=OSError("restore failed"),
|
|
)
|
|
patcher.start()
|
|
ctx.add_cleanup(patcher.stop)
|
|
|
|
|
|
@given("cowcov3 the sandbox has a pre_commit_backup directory")
|
|
def step_cowcov3_set_pre_commit_backup(ctx: Context) -> None:
|
|
"""Manually set _pre_commit_backup to a real temp directory.
|
|
|
|
This simulates the state after a failed commit where the backup
|
|
was preserved. cleanup() should remove it (lines 430-432).
|
|
"""
|
|
sandbox = ctx.cowcov3_sandbox
|
|
backup_dir = tempfile.mkdtemp(prefix="cowcov3-backup-")
|
|
# Put a marker file so we can verify it existed
|
|
with open(os.path.join(backup_dir, "marker.txt"), "w") as f:
|
|
f.write("backup marker")
|
|
sandbox._pre_commit_backup = backup_dir
|
|
ctx.cowcov3_backup_path = backup_dir
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# When
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
@when("cowcov3 commit is attempted")
|
|
def step_cowcov3_commit(ctx: Context) -> None:
|
|
"""Attempt to commit and capture any exception."""
|
|
# Attach a log handler to capture warnings from the cow module
|
|
cow_logger = logging.getLogger("cleveragents.infrastructure.sandbox.copy_on_write")
|
|
|
|
class _ListHandler(logging.Handler):
|
|
def __init__(self, records_list: list[logging.LogRecord]) -> None:
|
|
super().__init__()
|
|
self._records = records_list
|
|
|
|
def emit(self, record: logging.LogRecord) -> None:
|
|
self._records.append(record)
|
|
|
|
log_records: list[logging.LogRecord] = []
|
|
list_handler = _ListHandler(log_records)
|
|
list_handler.setLevel(logging.DEBUG)
|
|
cow_logger.addHandler(list_handler)
|
|
ctx.add_cleanup(lambda: cow_logger.removeHandler(list_handler))
|
|
|
|
try:
|
|
ctx.cowcov3_sandbox.commit("cowcov3 commit")
|
|
except Exception as exc:
|
|
ctx.cowcov3_error = exc
|
|
|
|
ctx.cowcov3_log_records = log_records
|
|
|
|
|
|
@when("cowcov3 cleanup is called")
|
|
def step_cowcov3_cleanup(ctx: Context) -> None:
|
|
"""Call cleanup on the sandbox."""
|
|
ctx.cowcov3_sandbox.cleanup()
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Then
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
@then("cowcov3 a SandboxCommitError should be raised")
|
|
def step_cowcov3_assert_commit_error(ctx: Context) -> None:
|
|
"""Verify that a SandboxCommitError was raised."""
|
|
assert ctx.cowcov3_error is not None, "Expected an error but none was raised"
|
|
assert isinstance(ctx.cowcov3_error, SandboxCommitError), (
|
|
f"Expected SandboxCommitError, got {type(ctx.cowcov3_error).__name__}: "
|
|
f"{ctx.cowcov3_error}"
|
|
)
|
|
|
|
|
|
@then("cowcov3 the temporary backup dir should have been cleaned up")
|
|
def step_cowcov3_assert_backup_cleaned(ctx: Context) -> None:
|
|
"""Verify that backup_dir was cleaned up after backup_directory failed.
|
|
|
|
When backup_directory raises (lines 263-265), the except block does
|
|
shutil.rmtree(backup_dir) then re-raises. The sandbox should have
|
|
_pre_commit_backup = None because it was never assigned.
|
|
"""
|
|
sandbox = ctx.cowcov3_sandbox
|
|
assert sandbox._pre_commit_backup is None, (
|
|
"Expected _pre_commit_backup to be None after backup_directory failure, "
|
|
f"but got {sandbox._pre_commit_backup}"
|
|
)
|
|
|
|
|
|
@then("cowcov3 the safe_restore failure should have been logged as a warning")
|
|
def step_cowcov3_assert_warning_logged(ctx: Context) -> None:
|
|
"""Verify that the safe_restore failure produced a warning log."""
|
|
warning_records = [
|
|
r for r in ctx.cowcov3_log_records if r.levelno == logging.WARNING
|
|
]
|
|
assert len(warning_records) > 0, (
|
|
"Expected a WARNING log record from safe_restore failure, but none found. "
|
|
f"All records: {[(r.levelno, r.getMessage()) for r in ctx.cowcov3_log_records]}"
|
|
)
|
|
msg = warning_records[0].getMessage()
|
|
assert "Failed to restore original from backup" in msg, (
|
|
f"Expected warning about failed restore, got: {msg}"
|
|
)
|
|
|
|
|
|
@then("cowcov3 the pre_commit_backup should be preserved for manual recovery")
|
|
def step_cowcov3_assert_backup_preserved(ctx: Context) -> None:
|
|
"""Verify _pre_commit_backup is NOT None (preserved for manual recovery).
|
|
|
|
When safe_restore fails (lines 297-302), the code logs a warning
|
|
and does NOT set _pre_commit_backup to None, preserving it for
|
|
manual recovery.
|
|
"""
|
|
sandbox = ctx.cowcov3_sandbox
|
|
assert sandbox._pre_commit_backup is not None, (
|
|
"Expected _pre_commit_backup to be preserved for manual recovery, "
|
|
"but it was set to None"
|
|
)
|
|
|
|
|
|
@then("cowcov3 the pre_commit_backup directory should be removed")
|
|
def step_cowcov3_assert_backup_removed(ctx: Context) -> None:
|
|
"""Verify the pre_commit_backup directory was removed by cleanup (lines 430-432)."""
|
|
backup_path = ctx.cowcov3_backup_path
|
|
assert not os.path.exists(backup_path), (
|
|
f"Expected pre_commit_backup directory to be removed, "
|
|
f"but it still exists at {backup_path}"
|
|
)
|
|
|
|
|
|
@then("cowcov3 the sandbox status should be CLEANED_UP")
|
|
def step_cowcov3_assert_cleaned_up(ctx: Context) -> None:
|
|
"""Verify the sandbox transitioned to CLEANED_UP."""
|
|
assert ctx.cowcov3_sandbox.status == SandboxStatus.CLEANED_UP, (
|
|
f"Expected CLEANED_UP status, got {ctx.cowcov3_sandbox.status.value}"
|
|
)
|