Testing and Linting

Overview of Rust and Python test suites, linter commands, and CI gates.

๐Ÿ”—How-to โ€” test and lint

Verified 2026-07-27.

๐Ÿ”—Commands

commandwhat
just testtest-py then cargo nextest run --all-features
just test-releasecargo nextest run --all-features --no-capture โ€” what CI runs
just test-pycd wrappers/python && uv run --extra dev pytest tests
just test-watchcargo watch -x 'nextest run --workspace'
just lintlint-rust + lint-py
just lint-rustlint-frontend first, then cargo clippy --all-features --all-targets -- -D warnings
just lint-frontendclippy against --target wasm32-unknown-unknown
just fmt / just fmt-checkcargo fmt --all
just cifmt-check lint test lint-py test-py โ€” the full local gate

All Rust test commands use --all-features, which enables server and so triggers the build.rs trunk build. Set EXPMAN_SKIP_FRONTEND_BUILD=1 if you already have a dist/. See build-and-run.

cli_test.rs uses assert_cmd::cargo::cargo_bin_cmd!("exp"), which needs the cli feature โ€” another reason the suite is always run with --all-features.

๐Ÿ”—What each suite covers

๐Ÿ”—tests/integration_test.rs โ€” 12 tests, the core engine

Run dir creation ยท vectors โ†’ Parquet ยท params โ†’ YAML ยท test_log_vector_is_fast (the throughput benchmark, also just bench) ยท status persisted on close ยท artifact save (relative and absolute paths) ยท Parquet schema merge ยท latest-scalar readback ยท corrupt YAML metadata ยท concurrent logging from multiple threads ยท same-step replacement.

๐Ÿ”—tests/api_test.rs โ€” 7 tests, the HTTP layer

In-process via tower::ServiceExt::oneshot against build_router + AppState โ€” no network. A TempDir fixture builds one experiment with one run.

Covers: list experiments, get/update experiment metadata, list runs, get run metadata, server config, get metrics.

Not covered: every Jupyter route, every TensorBoard route, both SSE streams, artifact content.

๐Ÿ”—tests/cli_test.rs โ€” 10 tests, the binary

list (with and without --experiment) ยท inspect ยท clean --keep 5 dry-run (asserts nothing is deleted without --force) ยท export json/csv, with and without data ยท import error paths ยท export --format tensorboard with no data.

๐Ÿ”—wrappers/python/tests/ โ€” 20 tests

test_experiment.py (6): basic file creation, singleton init, artifact round-trip, complex types in config.yaml, crash โ†’ status: FAILED, and test_vectors_vs_scalar โ€” the important one, pinning that scalars land in run.yaml and vectors in vectors.parquet, with cross-assertions both ways.

test_tensorboard.py (14): directory structure, context manager, default log_dir generation, scalars with and without step, add_scalars prefixing, add_text, add_hparams (including empty and None metrics), a parametrized sweep asserting all 11 stub methods do not raise, flush() no-op, sequential writers not interfering, and both log_dir mapping branches.

Not covered: tensorboard_dir; cli.py (the exp console script is never exercised from Python).

test_default_log_dir (test_tensorboard.py:45) takes no tmp_path and can write a runs/ directory into the CWD โ€” which is why wrappers/python/.gitignore contains exactly runs/.

๐Ÿ”—What CI runs

ci.yml (push + PR on main, ignoring **.md, examples/**, docs/**, .gitignore, LICENSE) calls build-assets, then rust.yml and python.yml in parallel.

  • rust.yml โ€” just fmt-check + just lint-rust, then just test-release. Both jobs download frontend-dist and set EXPMAN_SKIP_FRONTEND_BUILD=1.
  • python.yml โ€” just lint-py (ruff over wrappers/python and examples/), then downloads the CLI artifact, just bundle-cli-bin, uv sync --extra dev, builds and installs a wheel, and runs pytest.

Two gaps worth knowing:

  • ruff format never runs in CI โ€” only ruff check. Formatting drift is caught only by pre-commit hooks, which nothing installs automatically.
  • Python tests only ever run on Linux. python.yml:29 computes the CLI artifact name from runner.os with a nested ternary, implying an intended OS matrix that does not exist; the job is always ubuntu-latest.

And the big one: a release does not wait for tests. See release.