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

1"""Pytest fixtures for testing tasks — auto-loaded when footman is installed. 

2 

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. 

7 

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""" 

14 

15from __future__ import annotations 

16 

17import textwrap 

18from typing import TYPE_CHECKING 

19 

20import pytest 

21 

22if TYPE_CHECKING: 

23 from collections.abc import Callable, Iterator 

24 from pathlib import Path 

25 

26 from footman.context import Result 

27 from footman.testing import Runner 

28 

29__all__ = ["fm", "fm_project", "fm_record"] 

30 

31 

32@pytest.fixture 

33def fm() -> Runner: 

34 """A `Runner` for the project the test process runs in.""" 

35 from footman.testing import Runner 

36 

37 return Runner() 

38 

39 

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. 

45 

46 ```python 

47 def test_release(fm_project): 

48 fm = fm_project(''' 

49 from footman import task, run 

50 

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 ``` 

57 

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 

63 

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() 

73 

74 return make 

75 

76 

77@pytest.fixture 

78def fm_record() -> Iterator[list[Result]]: 

79 """Recorded steps for the whole test: task code runs, commands don't. 

80 

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 

89 

90 with recording() as steps: 

91 yield steps