Files
temp/features/steps/overlay_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

675 lines
22 KiB
Python

"""Step definitions for overlay_sandbox_coverage_r3.feature.
Exercises uncovered lines in overlay.py including _is_overlayfs_available,
real overlay mode, _mount_overlay, _unmount_overlay, commit/rollback
error paths, and cleanup edge cases.
All step text uses the ``oscov3`` prefix to avoid AmbiguousStep errors.
"""
from __future__ import annotations
import contextlib
import io
import os
import shutil
import subprocess
import tempfile
from unittest.mock import MagicMock, patch
from behave import given, then, when
from cleveragents.infrastructure.sandbox.overlay import (
OverlaySandbox,
_is_overlayfs_available,
)
from cleveragents.infrastructure.sandbox.protocol import (
SandboxCommitError,
SandboxCreationError,
SandboxRollbackError,
SandboxStateError,
SandboxStatus,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _cleanup_patcher(context, attr_name):
"""Stop a patcher stored on context and remove the attribute."""
patcher = getattr(context, attr_name, None)
if patcher is not None:
with contextlib.suppress(RuntimeError):
patcher.stop()
# ===================================================================
# Group 1: _is_overlayfs_available() steps
# ===================================================================
@given("oscov3 the lru_cache for _is_overlayfs_available is cleared")
def given_oscov3_clear_lru_cache(context):
"""Clear the lru_cache so the function executes fresh."""
_is_overlayfs_available.cache_clear()
context.oscov3_error = None
context.oscov3_result = None
# Track patchers for cleanup
context.oscov3_patchers = []
@given('oscov3 /proc/filesystems is mocked to contain "{content}"')
def given_oscov3_proc_fs_content(context, content):
"""Mock /proc/filesystems to contain specific text (no 'overlay')."""
# os.path.isfile returns True for /proc/filesystems
p1 = patch(
"cleveragents.infrastructure.sandbox.overlay.os.path.isfile",
return_value=True,
)
p1.start()
context.oscov3_patchers.append(p1)
context.add_cleanup(p1.stop)
# builtins open returns the content
p2 = patch(
"builtins.open",
return_value=io.StringIO(content),
)
p2.start()
context.oscov3_patchers.append(p2)
context.add_cleanup(p2.stop)
@given("oscov3 os.path.isfile is mocked to return False for /proc/filesystems")
def given_oscov3_isfile_false(context):
"""Mock os.path.isfile to return False."""
p = patch(
"cleveragents.infrastructure.sandbox.overlay.os.path.isfile",
return_value=False,
)
p.start()
context.oscov3_patchers.append(p)
context.add_cleanup(p.stop)
@given("oscov3 open is mocked to raise OSError for /proc/filesystems")
def given_oscov3_open_raises(context):
"""Mock open and os.path.isfile so that reading /proc/filesystems raises."""
p1 = patch(
"cleveragents.infrastructure.sandbox.overlay.os.path.isfile",
return_value=True,
)
p1.start()
context.oscov3_patchers.append(p1)
context.add_cleanup(p1.stop)
p2 = patch(
"builtins.open",
side_effect=OSError("mocked OSError"),
)
p2.start()
context.oscov3_patchers.append(p2)
context.add_cleanup(p2.stop)
@given("oscov3 the system is mocked as root with overlay in proc filesystems")
def given_oscov3_root_with_overlay(context):
"""Mock system as root with overlay present in /proc/filesystems."""
p1 = patch(
"cleveragents.infrastructure.sandbox.overlay.os.path.isfile",
return_value=True,
)
p1.start()
context.oscov3_patchers.append(p1)
context.add_cleanup(p1.stop)
p2 = patch(
"builtins.open",
return_value=io.StringIO("nodev\toverlay\n"),
)
p2.start()
context.oscov3_patchers.append(p2)
context.add_cleanup(p2.stop)
p3 = patch(
"cleveragents.infrastructure.sandbox.overlay.os.geteuid",
return_value=0,
)
p3.start()
context.oscov3_patchers.append(p3)
context.add_cleanup(p3.stop)
@given("oscov3 subprocess.run is mocked for successful mount probe")
def given_oscov3_mount_probe_success(context):
"""Mock subprocess.run for mount probe: mount returns 0, umount succeeds."""
mount_result = MagicMock()
mount_result.returncode = 0
umount_result = MagicMock()
umount_result.returncode = 0
call_count = {"n": 0}
def mock_run(cmd, **kwargs):
call_count["n"] += 1
if call_count["n"] == 1:
return mount_result
return umount_result
p = patch(
"cleveragents.infrastructure.sandbox.overlay.subprocess.run",
side_effect=mock_run,
)
p.start()
context.oscov3_patchers.append(p)
context.add_cleanup(p.stop)
@given("oscov3 subprocess.run is mocked for failed mount probe")
def given_oscov3_mount_probe_fail(context):
"""Mock subprocess.run for mount probe: mount returns non-zero."""
mount_result = MagicMock()
mount_result.returncode = 1
p = patch(
"cleveragents.infrastructure.sandbox.overlay.subprocess.run",
return_value=mount_result,
)
p.start()
context.oscov3_patchers.append(p)
context.add_cleanup(p.stop)
@given("oscov3 subprocess.run is mocked to raise OSError during probe")
def given_oscov3_probe_oserror(context):
"""Mock subprocess.run to raise OSError during mount probe."""
p = patch(
"cleveragents.infrastructure.sandbox.overlay.subprocess.run",
side_effect=OSError("mocked OSError"),
)
p.start()
context.oscov3_patchers.append(p)
context.add_cleanup(p.stop)
@given("oscov3 subprocess.run is mocked to raise TimeoutExpired during probe")
def given_oscov3_probe_timeout(context):
"""Mock subprocess.run to raise TimeoutExpired during mount probe."""
p = patch(
"cleveragents.infrastructure.sandbox.overlay.subprocess.run",
side_effect=subprocess.TimeoutExpired(cmd="mount", timeout=5),
)
p.start()
context.oscov3_patchers.append(p)
context.add_cleanup(p.stop)
@when("oscov3 _is_overlayfs_available is called")
def when_oscov3_is_overlayfs_called(context):
"""Call _is_overlayfs_available and store the result."""
try:
context.oscov3_result = _is_overlayfs_available()
except Exception as exc:
context.oscov3_error = exc
finally:
# Always clear cache after test to not affect other tests
_is_overlayfs_available.cache_clear()
@then("oscov3 the result should be False")
def then_oscov3_result_false(context):
"""Assert result is False."""
assert context.oscov3_result is False, (
f"Expected False, got {context.oscov3_result}"
)
@then("oscov3 the result should be True")
def then_oscov3_result_true(context):
"""Assert result is True."""
assert context.oscov3_result is True, f"Expected True, got {context.oscov3_result}"
# ===================================================================
# Group 2: Real overlay in create
# ===================================================================
@given("oscov3 a test directory is initialised")
def given_oscov3_test_directory(context):
"""Create a temporary directory with test files."""
context.oscov3_tmpdir = tempfile.mkdtemp(prefix="oscov3-test-")
context.add_cleanup(shutil.rmtree, context.oscov3_tmpdir, True)
context.oscov3_original = os.path.join(context.oscov3_tmpdir, "original")
os.makedirs(context.oscov3_original)
# Create a test file
with open(os.path.join(context.oscov3_original, "existing.txt"), "w") as fh:
fh.write("original content")
context.oscov3_error = None
context.oscov3_sandbox = None
context.oscov3_commit_result = None
context.oscov3_mount_mock = None
context.oscov3_unmount_mock = None
@given("oscov3 a sandbox is created with _use_real_overlay forced to True")
def given_oscov3_sandbox_real_overlay(context):
"""Create sandbox and force _use_real_overlay to True before create()."""
context.oscov3_sandbox = OverlaySandbox(
resource_id="res-oscov3",
original_path=context.oscov3_original,
)
context.oscov3_sandbox._use_real_overlay = True
@given("oscov3 _mount_overlay is mocked to succeed")
def given_oscov3_mount_overlay_mocked(context):
"""Mock _mount_overlay to be a no-op."""
p = patch.object(context.oscov3_sandbox, "_mount_overlay")
context.oscov3_mount_mock = p.start()
context.add_cleanup(p.stop)
@when('oscov3 the sandbox create is called for plan "{plan_id}"')
def when_oscov3_create_called(context, plan_id):
"""Call create() on the sandbox."""
try:
context.oscov3_sandbox.create(plan_id)
except Exception as exc:
context.oscov3_error = exc
@then('oscov3 the sandbox should be in "{state}" state')
def then_oscov3_sandbox_state(context, state):
"""Assert the sandbox is in the expected state."""
assert context.oscov3_sandbox.status.value == state, (
f"Expected {state}, got {context.oscov3_sandbox.status.value}"
)
@then("oscov3 _mount_overlay should have been called")
def then_oscov3_mount_called(context):
"""Assert _mount_overlay was invoked."""
assert context.oscov3_mount_mock is not None
context.oscov3_mount_mock.assert_called_once()
# ===================================================================
# Group 3: Commit edge cases
# ===================================================================
@given('oscov3 a sandbox is created and activated for plan "{plan_id}"')
def given_oscov3_sandbox_active(context, plan_id):
"""Create a sandbox and activate via get_path."""
context.oscov3_sandbox = OverlaySandbox(
resource_id="res-oscov3",
original_path=context.oscov3_original,
)
context.oscov3_sandbox.create(plan_id=plan_id)
context.oscov3_sandbox.get_path("existing.txt")
@given("oscov3 merged_dir is forced to None")
def given_oscov3_merged_dir_none(context):
"""Force the sandbox _merged_dir to None."""
context.oscov3_sandbox._merged_dir = None
@when("oscov3 commit is attempted")
def when_oscov3_commit_attempted(context):
"""Attempt commit, catching errors."""
try:
context.oscov3_commit_result = context.oscov3_sandbox.commit()
except Exception as exc:
context.oscov3_error = exc
@then('oscov3 a SandboxStateError should have been raised with "{msg}"')
def then_oscov3_state_error_with_msg(context, msg):
"""Assert a SandboxStateError was raised with expected message."""
assert context.oscov3_error is not None, (
"Expected SandboxStateError but none raised"
)
assert isinstance(context.oscov3_error, SandboxStateError), (
f"Expected SandboxStateError, got {type(context.oscov3_error).__name__}"
)
assert msg in str(context.oscov3_error), (
f"Expected '{msg}' in '{context.oscov3_error}'"
)
@given('oscov3 a file "{filename}" is added to the sandbox')
def given_oscov3_file_added(context, filename):
"""Create a file in the sandbox merged directory."""
merged = context.oscov3_sandbox._merged_dir
filepath = os.path.join(merged, filename)
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w") as fh:
fh.write("test content for " + filename)
@given("oscov3 backup_directory is mocked to raise RuntimeError")
def given_oscov3_backup_raises(context):
"""Mock backup_directory to raise RuntimeError."""
p = patch(
"cleveragents.infrastructure.sandbox.overlay.backup_directory",
side_effect=RuntimeError("backup failed"),
)
p.start()
context.add_cleanup(p.stop)
@then("oscov3 a SandboxCommitError should have been raised")
def then_oscov3_commit_error(context):
"""Assert a SandboxCommitError was raised."""
assert context.oscov3_error is not None, "Expected SandboxCommitError"
assert isinstance(context.oscov3_error, SandboxCommitError), (
f"Expected SandboxCommitError, got {type(context.oscov3_error).__name__}"
)
@given("oscov3 copy2 is mocked to fail after backup succeeds")
def given_oscov3_copy2_fails_after_backup(context):
"""Mock shutil.copy2 to raise OSError (backup_directory is NOT mocked)."""
p = patch(
"cleveragents.infrastructure.sandbox.overlay.shutil.copy2",
side_effect=OSError("disk full during copy"),
)
p.start()
context.add_cleanup(p.stop)
@given("oscov3 safe_restore is mocked to raise RuntimeError")
def given_oscov3_safe_restore_fails(context):
"""Mock safe_restore to raise RuntimeError (simulates restore failure)."""
p = patch(
"cleveragents.infrastructure.sandbox.overlay.safe_restore",
side_effect=RuntimeError("restore failed"),
)
p.start()
context.add_cleanup(p.stop)
# ===================================================================
# Group 4: Rollback edge cases
# ===================================================================
@when("oscov3 rollback is attempted")
def when_oscov3_rollback_attempted(context):
"""Attempt rollback, catching errors."""
try:
context.oscov3_sandbox.rollback()
except Exception as exc:
context.oscov3_error = exc
@given("oscov3 _use_real_overlay is forced to True")
def given_oscov3_force_real_overlay(context):
"""Force the sandbox to think it has real overlay support."""
context.oscov3_sandbox._use_real_overlay = True
@given("oscov3 the sandbox is committed successfully")
def given_oscov3_committed(context):
"""Commit the sandbox (should succeed)."""
context.oscov3_commit_result = context.oscov3_sandbox.commit()
assert context.oscov3_sandbox.status == SandboxStatus.COMMITTED
@given("oscov3 subprocess.run is mocked for umount and mount during rollback")
def given_oscov3_subprocess_umount_mount(context):
"""Mock subprocess.run to handle umount (check=True) and _mount_overlay."""
# For COMMITTED rollback with real overlay:
# 1. umount is called via subprocess.run with check=True (line 437-441)
# 2. _mount_overlay is called which also uses subprocess.run
umount_result = MagicMock()
umount_result.returncode = 0
mount_result = MagicMock()
mount_result.returncode = 0
p = patch(
"cleveragents.infrastructure.sandbox.overlay.subprocess.run",
return_value=mount_result,
)
p.start()
context.add_cleanup(p.stop)
@given("oscov3 subprocess.run is mocked to raise CalledProcessError for umount")
def given_oscov3_umount_raises_cpe(context):
"""Mock subprocess.run to raise CalledProcessError (umount failure)."""
p = patch(
"cleveragents.infrastructure.sandbox.overlay.subprocess.run",
side_effect=subprocess.CalledProcessError(
returncode=1, cmd=["umount", "/tmp/merged"]
),
)
p.start()
context.add_cleanup(p.stop)
@when("oscov3 rollback is called")
def when_oscov3_rollback_called(context):
"""Call rollback and catch any errors."""
try:
context.oscov3_sandbox.rollback()
except Exception as exc:
context.oscov3_error = exc
@then("oscov3 a SandboxRollbackError should have been raised")
def then_oscov3_rollback_error(context):
"""Assert a SandboxRollbackError was raised."""
assert context.oscov3_error is not None, "Expected SandboxRollbackError"
assert isinstance(context.oscov3_error, SandboxRollbackError), (
f"Expected SandboxRollbackError, got {type(context.oscov3_error).__name__}"
)
@given("oscov3 subprocess.run is mocked for unmount and mount during active rollback")
def given_oscov3_subprocess_active_rollback(context):
"""Mock subprocess.run for ACTIVE rollback with real overlay.
_unmount_overlay calls subprocess.run with check=True.
_mount_overlay also calls subprocess.run with check=True.
"""
result = MagicMock()
result.returncode = 0
p = patch(
"cleveragents.infrastructure.sandbox.overlay.subprocess.run",
return_value=result,
)
p.start()
context.add_cleanup(p.stop)
# ===================================================================
# Group 5: Cleanup
# ===================================================================
@given('oscov3 a sandbox is created for plan "{plan_id}"')
def given_oscov3_sandbox_created(context, plan_id):
"""Create and initialise a sandbox."""
context.oscov3_sandbox = OverlaySandbox(
resource_id="res-oscov3",
original_path=context.oscov3_original,
)
context.oscov3_sandbox.create(plan_id=plan_id)
@given("oscov3 _unmount_overlay is mocked")
def given_oscov3_unmount_mocked(context):
"""Mock _unmount_overlay."""
p = patch.object(context.oscov3_sandbox, "_unmount_overlay")
context.oscov3_unmount_mock = p.start()
context.add_cleanup(p.stop)
@when("oscov3 cleanup is called")
def when_oscov3_cleanup(context):
"""Call cleanup on the sandbox."""
try:
context.oscov3_sandbox.cleanup()
except Exception as exc:
context.oscov3_error = exc
@then("oscov3 _unmount_overlay should have been called")
def then_oscov3_unmount_called(context):
"""Assert _unmount_overlay was called."""
assert context.oscov3_unmount_mock is not None
context.oscov3_unmount_mock.assert_called_once()
@given("oscov3 a pre_commit_backup directory exists")
def given_oscov3_pre_commit_backup_exists(context):
"""Create a pre_commit_backup directory and set it on the sandbox."""
backup_dir = tempfile.mkdtemp(prefix="oscov3-backup-", dir=context.oscov3_tmpdir)
# Put a file in it so rmtree has something to clean
with open(os.path.join(backup_dir, "backup.txt"), "w") as fh:
fh.write("backup data")
context.oscov3_sandbox._pre_commit_backup = backup_dir
context.oscov3_backup_path = backup_dir
@then("oscov3 pre_commit_backup should be None")
def then_oscov3_backup_none(context):
"""Assert _pre_commit_backup is None after cleanup."""
assert context.oscov3_sandbox._pre_commit_backup is None
@given("oscov3 pre_commit_backup is set to a non-existent path")
def given_oscov3_backup_nonexistent(context):
"""Set _pre_commit_backup to a path that does not exist."""
context.oscov3_sandbox._pre_commit_backup = os.path.join(
context.oscov3_tmpdir, "nonexistent-backup-dir"
)
# ===================================================================
# Group 6: _mount_overlay / _unmount_overlay
# ===================================================================
@given("oscov3 _original_path contains a comma")
def given_oscov3_original_path_comma(context):
"""Set _original_path to contain a comma."""
context.oscov3_sandbox._original_path = "/tmp/path,with,commas"
@given("oscov3 _upper_dir contains a comma")
def given_oscov3_upper_dir_comma(context):
"""Set _upper_dir to contain a comma."""
context.oscov3_sandbox._upper_dir = "/tmp/upper,dir"
@when("oscov3 _mount_overlay is called directly")
def when_oscov3_mount_overlay_direct(context):
"""Call _mount_overlay directly and catch errors."""
try:
context.oscov3_sandbox._mount_overlay()
except Exception as exc:
context.oscov3_error = exc
@then('oscov3 a SandboxCreationError should have been raised with "{msg}"')
def then_oscov3_creation_error_msg(context, msg):
"""Assert a SandboxCreationError was raised with expected message."""
assert context.oscov3_error is not None, "Expected SandboxCreationError"
assert isinstance(context.oscov3_error, SandboxCreationError), (
f"Expected SandboxCreationError, got {type(context.oscov3_error).__name__}"
)
assert msg.lower() in str(context.oscov3_error).lower(), (
f"Expected '{msg}' in '{context.oscov3_error}'"
)
@given("oscov3 subprocess.run is mocked for successful mount")
def given_oscov3_subprocess_mount_ok(context):
"""Mock subprocess.run to succeed for mount."""
result = MagicMock()
result.returncode = 0
p = patch(
"cleveragents.infrastructure.sandbox.overlay.subprocess.run",
return_value=result,
)
p.start()
context.add_cleanup(p.stop)
@given("oscov3 subprocess.run is mocked to raise CalledProcessError for mount")
def given_oscov3_subprocess_mount_cpe(context):
"""Mock subprocess.run to raise CalledProcessError."""
p = patch(
"cleveragents.infrastructure.sandbox.overlay.subprocess.run",
side_effect=subprocess.CalledProcessError(
returncode=1, cmd=["mount", "-t", "overlay"]
),
)
p.start()
context.add_cleanup(p.stop)
@given("oscov3 subprocess.run is mocked to raise TimeoutExpired for mount")
def given_oscov3_subprocess_mount_timeout(context):
"""Mock subprocess.run to raise TimeoutExpired."""
p = patch(
"cleveragents.infrastructure.sandbox.overlay.subprocess.run",
side_effect=subprocess.TimeoutExpired(cmd="mount", timeout=30),
)
p.start()
context.add_cleanup(p.stop)
@given("oscov3 subprocess.run is mocked to raise OSError for mount")
def given_oscov3_subprocess_mount_oserror(context):
"""Mock subprocess.run to raise OSError."""
p = patch(
"cleveragents.infrastructure.sandbox.overlay.subprocess.run",
side_effect=OSError("no such file"),
)
p.start()
context.add_cleanup(p.stop)
@then("oscov3 no error should have been raised")
def then_oscov3_no_error(context):
"""Assert no error was raised."""
assert context.oscov3_error is None, (
f"Expected no error, got {context.oscov3_error}"
)
@given("oscov3 subprocess.run is mocked for successful umount")
def given_oscov3_subprocess_umount_ok(context):
"""Mock subprocess.run to succeed for umount."""
result = MagicMock()
result.returncode = 0
p = patch(
"cleveragents.infrastructure.sandbox.overlay.subprocess.run",
return_value=result,
)
p.start()
context.add_cleanup(p.stop)
@when("oscov3 _unmount_overlay is called directly")
def when_oscov3_unmount_direct(context):
"""Call _unmount_overlay directly and catch errors."""
try:
context.oscov3_sandbox._unmount_overlay()
except Exception as exc:
context.oscov3_error = exc