subprocess-vcr


Namesubprocess-vcr JSON
Version 0.1.0 PyPI version JSON
download
home_pageNone
SummaryVCR for subprocess - record and replay subprocess calls for testing
upload_time2025-08-17 21:43:23
maintainerNone
docs_urlNone
authorNone
requires_python>=3.9
licenseNone
keywords testing subprocess vcr mock pytest recording replay
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Subprocess VCR

A Video Cassette Recorder (VCR) for subprocess commands that dramatically speeds
up test execution by recording and replaying subprocess calls.

## Quick Start

```python
import subprocess
import pytest

# Mark test to use VCR - that's it!
@pytest.mark.subprocess_vcr
def test_with_vcr():
    result = subprocess.run(["echo", "hello"], capture_output=True, text=True)
    assert result.stdout == "hello\n"
```

Run tests:

```bash
# Record new interactions, replay existing ones
pytest --subprocess-vcr=record

# Replay only - fails if subprocess call not in cassette (for CI)
pytest --subprocess-vcr=replay
```

## Recording Modes

Subprocess VCR supports several recording modes:

- **`record`** - Replays existing recordings, records new ones. For each
  subprocess call, it first checks if a recording exists. If found, it replays
  that recording. If not found, it executes and records the new subprocess call.
  Useful for incremental test development.

- **`replay`** - Replay only. Fails if a subprocess call is not found in the
  cassette. Ensures deterministic test execution in CI.

- **`reset`** - Always record, replacing any existing cassettes and their
  metadata. Use this to refresh all recordings or when library behavior has
  changed.

- **`replay+reset`** - Attempts to replay from existing cassettes, but on any
  test failure or missing recording, automatically retries the ENTIRE test in
  reset mode. This has the benefit over `reset` of only resetting the cassette
  _where necessary_: when replay succeeds, the existing cassette and metadata
  are preserved.

- **`disable`** - No VCR, subprocess calls execute normally (default).

## Filters for Normalization and Redaction

Subprocess VCR provides a powerful filter system to normalize dynamic values and
redact sensitive information in your recordings. This ensures cassettes are
portable, secure, and deterministic.

### Built-in Filters

#### PathFilter

Normalizes filesystem paths that change between test runs, including paths
relative to the current working directory:

```python
from subprocess_vcr.filters import PathFilter

# Default normalization (pytest paths, home dirs, CWD, etc.)
@pytest.mark.subprocess_vcr(filters=[PathFilter()])
def test_with_paths():
    # Pytest temp paths
    subprocess.run(["ls", "/tmp/pytest-of-user/pytest-123/test_dir"])
    # Recorded as: ["ls", "<TMP>/test_dir"]

    # Current working directory paths
    subprocess.run(["cat", "/home/user/project/data/file.txt"], cwd="/home/user/project")
    # Recorded as: ["cat", "<CWD>/data/file.txt"] with cwd: "<CWD>"

# Custom path replacements
filter = PathFilter(replacements={
    r"/opt/myapp": "<APP_ROOT>",
    r"/var/log/\w+": "<LOG_DIR>",
})
```

#### RedactFilter

Removes sensitive information:

```python
from subprocess_vcr.filters import RedactFilter

# Redact by patterns
filter = RedactFilter(
    patterns=[r"api_key=\w+", r"Bearer \w+"],
    env_vars=["API_KEY", "DATABASE_URL"],
)

@pytest.mark.subprocess_vcr(filters=[filter])
def test_with_secrets():
    subprocess.run(["curl", "-H", "Authorization: Bearer abc123"])
    # Recorded as: ["curl", "-H", "Authorization: <REDACTED>"]
```

### Combining Filters

#### Using Multiple Filters

Filters are applied in order:

```python
@pytest.mark.subprocess_vcr(filters=[
    PathFilter(),  # Handles all path normalization including CWD
    RedactFilter(env_vars=["API_KEY", "DATABASE_URL"]),
])
def test_complex_command():
    subprocess.run(["docker", "build", "-t", "myapp:latest", "."])
```

#### Global Configuration

Set filters for all tests in `conftest.py`:

```python
@pytest.fixture(scope="session")
def subprocess_vcr_config():
    return {
        "filters": [
            PathFilter(),  # Handles all path normalization
            RedactFilter(env_vars=["API_KEY"]),
        ]
    }
```

### Creating Custom Filters

Inherit from `BaseFilter`:

```python
from subprocess_vcr.filters import BaseFilter

class MyCustomFilter(BaseFilter):
    def before_record(self, interaction: dict) -> dict:
        """Modify interaction before saving to cassette."""
        # Example: normalize custom IDs in output
        if interaction.get("stdout"):
            interaction["stdout"] = re.sub(
                r"request-id: \w+",
                "request-id: <REQUEST_ID>",
                interaction["stdout"]
            )
        return interaction

    def before_playback(self, interaction: dict) -> dict:
        """Modify interaction when loading from cassette."""
        # Usually just return unchanged
        return interaction

# Use the custom filter
@pytest.mark.subprocess_vcr(filters=[MyCustomFilter()])
def test_with_custom_filter():
    subprocess.run(["myapp", "process"])
```

