Coverage for src/footman/pytest_plugin.py: 100%
24 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-24 13:38 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-24 13:38 +0000
1"""Pytest fixtures for testing tasks — auto-loaded when footman is installed.
3Registered through the `pytest11` entry point, so `pip install footman`
4alongside pytest is all it takes; there is nothing to enable. Each fixture is
5a thin shim over `footman.testing` — non-pytest users get the same power from
6that module directly.
8Two deliberate laziness rules: this module is only ever imported by pytest
9itself (footman's runtime stays zero-dependency), and it imports nothing from
10footman at module level — pytest loads entry-point plugins before coverage
11tools start measuring, so an eager import here would make every downstream
12project's coverage of footman-adjacent code look worse than it is.
13"""
15from __future__ import annotations
17import textwrap
18from typing import TYPE_CHECKING
20import pytest
22if TYPE_CHECKING:
23 from collections.abc import Callable, Iterator
24 from pathlib import Path
26 from footman.context import Result
27 from footman.testing import Runner
29__all__ = ["fm", "fm_project", "fm_record"]
32@pytest.fixture
33def fm() -> Runner:
34 """A `Runner` for the project the test process runs in."""
35 from footman.testing import Runner
37 return Runner()
40@pytest.fixture
41def fm_project(
42 tmp_path: Path, monkeypatch: pytest.MonkeyPatch
43) -> Callable[..., Runner]:
44 """Factory: scaffold an isolated project from tasks-file source.
46 ```python
47 def test_release(fm_project):
48 fm = fm_project('''
49 from footman import task, run
51 @task
52 def release(version: str):
53 run(f"git tag v{version}")
54 ''')
55 assert fm.invoke("--dry-run release 1.2.0").ok
56 ```
58 Writes a minimal `pyproject.toml` plus the tasks file into `tmp_path`,
59 chdirs there for the test, and returns a `Runner`. Pass `name=` to use a
60 non-default tasks filename (wired up via `[tool.footman] tasks`).
61 """
62 from footman.testing import Runner
64 def make(source: str, *, name: str = "tasks.py") -> Runner:
65 config = "" if name == "tasks.py" else f"\n[tool.footman]\ntasks = '{name}'\n"
66 (tmp_path / "pyproject.toml").write_text(
67 f'[project]\nname = "test"\nversion = "0"\n{config}',
68 encoding="utf-8",
69 )
70 (tmp_path / name).write_text(textwrap.dedent(source), encoding="utf-8")
71 monkeypatch.chdir(tmp_path)
72 return Runner()
74 return make
77@pytest.fixture
78def fm_record() -> Iterator[list[Result]]:
79 """Recorded steps for the whole test: task code runs, commands don't.
81 ```python
82 def test_lint_fix(fm_record):
83 from tasks import lint
84 lint(fix=True)
85 assert fm_record[0].command == "ruff check . --fix"
86 ```
87 """
88 from footman.testing import recording
90 with recording() as steps:
91 yield steps