Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ CLI flag > env var > default. Implemented in `plugin.py` and read once into modu
| `PYTEST_TESTCONTAINERS=0` | `--no-testcontainers` |
| `PYTEST_TESTCONTAINERS_REUSE=1` | `--testcontainers-reuse` / `--testcontainers-no-reuse` |
| `PYTEST_TESTCONTAINERS_PROJECT=NAME` | `--testcontainers-project=NAME` |
| `PROJECT_NAME=NAME` | — |
| `COMPOSE_PROJECT_NAME=NAME` | — |
| `PYTEST_TESTCONTAINERS_NAME_TEMPLATE=TPL` | `--testcontainers-name-template=TPL` |
| `PYTEST_TESTCONTAINERS_NO_DAEMON_CHECK=1` | — |
| `PYTEST_TESTCONTAINERS_QUIET=1` | — |
| — | `--testcontainers-clean` (prune + exit 0) |
Expand Down
63 changes: 60 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,53 @@ Every plumbing concern — daemon ping, reuse name, atexit cleanup,
Ryuk-disable-when-reuse — applies. `args`/`kwargs` go to the upstream
constructor verbatim.

## Container names

Every container the plugin starts is named so you can find it in `docker ps`
(previously default-mode containers got random Docker names like
`heuristic_cannon`). The name is built from a template:

```
{project}-tc-{service}-{branch}-{dirtag}-{worker}
```

e.g. `myproj-tc-psql-feature-x-3f9ac1b2-master`.

Placeholders:

| Placeholder | Meaning |
|-------------|---------|
| `{project}` | Project name (see precedence below). |
| `{service}` | Service slug (`psql`, `redis`, `mysql`, `mongo`, `rabbitmq`, or derived from the container class). |
| `{branch}` | Current git branch (read from `.git/HEAD`, no subprocess). Detached HEAD → 12-char commit SHA. No repo → empty. |
| `{dir}` | Worktree-root basename. Readable, not collision-proof. |
| `{dirtag}` | 8-hex hash of the worktree-root absolute path. Disambiguates the same branch checked out in two directories. |
| `{worker}` | xdist worker id (`master` when not under xdist). |
| `{rand}` | 4 random hex chars. Empty in reuse mode; auto-appended in default mode when the template omits it. |

Empty placeholders collapse cleanly (no `--`, no dangling dashes). Names are
sanitized to Docker's charset and capped at 255 chars (trim + checksum).

Override the template (CLI > env > pyproject > built-in default):

```bash
pytest --testcontainers-name-template='{project}-{service}-{branch}'
# or
export PYTEST_TESTCONTAINERS_NAME_TEMPLATE='{project}-{service}-{branch}'
```

```toml
# pyproject.toml
[tool.pytest_testcontainers]
name_template = "{project}-{service}-{branch}"
```

The `{project}` component resolves as: `--testcontainers-project` →
`PYTEST_TESTCONTAINERS_PROJECT` → `PROJECT_NAME` → `COMPOSE_PROJECT_NAME` →
`pyproject.toml [project].name` → cwd basename.

An unknown placeholder raises `NameTemplateError` listing the valid ones.

## Reuse mode

