Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Run tests

./scripts/dev/setup-test-data.sh   # once: creates .venv and generates the fixtures
./scripts/dev/test.sh full         # cargo test --workspace --locked --no-fail-fast
./scripts/dev/test.sh --help       # the scoped commands

cargo test alone runs only the root package. --workspace adds datui-lib and datui-cli. CI runs the same tests with cargo nextest run --workspace --locked --no-fail-fast, one process per test, then cargo test --doc --workspace --locked. The Python bindings are tested separately; see Build Python bindings.

Select the checks

Run ./scripts/dev/test.sh check while editing, then select the relevant test target. A name filter selects tests to execute; it does not by itself restrict the executables Cargo builds.

CommandScope
./scripts/dev/test.sh checkCheck datui-lib without linking
./scripts/dev/test.sh unit data_quality::Library test executable; only data-quality tests execute
./scripts/dev/test.sh integration integration_test test_data_qualityApp integration executable; matching quality tests execute
./scripts/dev/test.sh integration home_testHome integration executable
./scripts/dev/test.sh integration statistics_testAnalysis integration executable
./scripts/dev/test.sh cliCLI library tests
./scripts/dev/test.sh preflightFormatting (workspace and fuzz targets) and workspace clippy with all targets
./scripts/dev/test.sh featuresClippy on datui and datui-lib, all targets, with no default features and then each feature alone
./scripts/dev/test.sh features none sqlOnly the listed combinations; none is no features
./scripts/dev/test.sh features --testThe same, then datui-lib’s library tests in each combination
./scripts/dev/test.sh fullFull workspace tests, including doctests; ignored tests remain opt-in
./scripts/dev/test.sh --print fullPrint the command without running it

The script works from any directory and returns the underlying command’s exit status. Apart from features, it keeps the current feature set. It does not install dependencies or prepare fixtures ahead of tests, and leaves ignored tests opt-in. Existing tests can still generate missing fixtures through their fallback helper. Clippy checks all targets, but does not execute tests or link their executables. The existing pre-commit hooks still run formatting and clippy.

Run features after gating code or tests on a feature, and features --test after changing behavior a feature decides. Each combination is a separate Polars build, so the first run is slow. CI runs features none (clippy only) on every pull request; the Nightly workflow runs features --test, then the root crate’s tests with cargo test --no-default-features.

During an edit, run the changed behavior’s regression and related tests. Before submission, broaden to related targets and run formatting/clippy for Rust changes. Run the full suite for cross-cutting App/event-loop, LazyFrame, loading/schema, shared configuration, dependency/feature, and harness/layout changes. For isolated changes, CI supplies full-workspace coverage; report which checks were local. Documentation-only changes need the documentation checks, not Rust tests. Replay the fuzz corpus for parser or matcher changes (./scripts/dev/test.sh integration fuzz_corpus_test). Do not rerun an unchanged broad check merely because another small scoped check finished.

Select multiple affected targets explicitly when needed:

cargo test --locked -p datui --test statistics_test --test distribution_detection_test

For changes to the binary itself, also run cargo check --locked -p datui and exercise the changed CLI behavior. CLI definition tests do not replace this.

Keep existing build artifacts for the edit loop. Changing compiler flags, toolchains or features can cause rebuilds; cargo clean is not a routine test step. tests/ORGANIZATION.md in the repository proposes structural changes to reduce linking and harness overhead.

Heavy runs queue

Waiting for one of 2 heavy test runs to finish (/run/user/1000/datui-test-heavy*.lock)...

unit, integration, preflight, features, full, and any command given --release take one of DATUI_TEST_HEAVY_SLOTS locks (default 2), shared by all of the user’s checkouts and worktrees on the machine, so only that many run at once instead of exhausting memory together. A run that has to wait prints a line once, then starts when a slot frees. check, cli and --print do not take it.

CaseBehavior
Lock file$XDG_RUNTIME_DIR/datui-test-heavy.lock, or /tmp/datui-test-heavy-<uid>.lock without XDG_RUNTIME_DIR
HeldUntil the command exits, by Ctrl-C or a crash too; never by a daemon it starts, such as sccache’s server
test.sh inside a heavy runRuns under the outer run’s lock (DATUI_TEST_LOCK_HELD is set)
No flock (macOS without util-linux)Runs unlocked and says so

When several agents or people share a machine, run full suites, workspace clippy and release builds through test.sh rather than cargo directly, so they queue.

Fixtures

The statistics, distribution-detection and pivot/melt tests read sample files that are too large to commit. scripts/dev/setup-test-data.sh creates .venv, installs scripts/requirements.txt (which pins Polars, NumPy, pyarrow, fastavro and openpyxl in scripts/requirements-fixtures.txt) and generates them, using uv when it is installed and python -m venv otherwise. It is safe to re-run; --force regenerates from scratch.

The test harness looks for .venv/bin/python (.venv\Scripts\python.exe on Windows) and falls back to the system Python, so the environment does not need to be activated. If the fixtures are missing when the tests start, they run the generator themselves.

To regenerate by hand:

.venv/bin/python scripts/generate_sample_data.py

The fixtures are not regenerated automatically once they exist.

CI’s linux job caches tests/sample-data under a key built from every input to the generator:

