Skip to content

Commit 8b17bd1

Browse files
authored
added AGENTS.md (#31)
1 parent 28e4310 commit 8b17bd1

File tree

1 file changed

+201
-0
lines changed

1 file changed

+201
-0
lines changed

AGENTS.md

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# AGENTS.md — coverage-sh
2+
3+
Guidelines for agentic coding agents working in this repository.
4+
5+
## Project Overview
6+
7+
`coverage-sh` is a [coverage.py](https://coverage.readthedocs.io/) plugin that measures
8+
code coverage for shell scripts executed from Python. The entire implementation lives in
9+
a single file (`coverage_sh/plugin.py`). The package uses `hatchling` as the build
10+
backend and `uv` as the package manager.
11+
12+
---
13+
14+
## Build & Tooling Commands
15+
16+
All commands are run via `uv run`. Sync dependencies first if needed:
17+
18+
```bash
19+
uv sync --locked --all-extras --dev
20+
```
21+
22+
| Task | Command |
23+
|---|---|
24+
| Format check | `uv run ruff format --check .` |
25+
| Auto-format | `uv run ruff format .` |
26+
| Lint | `uv run ruff check .` |
27+
| Lint + auto-fix | `uv run ruff check --fix --unsafe-fixes .` |
28+
| Type check | `uv run mypy .` |
29+
| Run all tests | `uv run pytest` |
30+
| Run tests with coverage | `uv run coverage run --parallel-mode -m pytest` |
31+
| Build package | `uv build` |
32+
33+
**Before marking any task as complete, always run:**
34+
```bash
35+
uv run ruff check . && uv run mypy . && uv run pytest
36+
```
37+
38+
---
39+
40+
## Running Tests
41+
42+
```bash
43+
# All tests
44+
uv run pytest
45+
46+
# Single test function
47+
uv run pytest tests/test_plugin.py::test_filename_suffix_should_match_pattern
48+
49+
# Single test class
50+
uv run pytest tests/test_plugin.py::TestShellFileReporter
51+
52+
# Single method in a class
53+
uv run pytest tests/test_plugin.py::TestShellFileReporter::test_lines_should_match_reference
54+
55+
# Parametrized variant
56+
uv run pytest "tests/test_plugin.py::test_end2end[True]"
57+
58+
# By keyword
59+
uv run pytest -k "test_handle_missing_file"
60+
```
61+
62+
CI requires **100% branch coverage** (`--fail-under=100`). Use `# pragma: no cover`
63+
only for genuinely unreachable defensive branches.
64+
65+
---
66+
67+
## Code Style
68+
69+
PEP8 + Black. Run `uv run ruff check --fix --unsafe-fixes .` then `uv run ruff format .` before committing.
70+
71+
---
72+
73+
## Import Conventions
74+
75+
Every source file begins with:
76+
77+
```python
78+
from __future__ import annotations
79+
```
80+
81+
Import ordering within a file:
82+
83+
1. `from __future__ import annotations` — always first
84+
2. Standard library bare imports (`import os`, `import sys`, …)
85+
3. Standard library `from … import …`
86+
4. Third-party bare imports
87+
5. Third-party `from … import …`
88+
6. `if TYPE_CHECKING:` block — types needed only for annotations
89+
90+
```python
91+
from __future__ import annotations
92+
93+
import contextlib
94+
import os
95+
from pathlib import Path
96+
from typing import TYPE_CHECKING, Any
97+
98+
import coverage
99+
100+
if TYPE_CHECKING:
101+
from collections.abc import Iterable
102+
from coverage.types import TLineNo
103+
from tree_sitter import Node
104+
```
105+
106+
- Within the package: use **relative** imports (`from .plugin import ShellPlugin`)
107+
- In tests and externally: use **absolute** imports (`from coverage_sh.plugin import …`)
108+
- Put `TYPE_CHECKING`-only imports in the `if TYPE_CHECKING:` block to avoid runtime cost
109+
110+
---
111+
112+
## Naming Conventions
113+
114+
| Construct | Convention | Example |
115+
|---|---|---|
116+
| Modules / files | `snake_case` | `plugin.py`, `test_plugin.py` |
117+
| Classes | `PascalCase` | `ShellFileReporter`, `CovLineParser` |
118+
| Test classes | `Test` prefix | `TestShellFileReporter` |
119+
| Functions / methods | `snake_case` | `find_executable_files()` |
120+
| Private methods | leading `_` | `_parse_ast()`, `_iterdir()` |
121+
| Test methods | `test_<what>_should_<expected_outcome>` | `test_source_should_be_cached` |
122+
| Module constants | `UPPER_SNAKE_CASE` | `TRACEFILE_PREFIX`, `TMP_PATH` |
123+
| Local variables | `snake_case` | `fifo_path`, `line_data` |
124+
| Type aliases | `PascalCase` | `LineData = dict[str, set[int]]` |
125+
126+
---
127+
128+
## Error Handling
129+
130+
Three patterns are used — pick the one appropriate to the situation:
131+
132+
**1. Suppress expected, ignorable errors (preferred for cleanup):**
133+
```python
134+
with contextlib.suppress(FileNotFoundError):
135+
self.fifo_path.unlink()
136+
```
137+
138+
**2. Re-raise with context (for parse/validation errors):**
139+
```python
140+
try:
141+
_, path_, lineno_, _ = line.split(":::", maxsplit=3)
142+
lineno = int(lineno_)
143+
except ValueError as e:
144+
raise ValueError(f"could not parse line {line}") from e
145+
```
146+
147+
**3. Safe fallback / early return for non-fatal failures:**
148+
```python
149+
try:
150+
self._content = self.path.read_text()
151+
except UnicodeDecodeError:
152+
return ""
153+
```
154+
155+
Never use bare `except:` or `except Exception:` without a very specific reason.
156+
157+
---
158+
159+
## Testing Patterns
160+
161+
Framework: **pytest** (see `pyproject.toml` for version).
162+
163+
- One test class per source class, one test function per module-level function
164+
- All tests live in `tests/test_plugin.py`; shared fixtures in `tests/conftest.py`
165+
- Use plain `assert` statements (not `assertEqual`/`assertTrue` etc.)
166+
- Use `pytest.raises(ExceptionType, match="regex")` for exception assertions
167+
- **No monkeypatching** — do not use `monkeypatch.setattr()` to patch attributes; changing
168+
directories or environment variables directly is fine
169+
- Use `tmp_path` (pytest built-in) for temporary file isolation
170+
- Test method naming: `test_<what>_should_<expected_outcome>`
171+
- Integration tests use `subprocess.run` to invoke `coverage run` against a real project
172+
in a temp directory
173+
174+
**Test double conventions (defined as inner classes inside test classes):**
175+
- Spy: subclass that overrides one method and records calls — e.g., `CovLineParserSpy`
176+
- Fake: minimal implementation of an interface — e.g., `CovWriterFake`
177+
- Stub: minimal object satisfying a dependency — e.g., `MainThreadStub`
178+
179+
**Parametrize:**
180+
```python
181+
@pytest.mark.parametrize("cover_always", [(True), (False)])
182+
def test_something(cover_always: bool) -> None:
183+
...
184+
```
185+
186+
---
187+
188+
## Architecture Notes
189+
190+
- **Single-module plugin:** all production logic is in `coverage_sh/plugin.py`;
191+
`coverage_sh/__init__.py` is only a registration entry point
192+
- **File detection:** uses `python-magic` (libmagic) MIME type detection
193+
(`text/x-shellscript`), not file extensions — do not assume `.sh` == shell script
194+
- **Two coverage modes:** subprocess monkey-patching (default) and `cover_always` mode
195+
(sets `BASH_ENV`/`ENV` globally)
196+
- **FIFO pipeline:** bash processes write `COV:::<path>:::<lineno>:::` lines via
197+
`BASH_XTRACEFD` into a named FIFO; `CoverageParserThread` reads and records them
198+
- **Thread safety:** `CoverageParserThread` and `MonitorThread` coordinate shutdown;
199+
always call `stop()` then `join()` in that order
200+
- The `src/coverage_sh/` directory exists but is **not used** — the real package is the
201+
root-level `coverage_sh/`

0 commit comments

Comments
 (0)