## VCR Context in Test Reports

When tests fail while using subprocess VCR, pytest shows additional context in
the test report to help with debugging:

```
----------------------------- subprocess-vcr -----------------------------
This test replayed subprocess calls from VCR cassette: test_example.yaml
To re-record this test, run with: --subprocess-vcr=reset
```

This context appears for ANY test failure when VCR is replaying, helping you
understand whether the failure might be due to outdated recordings.

## Example Cassette

```yaml
version: "1.0"
interactions:
  - args:
      - echo
      - hello world
    kwargs:
      stdout: PIPE
      stderr: PIPE
      text: true
    duration: 0.005
    returncode: 0
    stdout: |
      hello world
    stderr: ""
    pid: 12345
```

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "subprocess-vcr",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.9",
    "maintainer_email": null,
    "keywords": "testing, subprocess, vcr, mock, pytest, recording, replay",
    "author": null,
    "author_email": "Maximilian Roos <m@maxroos.com>",
    "download_url": "https://files.pythonhosted.org/packages/1c/a0/796b850f9fb6bb8c148130887f4dde565fa717f39686c706fdef55ffd515/subprocess_vcr-0.1.0.tar.gz",
    "platform": null,
    "description": "# Subprocess VCR\n\nA Video Cassette Recorder (VCR) for subprocess commands that dramatically speeds\nup test execution by recording and replaying subprocess calls.\n\n## Quick Start\n\n```python\nimport subprocess\nimport pytest\n\n# Mark test to use VCR - that's it!\n@pytest.mark.subprocess_vcr\ndef test_with_vcr():\n    result = subprocess.run([\"echo\", \"hello\"], capture_output=True, text=True)\n    assert result.stdout == \"hello\\n\"\n```\n\nRun tests:\n\n```bash\n# Record new interactions, replay existing ones\npytest --subprocess-vcr=record\n\n# Replay only - fails if subprocess call not in cassette (for CI)\npytest --subprocess-vcr=replay\n```\n\n## Recording Modes\n\nSubprocess VCR supports several recording modes:\n\n- **`record`** - Replays existing recordings, records new ones. For each\n  subprocess call, it first checks if a recording exists. If found, it replays\n  that recording. If not found, it executes and records the new subprocess call.\n  Useful for incremental test development.\n\n- **`replay`** - Replay only. Fails if a subprocess call is not found in the\n  cassette. Ensures deterministic test execution in CI.\n\n- **`reset`** - Always record, replacing any existing cassettes and their\n  metadata. Use this to refresh all recordings or when library behavior has\n  changed.\n\n- **`replay+reset`** - Attempts to replay from existing cassettes, but on any\n  test failure or missing recording, automatically retries the ENTIRE test in\n  reset mode. This has the benefit over `reset` of only resetting the cassette\n  _where necessary_: when replay succeeds, the existing cassette and metadata\n  are preserved.\n\n- **`disable`** - No VCR, subprocess calls execute normally (default).\n\n## Filters for Normalization and Redaction\n\nSubprocess VCR provides a powerful filter system to normalize dynamic values and\nredact sensitive information in your recordings. This ensures cassettes are\nportable, secure, and deterministic.\n\n### Built-in Filters\n\n#### PathFilter\n\nNormalizes filesystem paths that change between test runs, including paths\nrelative to the current working directory:\n\n```python\nfrom subprocess_vcr.filters import PathFilter\n\n# Default normalization (pytest paths, home dirs, CWD, etc.)\n@pytest.mark.subprocess_vcr(filters=[PathFilter()])\ndef test_with_paths():\n    # Pytest temp paths\n    subprocess.run([\"ls\", \"/tmp/pytest-of-user/pytest-123/test_dir\"])\n    # Recorded as: [\"ls\", \"<TMP>/test_dir\"]\n\n    # Current working directory paths\n    subprocess.run([\"cat\", \"/home/user/project/data/file.txt\"], cwd=\"/home/user/project\")\n    # Recorded as: [\"cat\", \"<CWD>/data/file.txt\"] with cwd: \"<CWD>\"\n\n# Custom path replacements\nfilter = PathFilter(replacements={\n    r\"/opt/myapp\": \"<APP_ROOT>\",\n    r\"/var/log/\\w+\": \"<LOG_DIR>\",\n})\n```\n\n#### RedactFilter\n\nRemoves sensitive information:\n\n```python\nfrom subprocess_vcr.filters import RedactFilter\n\n# Redact by patterns\nfilter = RedactFilter(\n    patterns=[r\"api_key=\\w+\", r\"Bearer \\w+\"],\n    env_vars=[\"API_KEY\", \"DATABASE_URL\"],\n)\n\n@pytest.mark.subprocess_vcr(filters=[filter])\ndef test_with_secrets():\n    subprocess.run([\"curl\", \"-H\", \"Authorization: Bearer abc123\"])\n    # Recorded as: [\"curl\", \"-H\", \"Authorization: <REDACTED>\"]\n```\n\n### Combining Filters\n\n#### Using Multiple Filters\n\nFilters are applied in order:\n\n```python\n@pytest.mark.subprocess_vcr(filters=[\n    PathFilter(),  # Handles all path normalization including CWD\n    RedactFilter(env_vars=[\"API_KEY\", \"DATABASE_URL\"]),\n])\ndef test_complex_command():\n    subprocess.run([\"docker\", \"build\", \"-t\", \"myapp:latest\", \".\"])\n```\n\n#### Global Configuration\n\nSet filters for all tests in `conftest.py`:\n\n```python\n@pytest.fixture(scope=\"session\")\ndef subprocess_vcr_config():\n    return {\n        \"filters\": [\n            PathFilter(),  # Handles all path normalization\n            RedactFilter(env_vars=[\"API_KEY\"]),\n        ]\n    }\n```\n\n### Creating Custom Filters\n\nInherit from `BaseFilter`:\n\n```python\nfrom subprocess_vcr.filters import BaseFilter\n\nclass MyCustomFilter(BaseFilter):\n    def before_record(self, interaction: dict) -> dict:\n        \"\"\"Modify interaction before saving to cassette.\"\"\"\n        # Example: normalize custom IDs in output\n        if interaction.get(\"stdout\"):\n            interaction[\"stdout\"] = re.sub(\n                r\"request-id: \\w+\",\n                \"request-id: <REQUEST_ID>\",\n                interaction[\"stdout\"]\n            )\n        return interaction\n\n    def before_playback(self, interaction: dict) -> dict:\n        \"\"\"Modify interaction when loading from cassette.\"\"\"\n        # Usually just return unchanged\n        return interaction\n\n# Use the custom filter\n@pytest.mark.subprocess_vcr(filters=[MyCustomFilter()])\ndef test_with_custom_filter():\n    subprocess.run([\"myapp\", \"process\"])\n```\n\n## VCR Context in Test Reports\n\nWhen tests fail while using subprocess VCR, pytest shows additional context in\nthe test report to help with debugging:\n\n```\n----------------------------- subprocess-vcr -----------------------------\nThis test replayed subprocess calls from VCR cassette: test_example.yaml\nTo re-record this test, run with: --subprocess-vcr=reset\n```\n\nThis context appears for ANY test failure when VCR is replaying, helping you\nunderstand whether the failure might be due to outdated recordings.\n\n## Example Cassette\n\n```yaml\nversion: \"1.0\"\ninteractions:\n  - args:\n      - echo\n      - hello world\n    kwargs:\n      stdout: PIPE\n      stderr: PIPE\n      text: true\n    duration: 0.005\n    returncode: 0\n    stdout: |\n      hello world\n    stderr: \"\"\n    pid: 12345\n```\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "VCR for subprocess - record and replay subprocess calls for testing",
    "version": "0.1.0",
    "project_urls": {
        "Homepage": "https://github.com/max-sixty/subprocess-vcr",
        "Issues": "https://github.com/max-sixty/subprocess-vcr/issues",
        "Repository": "https://github.com/max-sixty/subprocess-vcr"
    },
    "split_keywords": [
        "testing",
        " subprocess",
        " vcr",
        " mock",
        " pytest",
        " recording",
        " replay"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "aacb3bddbdb1cfb89dd346a42fe073d6c53f69fdbaa88fa0125d26000810c373",
                "md5": "94c04a98108500f0ccc9c4bc90146e79",
                "sha256": "c4c2e663a7cd8866b54aa4a267205e8b4f5554d1f08604ec8834b18eda5ae86a"
            },
            "downloads": -1,
            "filename": "subprocess_vcr-0.1.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "94c04a98108500f0ccc9c4bc90146e79",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.9",
            "size": 22161,
            "upload_time": "2025-08-17T21:43:22",
            "upload_time_iso_8601": "2025-08-17T21:43:22.221081Z",
            "url": "https://files.pythonhosted.org/packages/aa/cb/3bddbdb1cfb89dd346a42fe073d6c53f69fdbaa88fa0125d26000810c373/subprocess_vcr-0.1.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "1ca0796b850f9fb6bb8c148130887f4dde565fa717f39686c706fdef55ffd515",
                "md5": "851a368fddec5ff5025f9c9a7e35e714",
                "sha256": "323771e8755ce67ec889c31e12b5a1b7892c7ad97397b4cae0752a6db2ab3317"
            },
            "downloads": -1,
            "filename": "subprocess_vcr-0.1.0.tar.gz",
            "has_sig": false,
            "md5_digest": "851a368fddec5ff5025f9c9a7e35e714",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.9",
            "size": 57754,
            "upload_time": "2025-08-17T21:43:23",
            "upload_time_iso_8601": "2025-08-17T21:43:23.291985Z",
            "url": "https://files.pythonhosted.org/packages/1c/a0/796b850f9fb6bb8c148130887f4dde565fa717f39686c706fdef55ffd515/subprocess_vcr-0.1.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-08-17 21:43:23",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "max-sixty",
    "github_project": "subprocess-vcr",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "subprocess-vcr"
}
        
Elapsed time: 0.62740s