Metadata-Version: 2.4
Name: flask-coverage
Version: 0.1.0
Summary: Live code coverage for a running Flask application.
Author: Stefane Fermigier
Author-email: Stefane Fermigier <sf@abilian.com>
License-Expression: MIT
License-File: LICENSE
Requires-Dist: flask>=2.3
Requires-Dist: coverage>=7.4
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# flask-coverage

Live code coverage for a running Flask application.

`flask-coverage` wraps [coverage.py](https://coverage.readthedocs.io/) as a Flask extension and exposes a small debug blueprint at `/debug/coverage`. You can introspect what's been executed so far, take snapshots, view a per-file HTML report, and export the raw `.coverage` data — all from a *running* process, without restarting it.

It is designed for two scenarios:

- **Browser tests.** Run your full Flask app under Playwright/Selenium/Cypress, drive it however you like, then read the live coverage report to see which paths your end-to-end tests actually reach.
- **Production / canary.** Measure what code your live traffic exercises. Coverage measurement carries some overhead (typically <15% on Python 3.12+ with `sys.monitoring`), but for low-to-mid QPS services that's a reasonable trade for ground-truth dead-code detection.

## Install

```bash
pip install flask-coverage
```

Requires Python ≥ 3.12, Flask ≥ 2.3, coverage ≥ 7.4.

## Quickstart

```python
from flask import Flask
from flask_coverage import FlaskCoverage

app = Flask(__name__)
FlaskCoverage(app)   # mounts /debug/coverage
```

Run your app and visit [http://127.0.0.1:5000/debug/coverage/](http://127.0.0.1:5000/debug/coverage/).

A runnable demo with a step-by-step walkthrough lives in [`examples/`](./examples/README.md).

## Endpoints

Mounted under `/debug/coverage` by default (override with `FlaskCoverage(app, url_prefix="…")`).

| Method | Path | Purpose |
| --- | --- | --- |
| `GET`  | `/`         | Dashboard with text report and links to the others |
| `GET`  | `/report`   | Text report (same format as `coverage report`) |
| `GET`  | `/html/`    | Native coverage.py HTML report (per-file source + line highlighting) |
| `GET`  | `/files`    | JSON list of measured files: `{file, statements, missing, covered, percent}` |
| `POST` | `/snapshot` | Flush in-memory counters to disk; returns JSON stats |
| `GET`  | `/export`   | Download the merged `.coverage` data file |
| `POST` | `/reset`    | Erase all collected data |

## Configuration

Coverage settings are read from `[tool.coverage.*]` in `pyproject.toml` automatically (via `coverage.py`'s native config support), or from `.coveragerc` / `setup.cfg` / `tox.ini` if present.

```toml
[tool.coverage.run]
source = ["myapp"]
parallel = true        # recommended for gunicorn/uwsgi (see Multi-worker)

[tool.coverage.report]
omit = ["*/migrations/*", "*/tests/*"]
```

## Starting coverage early

For accurate measurement of module-level code, coverage must start *before* your application modules are imported. Three options, in order of preference:

1. **`COVERAGE_PROCESS_START` env var** (best for gunicorn/uwsgi):

   ```bash
   export COVERAGE_PROCESS_START=$(pwd)/pyproject.toml
   gunicorn myapp:app
   ```

2. **`flask-coverage` CLI shim**:

   ```bash
   flask-coverage --app myapp run --debug
   ```

3. **Manual `start_early()`** as the very first line in `wsgi.py`:

   ```python
   from flask_coverage import start_early
   start_early()                    # before any of your app modules
   from myapp import create_app
   app = create_app()
   ```

If a `Coverage` instance is already running (any of the above, or `pytest-cov` in tests), `FlaskCoverage(app)` *adopts* it instead of creating a duplicate tracer.

## Security

The `/debug/coverage` blueprint exposes filesystem paths for every measured source file — treat it as sensitive. Registration is fail-closed: it requires one of the following, or it raises `RuntimeError`:

- `app.debug` is `True`, or
- `FLASK_COVERAGE_PASSWORD` env var is set (HTTP Basic auth — user `admin`, override with `FLASK_COVERAGE_USERNAME`), or
- a custom `auth=` callback is passed to `FlaskCoverage(...)`:

  ```python
  FlaskCoverage(app, auth=lambda: current_user.is_authenticated and current_user.is_admin)
  ```

The basic-auth check uses `hmac.compare_digest` for constant-time comparison.

## Operations

### Disabling without redeploy

Set `FLASK_COVERAGE_DISABLED` to a truthy value (`1`, `true`, `yes`, `on`) before the process starts, and `FlaskCoverage(app)` becomes a no-op: no tracer, no blueprint, no auth check.

```bash
FLASK_COVERAGE_DISABLED=1 gunicorn myapp:app
```

### Multi-worker (gunicorn / uwsgi)

Each worker traces independently. Set `parallel = true` under `[tool.coverage.run]` so each worker writes `.coverage.<host>.<pid>.<rand>`. The `/report`, `/files`, `/html/`, and `/export` endpoints automatically merge sibling files into a temporary copy before generating output, so a request served by any worker sees coverage from all of them.

The merge is **non-destructive**: original per-worker files on disk are never deleted, so workers keep accumulating data normally.

### Performance

On Python 3.12+, `coverage.py` uses `sys.monitoring` (PEP 669), which is significantly faster than the legacy `sys.settrace` path — typically under 15% overhead. Acceptable for staging and canary; profile before turning on for high-QPS production traffic.

## Development

```bash
git clone https://github.com/abilian/flask-coverage
cd flask-coverage
uv sync
uv run pytest          # 45 tests
make check             # lint + format + type check
```

Tests are organised by the [test pyramid](https://martinfowler.com/articles/practical-test-pyramid.html):

- `tests/a_unit/` — fast, isolated, mock-based
- `tests/b_integration/` — Flask test-client + mocked Coverage
- `tests/c_e2e/` — real `coverage.Coverage` running, including multi-worker simulation

CI runs ruff (lint + format), `ty` (type check), and pytest across Python 3.12 / 3.13 / 3.14.

## License

MIT — see [LICENSE](./LICENSE).