For iterative dev loops where you don't want to pay container-start
Expand All @@ -395,9 +442,11 @@ pytest --testcontainers-reuse tests/
```

What changes:
- Each container gets a stable name `<project>-tc-<service>-<worker>`
(e.g. `myproject-tc-psql-master`). The project name comes from
`pyproject.toml [project].name`.
- Each container gets a stable, identifiable name from the template
(default `{project}-tc-{service}-{branch}-{dirtag}-{worker}`), so reuse is
scoped per project **and** per git branch **and** per directory. Switching
branch or worktree yields a *new* container; the previous one lingers
(stopped) until you run `pytest --testcontainers-clean`.
- Ryuk (testcontainers' reaper) is disabled so the named containers
survive between runs.
- On the next run we look up by name — found-and-running gets bound
Expand All @@ -419,6 +468,10 @@ own `PYTEST_TESTCONTAINERS_PROJECT` to avoid name collisions. The
plugin doesn't auto-namespace by PID — that would defeat the "reuse
across runs" point.

> **Upgrading from 0.1.0:** reuse-mode names now encode branch + directory, so
> containers reused from an older version are not found by name — a fresh one
> is created and the old one lingers until `pytest --testcontainers-clean`.

## Configuration

No TOML config table. The handful of toggles read at maker-call time:
Expand All @@ -430,6 +483,9 @@ No TOML config table. The handful of toggles read at maker-call time:
| `PYTEST_TESTCONTAINERS=0` | Disable plugin fixtures (raise UsageError). |
| `PYTEST_TESTCONTAINERS_REUSE=1` | Reuse named containers across runs. |
| `PYTEST_TESTCONTAINERS_PROJECT=<name>` | Override the `<project>` part of reuse names. |
| `PROJECT_NAME=<name>` | Generic project-name source (below the plugin var). |
| `COMPOSE_PROJECT_NAME=<name>` | docker-compose's project var (below `PROJECT_NAME`). |
| `PYTEST_TESTCONTAINERS_NAME_TEMPLATE=<t>` | Override the container-name template. |
| `PYTEST_TESTCONTAINERS_NO_DAEMON_CHECK=1` | Skip Docker daemon ping (rare). |
| `PYTEST_TESTCONTAINERS_QUIET=1` | Suppress one-shot informational advisories. |

Expand All @@ -441,6 +497,7 @@ No TOML config table. The handful of toggles read at maker-call time:
| `--testcontainers-reuse` | `PYTEST_TESTCONTAINERS_REUSE=1` |
| `--testcontainers-no-reuse` | force fresh-each-run mode |
| `--testcontainers-project=NAME` | `PYTEST_TESTCONTAINERS_PROJECT=NAME` |
| `--testcontainers-name-template=T` | `PYTEST_TESTCONTAINERS_NAME_TEMPLATE=T` |
| `--testcontainers-clean` | prune `<project>-tc-*` and exit 0 |

CLI > env > defaults.
Expand Down
95 changes: 66 additions & 29 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ MIT, matching `testcontainers-python`. Confirmed before publishing.
disabled, port-conflict detection on restart of stopped containers.
Activated globally via env (`PYTEST_TESTCONTAINERS_REUSE=1`) or CLI
(`--testcontainers-reuse`).
- xdist-worker-aware reuse names:
`<project>-tc-<service>-<worker_id>`, where `worker_id` defaults to
`master` for non-xdist runs.
- xdist-worker-aware container names: the worker id is one component
(`{worker}`) of the templated name (§8.7), defaulting to `master`
for non-xdist runs.
- Normalized `pytest.UsageError` when Docker daemon is unreachable
(§7).
- `atexit` safety net for cleanup when `pytest_unconfigure` is skipped
Expand Down Expand Up @@ -934,16 +934,19 @@ fixture (which we don't redefine — it's already in pytest-xdist).

### 7.4 Per-worker reuse-mode names

Reuse names are `<project>-tc-<service>-<worker_id>`. With `-n 8`:
Container names embed the xdist worker id via the `{worker}` template
component (§8.7), so each worker gets its own container. With `-n 8` the
names differ only in that trailing component (the shared
project/service/branch/dirtag prefix is shown here as `…`):

```
myproject-tc-psql-gw0
myproject-tc-psql-gw1
-gw0
-gw1
myproject-tc-psql-gw7
-gw7
```

For non-xdist runs: `myproject-tc-psql-master`.
For non-xdist runs it is `master` (e.g. `…-master`).

Subtlety: a previous run with `-n 8 --testcontainers-reuse` leaves 8
named containers. A subsequent `-n 4` reuses 4 of them and ignores
Expand Down Expand Up @@ -973,6 +976,9 @@ No TOML config table. The handful of toggles read at maker-call time:
| `PYTEST_TESTCONTAINERS=0` | Disable plugin fixtures only. Each `tc_*` fixture is replaced at registration time with a stub that raises `UsageError("--no-testcontainers; user expected to provide service externally")`. Maker functions called from non-pytest code (scripts, REPL — see §8.6) are unaffected. Accepts `0`/`false`/`no`. |
| `PYTEST_TESTCONTAINERS_REUSE=1` | Reuse mode (§8.5). |
| `PYTEST_TESTCONTAINERS_PROJECT` | Override the project name used in reuse names (§8.3). Default: derived from `pyproject.toml [project].name` if a `pyproject.toml` is reachable from `Path.cwd()` walking up; else `"pytest-tc"` with a one-shot stderr warning at first maker invocation (§8.3). |
| `PROJECT_NAME` | Generic project-name source, below `PYTEST_TESTCONTAINERS_PROJECT` and above `COMPOSE_PROJECT_NAME` (§8.3). |
| `COMPOSE_PROJECT_NAME` | docker-compose's standard project var, below `PROJECT_NAME` (§8.3). |
| `PYTEST_TESTCONTAINERS_NAME_TEMPLATE` | Override the container-name template (§8.7). |
| `PYTEST_TESTCONTAINERS_NO_DAEMON_CHECK=1` | Skip Docker `ping()` (rare, for debugging in environments where ping returns false-negatives). |
| `PYTEST_TESTCONTAINERS_QUIET=1` | Suppress all one-shot stderr advisory notices (docker-exec fallback warning §4.6, project-name fallback warning §8.3). Errors still surface; only informational tips are silenced. |

Expand All @@ -984,21 +990,25 @@ No TOML config table. The handful of toggles read at maker-call time:
| `--testcontainers-reuse` | Same as `PYTEST_TESTCONTAINERS_REUSE=1`. |
| `--testcontainers-no-reuse` | Force function-scoped mode regardless of env. |
| `--testcontainers-project=NAME` | Override `PYTEST_TESTCONTAINERS_PROJECT`. |
| `--testcontainers-name-template=T` | Override the container-name template (§8.7). Highest precedence. |
| `--testcontainers-clean` | Stop+remove all `<project>-tc-*` containers and exit pytest with code 0. |

Precedence: CLI > env > defaults.

### 8.3 Project name resolution (for reuse names)

The reuse name is `<project>-tc-<service>-<worker_id>`. Project
component resolved as:
The `{project}` placeholder in the container-name template (§8.7) resolves
as:

1. CLI `--testcontainers-project=NAME` if given.
2. Env `PYTEST_TESTCONTAINERS_PROJECT` if set.
3. `pyproject.toml [project].name` from the nearest `pyproject.toml`
walking up from `Path.cwd()`. We use `tomllib` (stdlib >=3.11; on
3.10 we depend on `tomli`).
4. Fallback literal `"pytest-tc"`.
3. Env `PROJECT_NAME` if set.
4. Env `COMPOSE_PROJECT_NAME` if set.
5. `pyproject.toml [project].name` from the nearest `pyproject.toml`
walking up from `Path.cwd()` (`tomllib` ≥3.11; `tomli` on 3.10).
6. The cwd directory basename.
7. Fallback literal `"pytest-tc"` (only when the cwd basename is empty,
i.e. the filesystem root) — this is what triggers the one-shot advisory.

Justification for using `pyproject.toml [project].name` as default:
- The project already names itself there for PyPI; reusing it avoids
Expand Down Expand Up @@ -1086,22 +1096,23 @@ safe but pass through `_sanitize` defensively.
`<worker_id>` from pytest-xdist is `master` or `gw<n>` (n in
0..255), already safe.

After composition the full name is bounded by Docker's 255-char
limit. Components are not individually capped; if a user has an
extreme `<project>` name and 8 workers' worth of long suffixes, the
plugin truncates the composed string to 255 chars and appends a
4-char hex hash of the original to keep uniqueness:
After rendering, the full name is bounded by Docker's 255-char limit.
Components are not individually capped; the plugin sanitizes the whole
rendered name and, if it exceeds 255 chars, truncates to 246 and appends
an 8-char hex hash of the *full untruncated* name to keep uniqueness
(`finalize_name`):

```python
def _compose(project: str, service: str, worker: str) -> str:
name = f"{project}-tc-{service}-{worker}"
if len(name) <= 255:
return name
h = hashlib.blake2s(name.encode(), digest_size=2).hexdigest() # 4 chars
return name[:250] + "-" + h
def finalize_name(name: str) -> str:
s = sanitize_component(name)
if len(s) <= 255:
return s
h = hashlib.blake2s(name.encode(), digest_size=4).hexdigest() # 8 chars
return s[:246] + "-" + h
```

In practice this never fires; included for robustness.
Branch names make long names more likely than the old worker-only
suffix, so this guard matters more than it once did.

### 8.5 Reuse mode mechanics

Expand Down Expand Up @@ -1177,8 +1188,8 @@ Behavior outside pytest:
random `/tmp` dir, this falls back to literal `pytest-tc` — set
`PYTEST_TESTCONTAINERS_PROJECT` if you care.
- **Worker-id component** (§7.3) defaults to `master` outside
pytest-xdist (the env var is unset), so reuse names are
`<project>-tc-<service>-master`. A script and a pytest run on the
pytest-xdist (the env var is unset), so the name's `{worker}`
component is `master`. A script and a pytest run on the
same machine without `-n` will share that container; with `-n N`
they don't (workers use `gw0..gwN-1`). This is desired — it lets
a `manage.py runserver` reuse the same container that `pytest`
Expand Down Expand Up @@ -1209,6 +1220,32 @@ with make_postgres(image="iplweb/bpp-dbserver:psql-16.13",

This is fully supported and will not be broken in v1.x.

### 8.7 Container name template

Every container the plugin starts — in **both** default and reuse mode — is
named by expanding a template and passing the result through `finalize_name`
(§8.4 sanitization + 255-char trim/checksum).

Default template: `{project}-tc-{service}-{branch}-{dirtag}-{worker}`.

Placeholders: `{project}` (§8.3), `{service}` (slug), `{branch}`
(`.git/HEAD`, no subprocess; detached → 12-char SHA; no repo → empty),
`{dir}` (worktree-root basename), `{dirtag}` (8-hex blake2s of the
worktree-root absolute path), `{worker}` (xdist id), `{rand}`
(`secrets.token_hex(2)` — empty in reuse mode for byte-stable find-or-create;
in default mode auto-appended when the template omits it). Empty placeholders
collapse with no dangling dashes. An unknown placeholder raises
`NameTemplateError`.

Template precedence: `--testcontainers-name-template` >
`PYTEST_TESTCONTAINERS_NAME_TEMPLATE` >
`pyproject.toml [tool.pytest_testcontainers].name_template` > built-in default.

Reuse-mode names are now per-(project, service, branch, directory, worker);
switching branch/worktree creates a new container and leaves the old one until
`--testcontainers-clean`. The maker `reuse_name=` argument still overrides the
whole scheme (sanitized verbatim, not templated).

---

## 9. Error handling
Expand Down Expand Up @@ -1559,7 +1596,7 @@ machinery is no longer in #1.
| `BPP_TESTCONTAINERS_REUSE=1` env | #1: `PYTEST_TESTCONTAINERS_REUSE=1` | |
| Hard-coded PG image `iplweb/bpp_dbserver:psql-16.13` | User-side: `make_postgres(image=…)` in their fixture | Default in #1 is `postgres:16`. |
| Hard-coded user `bpp` / pass `password` / db `bpp` | User-side: `make_postgres(username=…, password=…, database=…)` | Default in #1 is `test`/`test`/`test`. |
| `_PG_NAME = "bpp-tc-pg"` / `_REDIS_NAME = "bpp-tc-redis"` | #1: `<project>-tc-<service>-<worker_id>` (§8.3) | Project name resolved from `pyproject.toml [project].name`. |
| `_PG_NAME = "bpp-tc-pg"` / `_REDIS_NAME = "bpp-tc-redis"` | #1: templated name, default `{project}-tc-{service}-{branch}-{dirtag}-{worker}` (§8.7) | Project name resolved from `pyproject.toml [project].name`. |
| `os.environ["DJANGO_BPP_DB_HOST"] = …` (5 vars) | #2 (`pytest-testcontainers-django`) | This was the eager-start env-injection. Out of #1 entirely. |
| `os.environ["DJANGO_BPP_TEST_TEMPLATE"] = "bpp"` | #3 (`django-pg-baseline`) | TEMPLATE wiring is Django-creation-machinery layer. |
| `os.environ["DJANGO_BPP_SKIP_DOTENV"] = "1"` | #2 | Suppressing `.env` re-read after env injection — Django-specific. |
Expand Down
Loading
Loading