Skip to content

SubprocessPlugin

SubprocessPlugin

SubprocessPlugin(verifier)

Bases: BasePlugin

Subprocess interception plugin.

Patches subprocess.run and shutil.which globally. Uses reference counting so nested sandboxes work correctly, following the HttpPlugin pattern exactly.

Source code in src/bigfoot/plugins/subprocess.py
def __init__(self, verifier: "StrictVerifier") -> None:
    super().__init__(verifier)
    # FIFO queue for run mocks (per-plugin instance, per-verifier)
    self._run_queue: deque[RunMockConfig] = deque()
    # Dict keyed by binary name for which mocks
    self._which_mocks: dict[str, WhichMockConfig] = {}
    # Set of which() names that were actually called (for unused-mock tracking)
    self._which_called: set[str] = set()
    self._run_sentinel = SubprocessRunSentinel(self)
    self._which_sentinel = SubprocessWhichSentinel(self)

run property

run

Sentinel used as source argument in assert_interaction() for subprocess.run.

which property

which

Sentinel used as source argument in assert_interaction() for shutil.which.

install

install()

No-op. Called to ensure plugin is registered before sandbox entry.

Access to any attribute of subprocess_mock triggers plugin creation via _SubprocessProxy.getattr. This method exists as a named no-op so tests that want the bouncer active without any mocks have an explicit API to call.

Source code in src/bigfoot/plugins/subprocess.py
def install(self) -> None:
    """No-op. Called to ensure plugin is registered before sandbox entry.

    Access to any attribute of subprocess_mock triggers plugin creation via
    _SubprocessProxy.__getattr__. This method exists as a named no-op so
    tests that want the bouncer active without any mocks have an explicit
    API to call.
    """

mock_run

mock_run(command, *, returncode=0, stdout='', stderr='', raises=None, required=True)

Register a FIFO subprocess.run mock.

Calls are matched in registration order. An unmocked or out-of-order call raises UnmockedInteractionError immediately (bouncer guarantee).

Source code in src/bigfoot/plugins/subprocess.py
def mock_run(
    self,
    command: list[str],
    *,
    returncode: int = 0,
    stdout: str = "",
    stderr: str = "",
    raises: BaseException | None = None,
    required: bool = True,
) -> None:
    """Register a FIFO subprocess.run mock.

    Calls are matched in registration order. An unmocked or out-of-order
    call raises UnmockedInteractionError immediately (bouncer guarantee).
    """
    self._run_queue.append(
        RunMockConfig(
            command=command,
            returncode=returncode,
            stdout=stdout,
            stderr=stderr,
            raises=raises,
            required=required,
        )
    )

mock_which

mock_which(name, returns, *, required=False)

Register a shutil.which mock keyed by binary name.

Always-on: unregistered names are swallowed (return None) and recorded on the timeline, requiring assertion at teardown. Registered names return the configured value. required=False by default because tests often register more alternatives than will be hit in a given path.

Source code in src/bigfoot/plugins/subprocess.py
def mock_which(
    self,
    name: str,
    returns: str | None,
    *,
    required: bool = False,
) -> None:
    """Register a shutil.which mock keyed by binary name.

    Always-on: unregistered names are swallowed (return None) and recorded
    on the timeline, requiring assertion at teardown. Registered names
    return the configured value. required=False by default because
    tests often register more alternatives than will be hit in a given path.
    """
    self._which_mocks[name] = WhichMockConfig(
        name=name,
        returns=returns,
        required=required,
    )

assert_run

assert_run(command, returncode, stdout, stderr)

Assert the next subprocess.run interaction with all 4 fields.

Source code in src/bigfoot/plugins/subprocess.py
def assert_run(
    self,
    command: list[str],
    returncode: int,
    stdout: str,
    stderr: str,
) -> None:
    """Assert the next subprocess.run interaction with all 4 fields."""
    self.verifier.assert_interaction(
        self._run_sentinel,
        command=command,
        returncode=returncode,
        stdout=stdout,
        stderr=stderr,
    )

assert_which

assert_which(name, returns)

Assert the next shutil.which interaction with all 2 fields.

Source code in src/bigfoot/plugins/subprocess.py
def assert_which(
    self,
    name: str,
    returns: str | None,
) -> None:
    """Assert the next shutil.which interaction with all 2 fields."""
    self.verifier.assert_interaction(
        self._which_sentinel,
        name=name,
        returns=returns,
    )

activate

activate()

Reference-counted class-level patch installation.

Source code in src/bigfoot/plugins/subprocess.py
def activate(self) -> None:
    """Reference-counted class-level patch installation."""
    with SubprocessPlugin._install_lock:
        if SubprocessPlugin._install_count == 0:
            self._check_conflicts()
            self._install_patches()
        SubprocessPlugin._install_count += 1