# PopenPlugin Guide

`PopenPlugin` intercepts `subprocess.Popen` by replacing the class with a fake that routes process lifecycle through a session script. It is included in core bigfoot -- no extra required.

## Coexistence with SubprocessPlugin

`SubprocessPlugin` patches `subprocess.run` and `shutil.which`. `PopenPlugin` patches `subprocess.Popen`. The two plugins target independent names in the subprocess module and do not interfere with each other. Both can be active in the same sandbox simultaneously.

## Setup

In pytest, access `PopenPlugin` through the `bigfoot.popen_mock` proxy. It auto-creates the plugin for the current test on first use:

```python
import bigfoot

def test_run_command():
    (bigfoot.popen_mock
        .new_session()
        .expect("spawn",       returns=None)
        .expect("communicate", returns=(b"hello\n", b"", 0)))

    with bigfoot:
        import subprocess
        proc = subprocess.Popen(["echo", "hello"], stdout=subprocess.PIPE)
        stdout, stderr = proc.communicate()

    assert stdout == b"hello\n"
    assert proc.returncode == 0

    bigfoot.popen_mock.assert_spawn(command=["echo", "hello"], stdin=None)
    bigfoot.popen_mock.assert_communicate(input=None)
```

For manual use outside pytest, construct `PopenPlugin` explicitly:

```python
from bigfoot import StrictVerifier
from bigfoot.plugins.popen_plugin import PopenPlugin

verifier = StrictVerifier()
popen = PopenPlugin(verifier)
```

Each verifier may have at most one `PopenPlugin`. A second `PopenPlugin(verifier)` raises `ValueError`.

## State machine

```
created --spawn (Popen() call)--> running --communicate--> terminated
                                  running --wait--> terminated
```

The `spawn` step fires automatically during `subprocess.Popen(...)` construction. After that, either `communicate()` or `wait()` terminates the process.

## Scripting a session

Use `new_session()` to create a `SessionHandle` and chain `.expect()` calls to build the script:

```python
(bigfoot.popen_mock
    .new_session()
    .expect("spawn",       returns=None)
    .expect("communicate", returns=(b"output", b"errors", 0)))
```

### `expect()` parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `method` | `str` | required | Step name: `"spawn"`, `"communicate"`, or `"wait"` |
| `returns` | `Any` | required | Value returned by the step (see below) |
| `raises` | `BaseException \| None` | `None` | Exception to raise instead of returning |
| `required` | `bool` | `True` | Whether an unused step causes `UnusedMocksError` at teardown |

### Return values by step

| Step | `returns` type | Description |
|---|---|---|
| `spawn` | `None` | No return value; the `Popen` object is constructed |
| `communicate` | `tuple[bytes, bytes, int]` | `(stdout, stderr, returncode)` |
| `wait` | `int` | The process return code |

## Asserting interactions

Each step records an interaction on the timeline. Use the typed assertion helpers on `bigfoot.popen_mock`:

### `assert_spawn(*, command, stdin)`

Asserts the next spawn interaction. Both `command` and `stdin` are required fields.

```python
bigfoot.popen_mock.assert_spawn(command=["git", "status"], stdin=None)
```

### `assert_communicate(*, input)`

Asserts the next communicate interaction. The `input` field is required.

```python
bigfoot.popen_mock.assert_communicate(input=None)
```

### `assert_wait()`

Asserts the next wait interaction. No fields are required.

```python
bigfoot.popen_mock.assert_wait()
```

## Full example: communicate()

```python
import subprocess
import bigfoot

def run_linter(path):
    proc = subprocess.Popen(
        ["ruff", "check", path],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    stdout, stderr = proc.communicate()
    return proc.returncode, stdout.decode()

def test_linter_clean():
    (bigfoot.popen_mock
        .new_session()
        .expect("spawn",       returns=None)
        .expect("communicate", returns=(b"All checks passed.\n", b"", 0)))

    with bigfoot:
        rc, output = run_linter("src/")

    assert rc == 0
    assert output == "All checks passed.\n"

    bigfoot.popen_mock.assert_spawn(command=["ruff", "check", "src/"], stdin=None)
    bigfoot.popen_mock.assert_communicate(input=None)
```

## Full example: wait()

```python
import subprocess
import bigfoot

def test_wait_for_process():
    (bigfoot.popen_mock
        .new_session()
        .expect("spawn", returns=None)
        .expect("wait",  returns=0))

    with bigfoot:
        proc = subprocess.Popen(["sleep", "1"])
        rc = proc.wait()

    assert rc == 0
    assert proc.returncode == 0

    bigfoot.popen_mock.assert_spawn(command=["sleep", "1"], stdin=None)
    bigfoot.popen_mock.assert_wait()
```

## Non-zero exit code

```python
def test_failing_command():
    (bigfoot.popen_mock
        .new_session()
        .expect("spawn",       returns=None)
        .expect("communicate", returns=(b"", b"command not found\n", 127)))

    with bigfoot:
        proc = subprocess.Popen(["bogus-cmd"])
        stdout, stderr = proc.communicate()

    assert proc.returncode == 127
    assert stderr == b"command not found\n"

    bigfoot.popen_mock.assert_spawn(command=["bogus-cmd"], stdin=None)
    bigfoot.popen_mock.assert_communicate(input=None)
```

## Passing input to communicate()

```python
def test_communicate_with_input():
    (bigfoot.popen_mock
        .new_session()
        .expect("spawn",       returns=None)
        .expect("communicate", returns=(b"response\n", b"", 0)))

    with bigfoot:
        proc = subprocess.Popen(["cat"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
        stdout, stderr = proc.communicate(input=b"hello\n")

    assert stdout == b"response\n"

    bigfoot.popen_mock.assert_spawn(command=["cat"], stdin=None)
    bigfoot.popen_mock.assert_communicate(input=b"hello\n")
```

## ConflictError

At sandbox entry, `PopenPlugin` checks whether `subprocess.Popen` has already been patched by another library. If it has been modified by a third party (unittest.mock, pytest-mock, or an unknown library), bigfoot raises `ConflictError`:

```
ConflictError: target='subprocess.Popen', patcher='unittest.mock'
```

Nested bigfoot sandboxes use reference counting and do not conflict with each other.