Key partInput
scripts/generate_sample_data.pyThe generator; it reads no other file
scripts/requirements-fixtures.txtEvery package it imports, and their dependencies, at exact versions
Python versionAs setup-python resolved it
Runner OS and arch
sample-data-v1Schema version; bump it in ci.yml to discard every entry

A restored copy is checked against the SHA-256 manifest saved with it, and regenerated if anything differs. Only runs on main save an entry. If the generator starts reading another file or importing another package, add the file to the key or the package to requirements-fixtures.txt.

Tests only read tests/sample-data. Another test process may have its files memory-mapped, and rewriting one kills that process with SIGBUS. A test that writes its own data writes it elsewhere:

TestsWrite to
Integration testscommon::fixture_dir(): a fresh directory, removed when the process exits
Unit teststempfile::tempdir()

scripts/dev/test.sh fails a test run that wrote into tests/sample-data, unless that run generated the fixtures. The generator rewrites every fixture in place, so do not run it while tests are running.

Cache and config isolation

Tests never read or write the developer’s own cache or config. Each test process points DATUI_CACHE_DIR and DATUI_CONFIG_DIR at scratch directories, removed when it exits.

TestsIsolation
Unit testsAutomatic: CacheManager::new and ConfigManager::new call cache::isolate_cache() under cfg(test)
Integration testsTake the runtime from common::test_runtime(), or call common::isolate_cache(), before building an App, a CacheManager or a ConfigManager

A test binary that reaches either manager without the variables panics with DATUI_CACHE_DIR is not set or DATUI_CONFIG_DIR is not set. Under cargo test a test that forgot can still pass, because an earlier test in the same process set them. cargo nextest run --workspace runs each test in its own process, so it fails any test that depends on another having run first. Run it after adding tests that build an App or touch the cache or config.

Layout

PathTests
tests/integration_test.rsLoad, query, display, end to end; remote_quality:: (in tests/quality/remote.rs) counts Data Quality’s requests at an in-process S3 bucket (tests/common/fake_s3.rs)
tests/quality_spill_test.rsWhat a full Data Quality scan leaves on disk. Its own process: it sets Polars’ spill directory before Polars reads it
tests/quality_bench_test.rsData Quality’s cost: time, requests, bytes, peak memory and spill. Ignored; scripts/dev/quality_bench.py BEFORE_REF runs it here and at an earlier commit
tests/statistics_test.rs, tests/distribution_detection_test.rsAnalysis
tests/pivot_melt_backend_test.rsReshaping
tests/view_store_test.rsSaved views on disk and their scoring
tests/home_test.rs, tests/search_test.rs, tests/locality_test.rsHome screen, recursive search, filesystem detection
tests/config_test.rs, tests/config_integration_test.rs, tests/theme_application_test.rsConfiguration and themes
tests/startup_test.rsThe binary in a pseudo-terminal (Linux): a silent terminal, stalled settings, keys typed before the app exists, startup errors
tests/fuzz_corpus_test.rsEvery committed fuzz corpus input through its target’s body in fuzz/src/; see Fuzzing
tests/cloud_live_test.rsAgainst a real object store. Ignored by default; run with DATUI_LIVE_GCS=1 or DATUI_LIVE_S3=<endpoint> and --ignored
tests/wording_test.rsRetired words (glossary) and “opens anything” claims, in the UI strings, the key registry, docs, --help and the manpages. A real use goes in its ALLOWED list
crates/datui-cli/src/docgen.rsthe_generated_docs_are_current: the generated pages match the code (Build documentation)
crates/datui-lib/src/tests/doc_queries_tests.rsThe docs’ q blocks parse, and their sql and q blocks run on the datasets they name
tests/common/Shared helpers

Unit tests live beside the code they test.

Wait for completion

Tests that drive an App wait on the work, never on a quiet channel. The shared helpers in tests/common/:

HelperWaits for
pump_open_until_loaded(app, rx, paths, options)An open’s whole event chain, then work_pending to clear
drain_events(app, rx)Every queued event and each event it chains to, until work_pending clears
next_event(app, rx)One queued event, or one that pending work still owes; None once nothing is owed
work_pending(app)is_busy(), row_count_pending(), or a footer pass still reading the schema

A wait returns as soon as the work is done. One that runs past HANG_GUARD (300 s) fails the test, naming the wait’s location and what was still owed, rather than falling through to asserts on the previous state.

work_pending ignores abandoned work: a cancelled analysis or a stale worker can keep running after the app stops waiting on it. Tests about those (cancellation, stale results, chart preparation, background discovery) wait on their own condition, as integration_test.rs does with pump_until and ticks(). Library unit tests use crate::tests::work_pending, which also covers the buffer collect.

Startup timing

cargo build --release
scripts/dev/first_frame_probe.py before=/path/to/old/datui after=target/release/datui --runs 20
ColumnMeaning
first frameSpawn to the first output that draws a screen
first rowsSpawn to the first output holding the fixture’s first row
idle CPU, wakeups/s, bytesAll threads, over a window after the rows are drawn

It runs each binary in a 120×30 pseudo-terminal with isolated config and cache, on 1,000-row CSV and Parquet fixtures, and prints p50/p95 as a Markdown table. --silent never answers the keyboard-protocol query and --reply-delay MS answers it late; a build that never asks is unaffected. Linux only. The numbers depend on the machine: they belong in a PR description, not in a test.