Metadata-Version: 2.4
Name: flask-coverage
Version: 0.2.1
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
Project-URL: Source Code, https://github.com/abilian/flask-coverage
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: cache timestamp, snapshots panel, embedded text report |
| `GET`  | `/report`             | Cached text report (same format as `coverage report`) |
| `GET`  | `/html/`              | Cached HTML report (coverage.py's native, per-file source views) |
| `GET`  | `/files`              | Cached JSON list: `{file, statements, missing, covered, percent}` |
| `GET`  | `/export`             | Cached merged `.coverage` data file |
| `POST` | `/refresh`            | **Regenerate the cache.** Browser refresh does not. |
| `POST` | `/snapshot?label=…`   | Take a labelled, timestamped copy into `snapshot_dir` |
| `GET`  | `/snapshots`          | JSON list of saved snapshots |
| `GET`  | `/snapshots/<id>`     | Download a saved snapshot's `.coverage` file |
| `POST` | `/reset`              | Erase all collected data |

POST endpoints content-negotiate: `Accept: application/json` returns JSON, anything else gets a 303 redirect to the dashboard with a flash message. Snapshots and refresh are designed to be operable from the dashboard without leaving it.

## 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>`. Two things happen automatically:

1. **Auto-save thread.** Each worker runs a daemon thread that calls `cov.save()` every `autosave_interval` seconds (default 30, configurable via `FlaskCoverage(autosave_interval=…)`; set to 0 to disable). This bounds inter-worker staleness — when worker A serves `/refresh`, the on-disk data from workers B, C, D is at most 30s old.

2. **Non-destructive combine.** Refreshing the cache merges the running worker's data with all sibling parallel files in a temp dir, runs `coverage combine` there, and uses the merged file. The original per-worker files on disk are never deleted, so workers keep accumulating data normally.

The dashboard surfaces both pieces of information: it shows the last-refresh timestamp *and* the autosave interval, so you can reason about freshness from the UI alone.

### Caching and explicit refresh

`/report`, `/files`, `/html/`, and `/export` all serve from a file-based cache (`.flask-coverage-cache/` next to the data file, configurable via `cache_dir=…`). **Refreshing the browser never regenerates the cache** — only `POST /refresh` does. This makes the cost of hitting the dashboard predictable, and prevents accidental thundering herds in production. Cache regeneration is file-locked across workers.

### 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).
