1
Testing and CI
anti edited this page 2026-04-18 06:08:46 -04:00

Testing and CI

Pytest is the only game in town. Every new feature ships with tests. Never commit broken code. Those are house rules, not suggestions.

For the numbers that our stress tests produce, see Performance Story. For the knobs used in dev, see Tracing and Profiling and Environment variables.


Layout

Everything lives under tests/:

tests/
  conftest.py              # global fixtures; forces DECNET_DEVELOPER_TRACING=false
  api/                     # FastAPI + repository integration
  docker/                  # image-build smoke (marker: docker)
  live/                    # subprocess-backed service tests (marker: live)
  perf/                    # per-request profiling harness + README
  service_testing/         # protocol-level decoys (SSH, SMB, RDP, ...)
  stress/                  # Locust-driven load tests (marker: stress)
  test_*.py                # unit tests, flat layout

tests/conftest.py pins a few env vars for determinism:

  • DECNET_DEVELOPER=true
  • DECNET_DEVELOPER_TRACING=false (never trace in tests)
  • DECNET_DB_TYPE=sqlite

Running the fast suite

# activate the venv first — see ANTI's rule
source .venv/bin/activate

pytest

pyproject.toml sets the default opt-outs:

addopts = "-m 'not fuzz and not live and not stress and not bench and not docker' \
          -v -q -x -n logical --dist loadscope"

Meaning: the default run skips fuzz, live, stress, bench, and docker. Everything else runs in parallel with xdist, distributed by test module (--dist loadscope).

Markers

All markers are declared in pyproject.toml:

Marker Meaning Opt in
fuzz Hypothesis-based fuzz tests, slow pytest -m fuzz
live Live subprocess service tests pytest -m live
live_docker Live Docker containers (needs DECNET_LIVE_DOCKER=1) pytest -m live_docker
stress Locust load tests pytest -m stress
bench pytest-benchmark micro-benchmarks pytest -m bench
docker Tests that build and run Docker images pytest -m docker

Run everything, including the slow markers:

pytest -m ''

Run a single marker and nothing else:

pytest -m stress tests/stress/ -v -x -n0 -s

Stress tests (Locust)

The stress suite is how we produced every number in Performance Story.

Invocation:

STRESS_USERS=2000 STRESS_SPAWN_RATE=200 \
    pytest -m stress tests/stress/ -v -x -n0 -s

Notes:

  • -n0no xdist parallelism. Locust wants to own the workers.
  • -s — do not capture stdout; you want to see the RPS/latency stream.
  • -x — stop on first failure. A locust failure here means the API regressed.
  • STRESS_USERS and STRESS_SPAWN_RATE are the knobs you tune. 2000 users at spawn rate 200 is our standard "can the API hold" check.

Output CSVs land in development/profiles/ (or wherever the harness is configured to write) — the same CSV format parsed into the perf table on Performance Story.

Before you run stress tests against a live deploy: turn tracing off. Profiles with DECNET_DEVELOPER_TRACING=true are not production-representative. The 1500-user comparison proves this.


Project rules (non-negotiable)

From CLAUDE.md at repo root:

  1. Every new feature must have pytests. No exceptions. If you add a collector, a parser, a service decoy, or a repository method, ship tests alongside it.
  2. Never pass broken code. "Broken" here means: does not run, does not pass 100% of tests, leaves the suite in a red state. If the bar is red when you commit, you are violating the rule.
  3. 100% pass before commit. Green suite, then git commit. Not the other way around.

There is also a live rule worth calling out because it has bitten more than one contributor:

No scapy sniff threads in TestClient lifespan tests. Scapy's sniff() spawns a thread that does not clean up when FastAPI's TestClient tears down the lifespan context. Result: pytest hangs forever at teardown. If you need to verify sniffer behaviour under a live API, use static source inspection (import the module, assert on its attributes / dispatch table) rather than spinning up the real capture loop.

This is why tests/test_sniffer_*.py reaches into module internals instead of just hitting an endpoint.


Quick recipe for adding a feature

  1. Write the failing test (pick the right marker — probably none, i.e. default suite).
  2. Implement the feature.
  3. pytest until green.
  4. If the feature touches the storage layer, use get_repository() or the FastAPI get_repo dependency — never import SQLiteRepository directly. See the DI rule in CLAUDE.md.
  5. If the feature has a performance story, run the stress suite with tracing off and archive the CSV under development/profiles/.
  6. git commit.

Related: Design overview · Performance Story · Tracing and Profiling · Environment variables · Logging.