Pytest plugin for golden master (characterisation) testing with automatic expected file regeneration.
Project description
pytest-remaster
Pytest plugin for golden master (characterization) testing with automatic expected file regeneration.
Installation
pip install pytest-remaster
Configuration
[tool.pytest.ini_options]
remaster-by-default = false # default: true
Example 1: directory per test case
discover_test_cases(base_dir) finds leaf directories and returns CaseData with
.input pointing to each directory. Each test case has input files and numbered
expected outputs:
tests/cases/
greet/hello/
command # input
expected_0.txt # first expected output
help/unknown/
command
expected_0.txt
expected_1.txt # multiple outputs supported
import pytest
from pathlib import Path
from pytest_remaster import CaseData, GoldenMaster, discover_test_cases
CASES_DIR = Path(__file__).parent / "cases"
@pytest.mark.parametrize("case", discover_test_cases(CASES_DIR))
def test_command(case: CaseData, golden_master: GoldenMaster) -> None:
cmd = (case.input / "command").read_text().strip()
golden_master.check_all(lambda: my_app(cmd), case.input, suffix=".txt")
Example 2: one file per test case
discover_test_files(base_dir, pattern) finds files matching a glob and returns
CaseData with .input pointing to each file. Expected output is derived from the
filename:
tests/functional/
arguments.py # input (source to lint)
arguments.txt # expected output
anomalous.py
anomalous.txt
import pytest
from pathlib import Path
from pytest_remaster import CaseData, GoldenMaster, discover_test_files
from my_linter import lint
FUNC_DIR = Path(__file__).parent / "functional"
@pytest.mark.parametrize("case", discover_test_files(FUNC_DIR, "*.py"))
def test_lint(case: CaseData, golden_master: GoldenMaster) -> None:
golden_master.check(lambda: lint(case.input), case.expected(suffix=".txt"))
Example 3: capture stdout and stderr
Run a CLI in-process and golden-master each output stream with check_each:
tests/cases/
greet/
command # input: "greet Alice"
expected.stdout # expected stdout
divide-by-zero/
command
expected.stderr # only present when stderr is non-empty
import pytest
from pathlib import Path
from my_app import main
from pytest_remaster import CaseData, GoldenMaster, discover_test_cases
CASES_DIR = Path(__file__).parent / "cases"
@pytest.mark.parametrize("case", discover_test_cases(CASES_DIR))
def test_cli(
case: CaseData, golden_master: GoldenMaster, capsys: pytest.CaptureFixture[str]
) -> None:
def run(case: CaseData) -> pytest.CaptureResult[str]:
cmd = (case.input / "command").read_text().strip()
main(cmd)
return capsys.readouterr()
golden_master.check_each(
case,
runner=run,
extractors={".stdout": lambda r: r.out, ".stderr": lambda r: r.err},
)
All examples auto-update expected files on mismatch. Review the diff in git, rerun. Pass
--no-remaster for strict comparison.
Version-specific expected files with dimensions
When expected output varies by Python version, platform, or implementation, use
dimensions to let pytest-remaster resolve the right file automatically.
How it works
Given a base file and a set of dimensions, check() generates a priority-ordered chain
of override paths and uses the most specific existing file for comparison. Remastering
writes to the most specific path, keeping less specific files untouched. Redundant
overrides (identical to a less specific file) are deleted automatically.
tests/functional/
arguments.py # source to lint
arguments.txt # generic expected output
arguments.312.txt # Python 3.12 override
arguments.312.linux.txt # Python 3.12 on Linux
The resolution chain for dimensions={"version": "312", "platform": "linux"}:
arguments.312.linux.txt(most specific)arguments.312.txt(version only)arguments.linux.txt(platform only)arguments.txt(generic base)
The first existing file is used for comparison. If none match, the base is used.
Example: linter with version-dependent output
import sys
import pytest
from pathlib import Path
from my_linter import lint
from pytest_remaster import CaseData, GoldenMaster, discover_test_files
FUNC_DIR = Path(__file__).parent / "functional"
@pytest.mark.parametrize("case", discover_test_files(FUNC_DIR, "*.py"))
def test_lint(case: CaseData, golden_master: GoldenMaster) -> None:
actual = lint(case.input)
golden_master.check(
actual,
case.expected(suffix=".txt"),
dimensions={
"version": f"{sys.version_info[0]}{sys.version_info[1]}",
"platform": sys.platform,
},
)
On mismatch, --remaster creates the most specific override (e.g.
arguments.312.linux.txt). If the new file is identical to a less specific one (e.g.
arguments.312.txt), it is removed as redundant. This way, only the files that truly
differ between environments are kept.
Input file resolution with resolve_with_override
resolve_with_override(base, override) returns override if it exists on disk,
otherwise base. Useful for resolving input files (e.g. config files) that follow the
same override pattern but are never remastered:
from pytest_remaster import resolve_with_override
rc_file = resolve_with_override("test.rc", override="test.312.rc")
Patching with PatchRegistry
Load fixture files and set up mock patches:
import pytest
from pathlib import Path
from my_app import run_command
from pytest_remaster import PatchRegistry, discover_test_cases
CASES_DIR = Path(__file__).parent / "cases"
patcher = PatchRegistry()
patcher.add_file_patch("command", loader=str.strip)
patcher.add_file_patch(
"salt.json", target="pepper.Pepper", attr="return_value.low.side_effect"
)
patcher.add_file_patch("user.json", default={"name": "default"})
patcher.add_patch("subprocess.run")
@pytest.mark.parametrize("case", discover_test_cases(CASES_DIR))
def test_command(case, golden_master):
with patcher.mock(case) as ctx:
events = run_command(ctx["command"], ctx["user.json"])
golden_master.check_all(events, case.input)
add_file_patch(filename): load a file from the case directory, optionally patch a
target. Options: target, attr="return_value", loader=json.loads, default=None.
add_patch(target): patch a target without loading a file. The mock object is available
in the context dict. Options: name (dict key, defaults to target), **kwargs passed
to unittest.mock.patch.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytest_remaster-0.0.5.tar.gz.
File metadata
- Download URL: pytest_remaster-0.0.5.tar.gz
- Upload date:
- Size: 30.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d1fd1530dea8018b9fa03b5c8c551b0cbb93808051ca6e662768b43c55ed5ed
|
|
| MD5 |
e8ef36a1f28830c145e0760809d994b9
|
|
| BLAKE2b-256 |
84544df5c08785bea64b8073cec4bb71498fb0b33029eaaf334bd69d87772dd1
|
Provenance
The following attestation bundles were made for pytest_remaster-0.0.5.tar.gz:
Publisher:
release.yaml on Pierre-Sassoulas/pytest-remaster
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_remaster-0.0.5.tar.gz -
Subject digest:
7d1fd1530dea8018b9fa03b5c8c551b0cbb93808051ca6e662768b43c55ed5ed - Sigstore transparency entry: 1262337601
- Sigstore integration time:
-
Permalink:
Pierre-Sassoulas/pytest-remaster@f7e2018a95734ebb81f157917be88744993d9d8c -
Branch / Tag:
refs/tags/v0.0.5 - Owner: https://github.com/Pierre-Sassoulas
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@f7e2018a95734ebb81f157917be88744993d9d8c -
Trigger Event:
release
-
Statement type:
File details
Details for the file pytest_remaster-0.0.5-py3-none-any.whl.
File metadata
- Download URL: pytest_remaster-0.0.5-py3-none-any.whl
- Upload date:
- Size: 14.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0bb32ebed3e46cb753c76398acedd1893d6ee1e9d36f77aa0de1d58acade7999
|
|
| MD5 |
54bf430539882dce3056c2af0cac4d16
|
|
| BLAKE2b-256 |
8c3cfadb0f4ab1aa94dcd351f274e4859deeaace956ca4f90b16708174965851
|
Provenance
The following attestation bundles were made for pytest_remaster-0.0.5-py3-none-any.whl:
Publisher:
release.yaml on Pierre-Sassoulas/pytest-remaster
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_remaster-0.0.5-py3-none-any.whl -
Subject digest:
0bb32ebed3e46cb753c76398acedd1893d6ee1e9d36f77aa0de1d58acade7999 - Sigstore transparency entry: 1262337619
- Sigstore integration time:
-
Permalink:
Pierre-Sassoulas/pytest-remaster@f7e2018a95734ebb81f157917be88744993d9d8c -
Branch / Tag:
refs/tags/v0.0.5 - Owner: https://github.com/Pierre-Sassoulas
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@f7e2018a95734ebb81f157917be88744993d9d8c -
Trigger Event:
release
-
Statement type: