210 lines
6.6 KiB
Python
210 lines
6.6 KiB
Python
"""Mock subprocess runner for devcontainer CLI commands.
|
|
|
|
Provides configurable mock responses for ``devcontainer up``,
|
|
``devcontainer exec``, and ``docker stop`` commands used in
|
|
lifecycle BDD tests.
|
|
|
|
All mocks reside in ``features/mocks/`` per project convention.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
@dataclass
|
|
class MockProcessResult:
|
|
"""Mimics ``subprocess.CompletedProcess`` for test assertions."""
|
|
|
|
returncode: int = 0
|
|
stdout: str = ""
|
|
stderr: str = ""
|
|
args: list[str] = field(default_factory=list)
|
|
|
|
|
|
class MockDevcontainerRunner:
|
|
"""Configurable mock for subprocess.run calls.
|
|
|
|
Records all invocations and returns pre-configured results
|
|
based on the command being run.
|
|
|
|
Usage::
|
|
|
|
runner = MockDevcontainerRunner()
|
|
runner.set_up_result(container_id="abcdef012345", workspace="/ws")
|
|
# pass runner as ``run_command=runner`` in handler calls
|
|
|
|
Attributes:
|
|
calls: List of all recorded (args, kwargs) invocations.
|
|
up_result: Result returned for ``devcontainer up`` commands.
|
|
exec_result: Result returned for ``devcontainer exec`` commands.
|
|
stop_result: Result returned for ``docker stop`` commands.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self.calls: list[tuple[list[str], dict[str, object]]] = []
|
|
self.up_result: MockProcessResult = MockProcessResult()
|
|
self.exec_result: MockProcessResult = MockProcessResult()
|
|
self.stop_result: MockProcessResult = MockProcessResult()
|
|
|
|
#: TEST-4 fix: expected subprocess kwargs that production code must pass.
|
|
#: If production code sets ``check=True`` (causing CalledProcessError on
|
|
#: non-zero rc) or omits ``capture_output``/``text``, this mock will
|
|
#: flag the mismatch.
|
|
_EXPECTED_KWARGS = frozenset({"capture_output", "text", "timeout", "check"})
|
|
|
|
def __call__(
|
|
self,
|
|
args: list[str],
|
|
**kwargs: object,
|
|
) -> MockProcessResult:
|
|
"""Handle a subprocess.run invocation.
|
|
|
|
Args:
|
|
args: Command-line arguments.
|
|
**kwargs: Additional subprocess kwargs — validated against
|
|
expected production kwargs (TEST-4 fix).
|
|
|
|
Returns:
|
|
Mock result based on the command.
|
|
|
|
Raises:
|
|
AssertionError: If required kwargs are missing or ``check``
|
|
is set to ``True`` (which would raise
|
|
``CalledProcessError`` in production).
|
|
"""
|
|
self.calls.append((list(args), dict(kwargs)))
|
|
|
|
# TEST-4 fix: validate subprocess kwargs.
|
|
missing = self._EXPECTED_KWARGS - set(kwargs)
|
|
if missing:
|
|
raise AssertionError(
|
|
f"MockDevcontainerRunner: missing expected subprocess kwargs "
|
|
f"{missing} in call {args}"
|
|
)
|
|
if kwargs.get("check") is True:
|
|
raise AssertionError(
|
|
f"MockDevcontainerRunner: check=True would raise "
|
|
f"CalledProcessError in production for {args}"
|
|
)
|
|
|
|
if len(args) >= 2 and args[0] == "devcontainer" and args[1] == "up":
|
|
return self.up_result
|
|
if len(args) >= 2 and args[0] == "devcontainer" and args[1] == "exec":
|
|
return self.exec_result
|
|
if len(args) >= 2 and args[0] == "docker" and args[1] == "stop":
|
|
return self.stop_result
|
|
|
|
return MockProcessResult(returncode=0)
|
|
|
|
def set_up_result(
|
|
self,
|
|
*,
|
|
container_id: str = "aabbccddee0011223344",
|
|
workspace: str = "/workspaces/project",
|
|
returncode: int = 0,
|
|
stderr: str = "",
|
|
) -> None:
|
|
"""Configure the result for ``devcontainer up``.
|
|
|
|
Args:
|
|
container_id: Container ID in the JSON output.
|
|
workspace: Workspace path in the JSON output.
|
|
returncode: Exit code.
|
|
stderr: Stderr text.
|
|
"""
|
|
output = json.dumps(
|
|
{
|
|
"outcome": "success" if returncode == 0 else "error",
|
|
"containerId": container_id,
|
|
"remoteWorkspaceFolder": workspace,
|
|
}
|
|
)
|
|
self.up_result = MockProcessResult(
|
|
returncode=returncode,
|
|
stdout=output,
|
|
stderr=stderr,
|
|
)
|
|
|
|
def set_up_no_container_id(self) -> None:
|
|
"""Configure ``devcontainer up`` to succeed without a containerId."""
|
|
output = json.dumps({"outcome": "success"})
|
|
self.up_result = MockProcessResult(
|
|
returncode=0,
|
|
stdout=output,
|
|
stderr="",
|
|
)
|
|
|
|
def set_up_failure(self, stderr: str = "container build failed") -> None:
|
|
"""Configure ``devcontainer up`` to fail.
|
|
|
|
Args:
|
|
stderr: Error message.
|
|
"""
|
|
self.up_result = MockProcessResult(
|
|
returncode=1,
|
|
stdout="",
|
|
stderr=stderr,
|
|
)
|
|
|
|
def set_exec_result(self, *, returncode: int = 0, stdout: str = "ping") -> None:
|
|
"""Configure the result for ``devcontainer exec``.
|
|
|
|
Args:
|
|
returncode: Exit code.
|
|
stdout: Stdout text.
|
|
"""
|
|
self.exec_result = MockProcessResult(
|
|
returncode=returncode,
|
|
stdout=stdout,
|
|
)
|
|
|
|
def set_exec_failure(self) -> None:
|
|
"""Configure ``devcontainer exec`` to fail."""
|
|
self.exec_result = MockProcessResult(
|
|
returncode=1,
|
|
stdout="",
|
|
stderr="container not responding",
|
|
)
|
|
|
|
def set_stop_result(self, *, returncode: int = 0) -> None:
|
|
"""Configure the result for ``docker stop``.
|
|
|
|
Args:
|
|
returncode: Exit code.
|
|
"""
|
|
self.stop_result = MockProcessResult(returncode=returncode)
|
|
|
|
@property
|
|
def call_count(self) -> int:
|
|
"""Total number of invocations."""
|
|
return len(self.calls)
|
|
|
|
@property
|
|
def up_calls(self) -> list[tuple[list[str], dict[str, object]]]:
|
|
"""Calls to ``devcontainer up``."""
|
|
return [
|
|
(a, k)
|
|
for a, k in self.calls
|
|
if len(a) >= 2 and a[0] == "devcontainer" and a[1] == "up"
|
|
]
|
|
|
|
@property
|
|
def exec_calls(self) -> list[tuple[list[str], dict[str, object]]]:
|
|
"""Calls to ``devcontainer exec``."""
|
|
return [
|
|
(a, k)
|
|
for a, k in self.calls
|
|
if len(a) >= 2 and a[0] == "devcontainer" and a[1] == "exec"
|
|
]
|
|
|
|
@property
|
|
def stop_calls(self) -> list[tuple[list[str], dict[str, object]]]:
|
|
"""Calls to ``docker stop``."""
|
|
return [
|
|
(a, k)
|
|
for a, k in self.calls
|
|
if len(a) >= 2 and a[0] == "docker" and a[1] == "stop"
|
|
]
|