Files
temp/features/steps/cow_sandbox_coverage_r3_steps.py
freemo 31472b5413 test(coverage): add Behave scenarios for 39 under-covered modules
Add Behave feature/step pairs that exercise uncovered branches across handlers, LSP, CLI, and service layers to reach the coverage gate.

ISSUES CLOSED: #1232
2026-03-31 21:47:12 +00:00

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}"
)