pytest-mockrot 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_mockrot-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +53 -0
- pytest_mockrot-0.1.0/.github/ISSUE_TEMPLATE/config.yml +8 -0
- pytest_mockrot-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +33 -0
- pytest_mockrot-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +23 -0
- pytest_mockrot-0.1.0/.github/workflows/ci.yml +47 -0
- pytest_mockrot-0.1.0/.github/workflows/publish.yml +33 -0
- pytest_mockrot-0.1.0/.gitignore +51 -0
- pytest_mockrot-0.1.0/AGENTS.md +394 -0
- pytest_mockrot-0.1.0/CHANGELOG.md +21 -0
- pytest_mockrot-0.1.0/CLAUDE.md +74 -0
- pytest_mockrot-0.1.0/CODE_OF_CONDUCT.md +122 -0
- pytest_mockrot-0.1.0/CONTRIBUTING.md +116 -0
- pytest_mockrot-0.1.0/LICENSE +21 -0
- pytest_mockrot-0.1.0/PKG-INFO +261 -0
- pytest_mockrot-0.1.0/README.md +226 -0
- pytest_mockrot-0.1.0/SECURITY.md +29 -0
- pytest_mockrot-0.1.0/cahierdecharge.md +463 -0
- pytest_mockrot-0.1.0/pyproject.toml +134 -0
- pytest_mockrot-0.1.0/src/mockrot/__init__.py +7 -0
- pytest_mockrot-0.1.0/src/mockrot/cli.py +23 -0
- pytest_mockrot-0.1.0/src/mockrot/errors.py +19 -0
- pytest_mockrot-0.1.0/src/mockrot/extractor.py +180 -0
- pytest_mockrot-0.1.0/src/mockrot/interceptor.py +418 -0
- pytest_mockrot-0.1.0/src/mockrot/models.py +66 -0
- pytest_mockrot-0.1.0/src/mockrot/plugin.py +186 -0
- pytest_mockrot-0.1.0/src/mockrot/py.typed +1 -0
- pytest_mockrot-0.1.0/src/mockrot/reporter.py +164 -0
- pytest_mockrot-0.1.0/src/mockrot/validator.py +420 -0
- pytest_mockrot-0.1.0/tests/conftest.py +5 -0
- pytest_mockrot-0.1.0/tests/integration/test_async_validation_pytester.py +218 -0
- pytest_mockrot-0.1.0/tests/integration/test_interceptor_pytester.py +60 -0
- pytest_mockrot-0.1.0/tests/integration/test_plugin_registration.py +49 -0
- pytest_mockrot-0.1.0/tests/integration/test_reporter_and_exemption_pytester.py +128 -0
- pytest_mockrot-0.1.0/tests/integration/test_return_type_validation_pytester.py +173 -0
- pytest_mockrot-0.1.0/tests/integration/test_side_effect_pytester.py +214 -0
- pytest_mockrot-0.1.0/tests/integration/test_signature_validation_pytester.py +196 -0
- pytest_mockrot-0.1.0/tests/integration/test_zombie_fields_pytester.py +217 -0
- pytest_mockrot-0.1.0/tests/unit/test_cli.py +25 -0
- pytest_mockrot-0.1.0/tests/unit/test_extractor.py +202 -0
- pytest_mockrot-0.1.0/tests/unit/test_interceptor.py +362 -0
- pytest_mockrot-0.1.0/tests/unit/test_plugin.py +154 -0
- pytest_mockrot-0.1.0/tests/unit/test_reporter.py +195 -0
- pytest_mockrot-0.1.0/tests/unit/test_smoke.py +42 -0
- pytest_mockrot-0.1.0/tests/unit/test_validator.py +1202 -0
- pytest_mockrot-0.1.0/uv.lock +783 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: Bug Report
|
|
2
|
+
description: Create a report to help us reproduce and fix a bug in Mockrot
|
|
3
|
+
title: "fix: "
|
|
4
|
+
labels: ["bug"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: |
|
|
9
|
+
Thanks for taking the time to report an issue! Please check if the issue has already been reported before submitting.
|
|
10
|
+
|
|
11
|
+
- type: textarea
|
|
12
|
+
id: description
|
|
13
|
+
attributes:
|
|
14
|
+
label: Bug Description
|
|
15
|
+
description: A clear and concise description of what the bug is.
|
|
16
|
+
validations:
|
|
17
|
+
required: true
|
|
18
|
+
|
|
19
|
+
- type: textarea
|
|
20
|
+
id: reproduction
|
|
21
|
+
attributes:
|
|
22
|
+
label: Minimal Reproducible Example
|
|
23
|
+
description: |
|
|
24
|
+
Please provide a short snippet of code or minimal test case reproducing the issue.
|
|
25
|
+
placeholder: |
|
|
26
|
+
def test_example():
|
|
27
|
+
with patch("..."):
|
|
28
|
+
...
|
|
29
|
+
validations:
|
|
30
|
+
required: true
|
|
31
|
+
|
|
32
|
+
- type: textarea
|
|
33
|
+
id: environment
|
|
34
|
+
attributes:
|
|
35
|
+
label: Environment Info
|
|
36
|
+
description: |
|
|
37
|
+
OS version, Python version, pytest version, and pydantic version.
|
|
38
|
+
placeholder: |
|
|
39
|
+
- OS: macOS / Ubuntu / Windows
|
|
40
|
+
- Python: 3.12.2
|
|
41
|
+
- pytest: 8.3.0
|
|
42
|
+
- pydantic: 2.8.2
|
|
43
|
+
- pytest-mockrot: 0.1.0
|
|
44
|
+
validations:
|
|
45
|
+
required: true
|
|
46
|
+
|
|
47
|
+
- type: textarea
|
|
48
|
+
id: expected
|
|
49
|
+
attributes:
|
|
50
|
+
label: Expected Behavior
|
|
51
|
+
description: What did you expect to happen?
|
|
52
|
+
validations:
|
|
53
|
+
required: true
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
2
|
+
contact_links:
|
|
3
|
+
- name: Questions & Community Discussions
|
|
4
|
+
url: https://github.com/ademboukabes/Mockrot/discussions
|
|
5
|
+
about: Ask questions and discuss features with the community.
|
|
6
|
+
- name: Security Vulnerability Reports
|
|
7
|
+
url: https://github.com/ademboukabes/Mockrot/blob/main/SECURITY.md
|
|
8
|
+
about: Privately report security concerns via email.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Feature Request
|
|
2
|
+
description: Suggest an idea or enhancement for Mockrot
|
|
3
|
+
title: "feat: "
|
|
4
|
+
labels: ["enhancement"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: |
|
|
9
|
+
Thanks for helping improve Mockrot! Please describe your proposed feature or enhancement.
|
|
10
|
+
|
|
11
|
+
- type: textarea
|
|
12
|
+
id: problem
|
|
13
|
+
attributes:
|
|
14
|
+
label: Problem or Motivation
|
|
15
|
+
description: Is your feature request related to a specific problem or limitation?
|
|
16
|
+
validations:
|
|
17
|
+
required: true
|
|
18
|
+
|
|
19
|
+
- type: textarea
|
|
20
|
+
id: solution
|
|
21
|
+
attributes:
|
|
22
|
+
label: Proposed Solution
|
|
23
|
+
description: A clear and concise description of what you want to happen.
|
|
24
|
+
validations:
|
|
25
|
+
required: true
|
|
26
|
+
|
|
27
|
+
- type: textarea
|
|
28
|
+
id: alternatives
|
|
29
|
+
attributes:
|
|
30
|
+
label: Alternatives Considered
|
|
31
|
+
description: A description of any alternative solutions or workarounds you have considered.
|
|
32
|
+
validations:
|
|
33
|
+
required: false
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## Description
|
|
2
|
+
|
|
3
|
+
Please provide a brief summary of the changes introduced in this pull request and why they are needed.
|
|
4
|
+
|
|
5
|
+
Fixes #(issue number)
|
|
6
|
+
|
|
7
|
+
## Type of Change
|
|
8
|
+
|
|
9
|
+
- [ ] Bug fix (`fix:`)
|
|
10
|
+
- [ ] New feature (`feat:`)
|
|
11
|
+
- [ ] Performance improvement (`perf:`)
|
|
12
|
+
- [ ] Refactor (`refactor:`)
|
|
13
|
+
- [ ] Documentation update (`docs:`)
|
|
14
|
+
- [ ] CI or build tooling (`ci:`, `build:`)
|
|
15
|
+
|
|
16
|
+
## Checklist
|
|
17
|
+
|
|
18
|
+
- [ ] My code follows the coding style and guidelines in `CONTRIBUTING.md`.
|
|
19
|
+
- [ ] I have added positive and negative test cases covering my changes.
|
|
20
|
+
- [ ] All checks pass: `uv run ruff format --check . && uv run ruff check . && uv run pyright && uv run pytest`.
|
|
21
|
+
- [ ] Code coverage remains >= 90%.
|
|
22
|
+
- [ ] No emojis have been introduced in outputs or codebase.
|
|
23
|
+
- [ ] My commits adhere to the [Conventional Commits](https://www.conventionalcommits.org/) format.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
|
|
12
|
+
runs-on: ${{ matrix.os }}
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
os: [ubuntu-latest]
|
|
17
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v3
|
|
24
|
+
with:
|
|
25
|
+
version: "latest"
|
|
26
|
+
enable-cache: true
|
|
27
|
+
|
|
28
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
29
|
+
run: uv python install ${{ matrix.python-version }}
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: uv sync --all-extras --dev
|
|
33
|
+
|
|
34
|
+
- name: Check code formatting (ruff format)
|
|
35
|
+
run: uv run ruff format --check .
|
|
36
|
+
|
|
37
|
+
- name: Lint codebase (ruff check)
|
|
38
|
+
run: uv run ruff check .
|
|
39
|
+
|
|
40
|
+
- name: Strict type check (pyright)
|
|
41
|
+
run: uv run pyright
|
|
42
|
+
|
|
43
|
+
- name: Run test suite with coverage
|
|
44
|
+
run: uv run pytest --cov=mockrot --cov-branch --cov-report=term-missing --cov-fail-under=90
|
|
45
|
+
|
|
46
|
+
- name: Build distribution package
|
|
47
|
+
run: uv build
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
pypi-publish:
|
|
9
|
+
name: Build and publish to PyPI
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment:
|
|
12
|
+
name: pypi
|
|
13
|
+
url: https://pypi.org/p/pytest-mockrot
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v3
|
|
22
|
+
with:
|
|
23
|
+
version: "latest"
|
|
24
|
+
enable-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Set up Python
|
|
27
|
+
run: uv python install 3.12
|
|
28
|
+
|
|
29
|
+
- name: Build distributions
|
|
30
|
+
run: uv build
|
|
31
|
+
|
|
32
|
+
- name: Publish package distributions to PyPI
|
|
33
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Python bytecode and caches
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
|
|
7
|
+
# Environments
|
|
8
|
+
.venv/
|
|
9
|
+
env/
|
|
10
|
+
venv/
|
|
11
|
+
ENV/
|
|
12
|
+
|
|
13
|
+
# Distribution & build
|
|
14
|
+
build/
|
|
15
|
+
develop-eggs/
|
|
16
|
+
dist/
|
|
17
|
+
downloads/
|
|
18
|
+
eggs/
|
|
19
|
+
.eggs/
|
|
20
|
+
lib/
|
|
21
|
+
lib64/
|
|
22
|
+
parts/
|
|
23
|
+
sdist/
|
|
24
|
+
var/
|
|
25
|
+
wheels/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.installed.cfg
|
|
28
|
+
*.egg
|
|
29
|
+
|
|
30
|
+
# Testing & coverage
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
.coverage
|
|
33
|
+
.coverage.*
|
|
34
|
+
htmlcov/
|
|
35
|
+
coverage.xml
|
|
36
|
+
|
|
37
|
+
# Linters & type checkers
|
|
38
|
+
.ruff_cache/
|
|
39
|
+
.mypy_cache/
|
|
40
|
+
.pytype/
|
|
41
|
+
|
|
42
|
+
# IDEs & OS
|
|
43
|
+
.vscode/
|
|
44
|
+
.idea/
|
|
45
|
+
*.swp
|
|
46
|
+
*.swo
|
|
47
|
+
*~
|
|
48
|
+
.DS_Store
|
|
49
|
+
|
|
50
|
+
# Mockrot lock files
|
|
51
|
+
.mockrot.lock
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# AGENTS.md — Mockrot
|
|
2
|
+
|
|
3
|
+
> **Source unique de vérité** pour tout agent IA (Claude Code, Antigravity, Cursor, Codex…) et tout
|
|
4
|
+
> contributeur humain. La spécification produit complète se trouve dans
|
|
5
|
+
> [`cahierdecharge.md`](./cahierdecharge.md). Ce fichier définit **comment** on travaille ; le cahier
|
|
6
|
+
> des charges définit **quoi** construire.
|
|
7
|
+
>
|
|
8
|
+
> **Règle d'or :** en cas d'ambiguïté, de contradiction ou de décision structurante non couverte ici,
|
|
9
|
+
> **s'arrêter et demander** plutôt que supposer.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Contexte du projet
|
|
14
|
+
|
|
15
|
+
**Mockrot** (`pytest-mockrot`) est un plugin `pytest` qui détecte les **mocks obsolètes
|
|
16
|
+
(« Rotting Mocks »)** : des mocks dont les arguments d'appel ou les valeurs de retour ne
|
|
17
|
+
correspondent plus au contrat réel (signature + type de retour) du code de production.
|
|
18
|
+
|
|
19
|
+
- **Le problème :** `unittest.mock` (même avec `autospec=True`) ne valide jamais que `return_value`
|
|
20
|
+
respecte l'annotation `-> ReturnType` de la vraie fonction. Les tests restent verts sur des données
|
|
21
|
+
figées depuis des mois, la prod casse (`AttributeError`, `KeyError`, `ValidationError`).
|
|
22
|
+
- **La solution :** intercepter chaque mock, extraire le contrat de la cible réelle
|
|
23
|
+
(`inspect.signature` + `typing.get_type_hints`), et valider **entrées ET sorties** à chaque appel
|
|
24
|
+
via `pydantic.TypeAdapter`.
|
|
25
|
+
- **Promesse produit :** zéro configuration (`pytest --mockrot`), zéro faux positif bruyant,
|
|
26
|
+
overhead négligeable, ne casse jamais la suite de l'utilisateur.
|
|
27
|
+
- **Cible :** projets Python 3.10+ typés (Pydantic, dataclass, TypedDict), et garde-fou pour le code
|
|
28
|
+
généré/refactoré par des agents IA.
|
|
29
|
+
|
|
30
|
+
### Périmètre
|
|
31
|
+
|
|
32
|
+
| Version | Exigences | Contenu |
|
|
33
|
+
| :--- | :--- | :--- |
|
|
34
|
+
| **V1 (MVP)** | F1 → F8 | Interception, validation signature, validation retour, async, `side_effect`, champs zombies, modes `warn`/`strict`, exemptions |
|
|
35
|
+
| **V1.1** | F9 → F12 | Mode record (`.mockrot.lock`), mocks HTTP/OpenAPI, auto-fixer LibCST, export GitHub/JSON |
|
|
36
|
+
|
|
37
|
+
**Ne jamais implémenter une exigence V1.1 tant que la V1 n'est pas livrée et validée**, sauf demande
|
|
38
|
+
explicite.
|
|
39
|
+
|
|
40
|
+
### Plan de réalisation
|
|
41
|
+
|
|
42
|
+
1. **Semaine 1 :** bootstrap du dépôt, `interceptor`, `extractor`, `validator`, support async.
|
|
43
|
+
2. **Semaine 2 :** champs zombies, sous-mocks `MagicMock`, `reporter` Rich.
|
|
44
|
+
3. **Semaine 3 :** `recorder`, export GitHub, démo, publication PyPI v0.1.0.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 2. Stack technique
|
|
49
|
+
|
|
50
|
+
| Rôle | Outil | Contrainte |
|
|
51
|
+
| :--- | :--- | :--- |
|
|
52
|
+
| Langage | Python | `>= 3.10` (CI : 3.10 → 3.13) |
|
|
53
|
+
| Projet & build | `uv` + `hatchling` | `pyproject.toml` PEP 621, `src-layout`, `uv.lock` versionné |
|
|
54
|
+
| Plugin | `pytest` | `>= 8.0`, entry-point `pytest11` |
|
|
55
|
+
| Validation | `pydantic` v2 | `TypeAdapter` uniquement, jamais d'API v1 |
|
|
56
|
+
| Rendu terminal | `rich` | Uniquement dans `reporter.py` |
|
|
57
|
+
| Auto-fixer | `libcst` | Extra optionnel `pytest-mockrot[fix]` (dépendance lourde) |
|
|
58
|
+
| Lint & format | `ruff` | Zéro warning |
|
|
59
|
+
| Typage | `pyright` mode **strict** | Zéro erreur |
|
|
60
|
+
| Tests | `pytest`, `pytester`, `pytest-cov`, `pytest-mock` | Couverture ≥ 90 % (lignes + branches) |
|
|
61
|
+
|
|
62
|
+
- Nom de distribution : `pytest-mockrot` — package importable : `mockrot`.
|
|
63
|
+
- **Aucune nouvelle dépendance runtime** sans justification écrite et validation explicite. Chaque
|
|
64
|
+
dépendance runtime est un coût imposé à tous les utilisateurs.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 3. Architecture
|
|
69
|
+
|
|
70
|
+
### Pipeline
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
pytest ──▶ plugin.py ──▶ interceptor.py ──▶ extractor.py ──▶ validator.py ──▶ reporter.py
|
|
74
|
+
(hooks) (capture cible) (contrat) (violations) (affichage)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Responsabilités et règles de dépendance
|
|
78
|
+
|
|
79
|
+
| Module | Responsabilité | Peut importer |
|
|
80
|
+
| :--- | :--- | :--- |
|
|
81
|
+
| `plugin.py` | Hooks pytest, options CLI, config `[tool.mockrot]`, **seule frontière fail-safe** | tout |
|
|
82
|
+
| `interceptor.py` | Wrap de `unittest.mock._patch` et `MonkeyPatch.setattr`, proxy d'appel | `extractor`, `validator`, `models` |
|
|
83
|
+
| `extractor.py` | Signature, type hints, `ForwardRef`/`TYPE_CHECKING`, async, cache | `models` |
|
|
84
|
+
| `validator.py` | Validation des arguments et du retour, champs zombies | `extractor`, `models` |
|
|
85
|
+
| `reporter.py` | Rendu Rich, export JSON / SARIF / GitHub | `models` |
|
|
86
|
+
| `recorder.py` | Enregistrement d'empreintes `.mockrot.lock` (V1.1) | `models` |
|
|
87
|
+
| `fixer.py` | Réécriture LibCST (V1.1) | `models` |
|
|
88
|
+
| `models.py` | Types de domaine (`Contract`, `Violation`, `MockSite`…) en `dataclass(frozen=True, slots=True)` | stdlib uniquement |
|
|
89
|
+
|
|
90
|
+
Règles :
|
|
91
|
+
|
|
92
|
+
- **Seul `plugin.py` importe `pytest`.** Le cœur (`extractor`, `validator`) est testable sans pytest.
|
|
93
|
+
- **Seul `reporter.py` importe `rich`.** Seul `fixer.py` importe `libcst`.
|
|
94
|
+
- **Seul `interceptor.py` touche aux API privées** (`unittest.mock._patch`). Toute dépendance à une
|
|
95
|
+
API privée est isolée, commentée, et testée sur toute la matrice Python.
|
|
96
|
+
- Les modules communiquent via les types de `models.py`, jamais via des `str` ou des `dict` ad hoc.
|
|
97
|
+
- Pas de dépendance circulaire. Pas d'état global mutable, sauf le registre de session explicitement
|
|
98
|
+
créé et détruit par `plugin.py`.
|
|
99
|
+
|
|
100
|
+
### Structure cible du dépôt
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
mockrot/
|
|
104
|
+
├── pyproject.toml
|
|
105
|
+
├── uv.lock
|
|
106
|
+
├── AGENTS.md / CLAUDE.md / README.md / CHANGELOG.md / LICENSE
|
|
107
|
+
├── .github/workflows/ci.yml
|
|
108
|
+
├── src/mockrot/
|
|
109
|
+
│ ├── __init__.py plugin.py interceptor.py extractor.py
|
|
110
|
+
│ ├── validator.py reporter.py models.py errors.py
|
|
111
|
+
│ ├── recorder.py fixer.py # V1.1
|
|
112
|
+
│ └── py.typed
|
|
113
|
+
└── tests/
|
|
114
|
+
├── unit/ # cœur pur, sans plugin pytest
|
|
115
|
+
├── integration/ # plugin de bout en bout via la fixture `pytester`
|
|
116
|
+
└── fixtures/ # faux modules « production » (sync, async, pydantic, dataclass, TypedDict…)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Toute déviation par rapport à cette structure doit être justifiée et validée.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 4. Invariants produit (non négociables)
|
|
124
|
+
|
|
125
|
+
1. **Ne jamais faire planter la suite de tests de l'utilisateur.** Toute erreur interne est capturée
|
|
126
|
+
à une **unique frontière** dans `plugin.py`, convertie en avertissement « mode dégradé », et le test
|
|
127
|
+
continue normalement.
|
|
128
|
+
2. **Transparence totale.** Le proxy ne modifie jamais la valeur retournée, l'exception levée, ni le
|
|
129
|
+
comportement du mock. En mode `warn`, un test doit produire exactement le même résultat avec ou
|
|
130
|
+
sans Mockrot.
|
|
131
|
+
3. **Zéro effet de bord quand désactivé.** Sans `--mockrot`, aucun patch n'est installé, aucun import
|
|
132
|
+
coûteux n'est effectué. Tous les patchs sont restaurés en fin de session, de manière idempotente.
|
|
133
|
+
4. **Faux négatif > faux positif.** En cas de doute (type non résolu, signature indisponible, cible
|
|
134
|
+
dynamique), on passe en mode dégradé silencieux. La confiance est le produit.
|
|
135
|
+
5. **Zéro configuration.** Tout fonctionne sans `[tool.mockrot]`. La config ne fait qu'affiner.
|
|
136
|
+
6. **Budget de performance.** Contrats et `TypeAdapter` compilés **une seule fois par cible**.
|
|
137
|
+
Objectif : overhead < 15 ms sur 500 tests (seuil d'alerte : 50 ms). Toute régression est mesurée.
|
|
138
|
+
7. **Chaque violation est actionnable.** Fichier + ligne du mock, cible, contrat réel, et diff précis.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 5. Pièges techniques connus (à lire avant de coder)
|
|
143
|
+
|
|
144
|
+
Le code de la §6 du cahier des charges est **indicatif**, pas à copier tel quel. Points identifiés :
|
|
145
|
+
|
|
146
|
+
- **`_patch.start()` ne suffit pas.** `@patch` en décorateur et `with patch(...)` passent par
|
|
147
|
+
`_patch.__enter__` (que `start()` appelle). Il faut intercepter `__enter__` pour couvrir les trois
|
|
148
|
+
usages et `pytest-mock`.
|
|
149
|
+
- **Mode lax de Pydantic = faux négatifs.** `validate_python("5000", strict=False)` accepte `"5000"`
|
|
150
|
+
pour un `int`. Le rapport attendu signale ce cas comme « Type mismatch » : la validation des valeurs
|
|
151
|
+
mockées doit être **stricte** (`strict=True`), avec les adaptations nécessaires (ex. `dict` → modèle).
|
|
152
|
+
- **Champs zombies.** `TypeAdapter(BaseModel)` ignore les clés en trop par défaut. Ne **jamais**
|
|
153
|
+
muter la config du modèle utilisateur : calculer les clés obsolètes séparément (`model_fields`,
|
|
154
|
+
alias, `TypedDict.__annotations__` / `__required_keys__`).
|
|
155
|
+
- **Cache.** `lru_cache` sur `real_fn` échoue sur les callables non hashables, et un cache par
|
|
156
|
+
`id()` est faux (réutilisation des `id` après GC). Utiliser un cache par identité robuste
|
|
157
|
+
(`WeakKeyDictionary` avec repli) et garder `lru_cache` pour les cas hashables.
|
|
158
|
+
- **`self` / `cls`.** Utiliser `inspect.getattr_static` pour distinguer fonction, `staticmethod`,
|
|
159
|
+
`classmethod`, méthode liée ; ajuster la signature avant `bind()`.
|
|
160
|
+
- **`side_effect` itérable.** Ne jamais consommer l'itérateur par anticipation : valider chaque
|
|
161
|
+
valeur au moment où elle est produite. Exceptions (classes **et** instances) = toujours légitimes.
|
|
162
|
+
- **`AsyncMock`.** Valider la valeur **résolue après `await`**, jamais la coroutine.
|
|
163
|
+
- **Mocks déjà spécifiés.** `autospec=True` / `spec=` : ne pas dupliquer les erreurs de signature
|
|
164
|
+
déjà levées par `unittest.mock`.
|
|
165
|
+
- **Cibles non fonctionnelles.** Patch d'une classe, d'une constante, d'une propriété, d'un module :
|
|
166
|
+
définir le contrat explicitement (ex. classe → `__init__` + instance) ou ignorer proprement.
|
|
167
|
+
- **`monkeypatch.setattr` avec un vrai callable** (lambda, fonction factice) : même validation que
|
|
168
|
+
pour un `Mock`, via un wrapper transparent.
|
|
169
|
+
- **`ForwardRef` / `TYPE_CHECKING`.** Résolution via le `globalns` du module, puis repli AST isolé
|
|
170
|
+
et mis en cache. Échec → mode dégradé, jamais d'exception.
|
|
171
|
+
- **`pytest-xdist`.** Le rapport doit à terme agréger les résultats des workers : ne pas coder un
|
|
172
|
+
registre qui suppose un processus unique sans l'isoler derrière une interface.
|
|
173
|
+
|
|
174
|
+
- **`inspect.stack()` vs `sys._getframe`.** `inspect.stack()` effectue des appels `os.stat`
|
|
175
|
+
sur chaque fichier de la pile pour lire le code source, ce qui brise les suites asynchrones
|
|
176
|
+
vérifiant les I/O bloquantes (ex. `blockbuster.BlockingError` dans Starlette) et introduit un
|
|
177
|
+
surcoût majeur. Utiliser `sys._getframe()` pour une inspection purement mémoire sans I/O.
|
|
178
|
+
- **Plugins pytest internes utilisant `MonkeyPatch.setattr`.** `pytest-asyncio` monkeypatche
|
|
179
|
+
`item.obj` pour synchroniser l'exécution des tests coroutines. Le hook `_patched_monkeypatch_setattr`
|
|
180
|
+
doit ignorer les appels issus des plugins internes de test runner (`pytest_asyncio`, `_pytest`,
|
|
181
|
+
`pluggy`) pour ne pas valider à tort les fonctions de test elles-mêmes comme des mocks.
|
|
182
|
+
|
|
183
|
+
Tout nouveau piège découvert est **ajouté à cette section** dans le même commit que son correctif.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 6. Méthode de travail (niveau senior)
|
|
188
|
+
|
|
189
|
+
### Cycle pour chaque tâche
|
|
190
|
+
|
|
191
|
+
1. **Comprendre** — relire l'exigence (F-ID) dans le cahier des charges, lire le code existant
|
|
192
|
+
concerné, identifier les cas limites. Reformuler l'objectif en une phrase.
|
|
193
|
+
2. **Planifier** — pour toute tâche touchant plus d'un module ou l'API publique : présenter un plan
|
|
194
|
+
court (fichiers impactés, approche, tests prévus, risques) et **attendre validation**.
|
|
195
|
+
3. **Test rouge** — écrire d'abord le test qui échoue (détection attendue **et** absence de faux
|
|
196
|
+
positif). Pour un bug : un test qui reproduit le bug avant tout correctif.
|
|
197
|
+
4. **Implémenter** — le minimum pour passer au vert. Pas de code spéculatif (YAGNI).
|
|
198
|
+
5. **Refactorer** — nommage, duplication, lisibilité, sans changer le comportement.
|
|
199
|
+
6. **Vérifier** — lancer la vérification complète (§10). Rien n'est « fini » sans preuve.
|
|
200
|
+
7. **Commiter** — commits atomiques et propres (§8).
|
|
201
|
+
|
|
202
|
+
### Principes
|
|
203
|
+
|
|
204
|
+
- **Petits incréments.** Une branche = un objectif. Une PR relisible en moins de 15 minutes
|
|
205
|
+
(idéalement < 400 lignes de diff hors tests et fixtures).
|
|
206
|
+
- **Pas de changement hors périmètre.** Ne pas reformater, renommer ou « améliorer » du code non lié
|
|
207
|
+
à la tâche. Les améliorations repérées sont notées et proposées séparément.
|
|
208
|
+
- **Lire avant d'écrire.** Réutiliser les abstractions existantes avant d'en créer de nouvelles.
|
|
209
|
+
- **Décisions tracées.** Toute décision d'architecture non triviale est consignée dans
|
|
210
|
+
`docs/adr/NNNN-titre.md` (contexte, décision, alternatives, conséquences).
|
|
211
|
+
- **Honnêteté.** Ne jamais affirmer qu'un test passe sans l'avoir exécuté. Signaler explicitement
|
|
212
|
+
ce qui n'a pas été vérifié, les compromis faits, et les limites connues.
|
|
213
|
+
- **Dogfooding.** Dès que F1–F3 sont livrés, la suite de tests de Mockrot tourne elle-même avec
|
|
214
|
+
`--mockrot=strict` en CI.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 7. Standards de code
|
|
219
|
+
|
|
220
|
+
### Langue
|
|
221
|
+
|
|
222
|
+
- **Anglais** pour tout ce qui est versionné et public : code, identifiants, docstrings,
|
|
223
|
+
commentaires, messages d'erreur, commits, branches, PR, README, CHANGELOG.
|
|
224
|
+
- **Français** pour les échanges avec l'utilisateur et la documentation interne (`AGENTS.md`,
|
|
225
|
+
`CLAUDE.md`, cahier des charges).
|
|
226
|
+
|
|
227
|
+
### Python
|
|
228
|
+
|
|
229
|
+
- `from __future__ import annotations` en tête de chaque module.
|
|
230
|
+
- **Typage complet**, `pyright --strict` à zéro erreur. `Any` uniquement aux frontières (valeurs
|
|
231
|
+
mockées arbitraires) et jamais propagé. `# type: ignore[code]` / `# pyright: ignore[rule]`
|
|
232
|
+
interdit sans commentaire justificatif sur la même ligne.
|
|
233
|
+
- Types de domaine : `@dataclass(frozen=True, slots=True)`. Énumérations : `enum.Enum`/`StrEnum`
|
|
234
|
+
(avec repli 3.10).
|
|
235
|
+
- Fonctions courtes, pures dès que possible, une seule responsabilité. Pas d'effets de bord à
|
|
236
|
+
l'import.
|
|
237
|
+
- **Exceptions :** hiérarchie dédiée dans `errors.py` (`MockrotError` → …). Jamais de `except:` nu
|
|
238
|
+
ni de `except Exception` hors de la frontière fail-safe de `plugin.py` (et du repli documenté de
|
|
239
|
+
`extractor.py`). Toujours chaîner (`raise ... from exc`).
|
|
240
|
+
- Pas de `print()`. Sortie utilisateur via `reporter.py` ; diagnostic interne via `logging`
|
|
241
|
+
(logger `mockrot`) ou `warnings`.
|
|
242
|
+
- Docstrings style Google sur toute l'API publique et les fonctions non triviales. Les commentaires
|
|
243
|
+
expliquent le **pourquoi**, pas le quoi.
|
|
244
|
+
- Pas de code mort, pas de code commenté, pas de `TODO` sans référence (`TODO(F9): ...`).
|
|
245
|
+
- Ruff (base) : `E, W, F, I, B, UP, SIM, RUF, PT, N, C4, TC, ARG, BLE, ERA, PL`. Format : `ruff format`.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 8. Workflow Git
|
|
250
|
+
|
|
251
|
+
### Branches
|
|
252
|
+
|
|
253
|
+
- **`main`** : protégée, toujours verte, toujours publiable. **Aucun commit direct.** Historique
|
|
254
|
+
**linéaire** (pas de merge commits).
|
|
255
|
+
- Branches courtes (durée de vie ≤ 2–3 jours), créées depuis `main` à jour :
|
|
256
|
+
|
|
257
|
+
```text
|
|
258
|
+
<type>/<scope-ou-fid>-<description-kebab-case>
|
|
259
|
+
|
|
260
|
+
feat/f2-signature-validation
|
|
261
|
+
feat/f4-async-mock-support
|
|
262
|
+
fix/extractor-forward-ref-name-error
|
|
263
|
+
refactor/validator-split-zombie-detection
|
|
264
|
+
test/integration-side-effect-iterables
|
|
265
|
+
ci/python-matrix
|
|
266
|
+
docs/readme-quickstart
|
|
267
|
+
chore/bootstrap-uv-project
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
- Une branche = un sujet. Rebaser régulièrement : `git fetch origin && git rebase origin/main`.
|
|
271
|
+
- Supprimer la branche après merge.
|
|
272
|
+
|
|
273
|
+
### Commits — Conventional Commits 1.0
|
|
274
|
+
|
|
275
|
+
```text
|
|
276
|
+
<type>(<scope>): <résumé impératif, minuscules, sans point, ≤ 72 caractères>
|
|
277
|
+
|
|
278
|
+
<corps optionnel : POURQUOI ce changement, contexte, alternatives écartées.
|
|
279
|
+
Lignes ≤ 72 caractères.>
|
|
280
|
+
|
|
281
|
+
<footer optionnel : Refs: F2 | Closes #12 | BREAKING CHANGE: ...>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
- **Types :** `feat`, `fix`, `refactor`, `perf`, `test`, `docs`, `build`, `ci`, `chore`, `style`,
|
|
285
|
+
`revert`.
|
|
286
|
+
- **Scopes :** `plugin`, `interceptor`, `extractor`, `validator`, `reporter`, `recorder`, `fixer`,
|
|
287
|
+
`models`, `config`, `deps`, `ci`, `release`.
|
|
288
|
+
|
|
289
|
+
Exemples :
|
|
290
|
+
|
|
291
|
+
```text
|
|
292
|
+
feat(interceptor): wrap _patch.__enter__ to capture original target
|
|
293
|
+
fix(validator): validate scalar return values in strict mode
|
|
294
|
+
test(validator): cover zombie keys on nested TypedDict
|
|
295
|
+
refactor(extractor): extract self/cls stripping into dedicated helper
|
|
296
|
+
perf(extractor): cache TypeAdapter per target with weak references
|
|
297
|
+
build(deps): add pydantic>=2.7 as runtime dependency
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Règles :
|
|
301
|
+
|
|
302
|
+
- **Atomique :** un commit = un changement logique cohérent. Chaque commit passe la vérification
|
|
303
|
+
complète (historique *bisectable*).
|
|
304
|
+
- Ne jamais mélanger refactor, formatage et changement de comportement dans le même commit.
|
|
305
|
+
- Le code et ses tests vont **dans le même commit**.
|
|
306
|
+
- Interdits : `wip`, `fix`, `update`, `misc`, `changes`, messages vagues ou en français.
|
|
307
|
+
- Avant d'ouvrir une PR : nettoyer l'historique de **sa propre branche** (`git rebase -i`,
|
|
308
|
+
`git commit --fixup` + `--autosquash`).
|
|
309
|
+
|
|
310
|
+
### Pull Requests
|
|
311
|
+
|
|
312
|
+
- Titre = message Conventional Commit (il devient le commit sur `main`).
|
|
313
|
+
- Description : **Quoi / Pourquoi / Comment / Comment tester / Risques**, F-ID référencée.
|
|
314
|
+
- Merge uniquement si CI verte (lint, types, tests matrice, couverture). Stratégie : **squash merge**.
|
|
315
|
+
|
|
316
|
+
### Releases
|
|
317
|
+
|
|
318
|
+
- **SemVer** (`0.y.z` tant que l'API n'est pas stable). Tags annotés `vX.Y.Z` sur `main`.
|
|
319
|
+
- `CHANGELOG.md` au format *Keep a Changelog*, alimenté depuis les commits.
|
|
320
|
+
|
|
321
|
+
### Interdits absolus
|
|
322
|
+
|
|
323
|
+
- `git push` sur `main`, `git push --force` (seul `--force-with-lease` sur sa propre branche, après
|
|
324
|
+
accord), `--no-verify`, réécriture d'un historique partagé.
|
|
325
|
+
- `git add -A` / `git add .` à l'aveugle : ajouter des chemins explicites et relire
|
|
326
|
+
`git diff --staged` avant chaque commit.
|
|
327
|
+
- Commiter des secrets, `.env`, caches (`.venv`, `__pycache__`, `.pytest_cache`, `.ruff_cache`),
|
|
328
|
+
ou des fichiers sans rapport avec la tâche.
|
|
329
|
+
- Toute commande destructive (`reset --hard`, `clean -fd`, `branch -D`) sans accord explicite.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## 9. Stratégie de tests
|
|
334
|
+
|
|
335
|
+
- **Unitaires (`tests/unit/`)** : `extractor`, `validator`, `models` testés en isolation, sans
|
|
336
|
+
charger le plugin.
|
|
337
|
+
- **Intégration (`tests/integration/`)** : plugin de bout en bout via la fixture officielle
|
|
338
|
+
**`pytester`** (fichiers de test générés, assertions sur la sortie et le code de retour).
|
|
339
|
+
- **Chaque détection a son test négatif** : un test prouvant l'absence de faux positif sur un mock
|
|
340
|
+
correct équivalent.
|
|
341
|
+
- Couvrir systématiquement : fonction, méthode d'instance, `classmethod`, `staticmethod`, `async def`,
|
|
342
|
+
`BaseModel`, `dataclass`, `TypedDict`, `Union`/`Optional`, génériques, `side_effect` (itérable,
|
|
343
|
+
callable, exception), `ForwardRef`, `TYPE_CHECKING`, builtins C sans signature.
|
|
344
|
+
- Noms explicites : `test_<comportement>_when_<condition>` (ex.
|
|
345
|
+
`test_reports_zombie_key_when_dict_has_removed_field`).
|
|
346
|
+
- Tests déterministes, sans réseau, sans dépendance à l'ordre. Paramétrer (`pytest.mark.parametrize`)
|
|
347
|
+
plutôt que dupliquer.
|
|
348
|
+
- Couverture ≥ 90 % lignes + branches sur `src/mockrot`. Pas de test écrit uniquement pour la
|
|
349
|
+
couverture.
|
|
350
|
+
- Benchmark de l'overhead sur une suite synthétique de 500 tests avant chaque release.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## 10. Commandes
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
uv sync # installer l'environnement
|
|
358
|
+
uv run ruff format . # formater
|
|
359
|
+
uv run ruff check . # linter (ajouter --fix si pertinent)
|
|
360
|
+
uv run pyright # typage strict
|
|
361
|
+
uv run pytest # tests
|
|
362
|
+
uv run pytest --cov=mockrot --cov-branch # tests + couverture
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Vérification complète (obligatoire avant chaque commit) :**
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
uv run ruff format --check . && uv run ruff check . && uv run pyright && uv run pytest
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## 11. Definition of Done
|
|
374
|
+
|
|
375
|
+
Une tâche est terminée **uniquement si** :
|
|
376
|
+
|
|
377
|
+
- [ ] L'exigence (F-ID) est satisfaite, cas limites inclus.
|
|
378
|
+
- [ ] Tests ajoutés : cas positif **et** test négatif (pas de faux positif).
|
|
379
|
+
- [ ] `ruff format`, `ruff check`, `pyright --strict`, `pytest` : tous au vert.
|
|
380
|
+
- [ ] Couverture ≥ 90 % maintenue.
|
|
381
|
+
- [ ] Les invariants de la §4 sont respectés (fail-safe, transparence, perf).
|
|
382
|
+
- [ ] Docstrings / README / CHANGELOG mis à jour si le comportement visible change.
|
|
383
|
+
- [ ] Nouveau piège technique documenté en §5 le cas échéant.
|
|
384
|
+
- [ ] Commits atomiques, Conventional Commits, branche propre et rebasée sur `main`.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## 12. Décisions tranchées et actées
|
|
389
|
+
|
|
390
|
+
- **CLI autonome** : module léger `cli.py` (point d'entrée console `mockrot = "mockrot.cli:main"` dans `pyproject.toml`) qui délègue à `pytest` avec `--mockrot` activé, garantissant le fonctionnement sans friction de `uvx mockrot`.
|
|
391
|
+
- **Budget de performance** : overhead du moteur de validation pur ciblé à **< 15 ms** sur 500 tests ; seuil d'alerte global de la suite de tests (incluant le rapport terminal Rich) fixé à **< 50 ms**.
|
|
392
|
+
- **Mode `accessed-only`** : planifié pour la V1.1 (`--mockrot-mode=accessed-only`) pour éliminer le bruit sur les vieilles bases de code sans compliquer le MVP V1.
|
|
393
|
+
- **LibCST** : confirmé en extra optionnel `pytest-mockrot[fix]` pour garder le package principal ultraléger.
|
|
394
|
+
- **Licence** : licence **MIT**.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-10-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Core Interception (F1):** Transparent proxy for `unittest.mock._patch` (`@patch`, `with patch(...)`, `.start()`) and `pytest.MonkeyPatch.setattr` with fast, non-blocking stack inspection via `sys._getframe`.
|
|
12
|
+
- **Signature Validation (F2):** Runtime signature contract extraction via `inspect.signature` with `self`/`cls` parameter binding and keyword argument validation.
|
|
13
|
+
- **Return Type Validation (F3):** Strict runtime type validation of scalar types, Pydantic v2 models, dataclasses, and TypedDicts powered by `pydantic.TypeAdapter`.
|
|
14
|
+
- **Async & Coroutine Support (F4):** Interception and validation of `async def` targets, coroutines, and `AsyncMock` post-await values.
|
|
15
|
+
- **Side Effect Handling (F5):** Dynamic per-item validation for `side_effect` generators, iterables, and callables, with built-in immunity for intentional exception simulation.
|
|
16
|
+
- **Zombie Field Detection (F6):** Automatic detection of deleted or renamed keys in mocked dictionaries for `BaseModel`, `dataclass`, and `TypedDict`.
|
|
17
|
+
- **Execution Modes (F7):** Non-blocking audit mode (`--mockrot=warn`) and strict CI gatekeeper mode (`--mockrot=strict`).
|
|
18
|
+
- **Exemptions & Filtering (F8):** Line-level `# mockrot: ignore` comments and `@pytest.mark.mockrot_ignore` test marks.
|
|
19
|
+
- **Rich Terminal Reporting:** Clean, emoji-free terminal output with actionable diff tables and line-number diagnostics.
|
|
20
|
+
- **CLI Entry Point:** Standalone `mockrot` command allowing instant execution via `uvx mockrot`.
|
|
21
|
+
- **CI & Quality Assurance:** Complete GitHub Actions test matrix across Python 3.10, 3.11, 3.12, and 3.13 with 92% code coverage and strict Pyright verification.
|