testless
A Rust CLI that selects tests at function granularity from static AST analysis. It reads a diff, walks a tree-sitter-built graph of defs, tests, calls, and reads, and outputs the exact tests a change affects, free and local, with no runtime coverage instrumentation.
Test suites grow, and CI time grows with them. testless reads a code change and answers which tests it actually affects, at the function level, from static analysis alone. No test runner, no coverage collection, no instrumented process.
The problem
Existing test-selection tools stop at the file. jest --findRelatedTests,
vitest --changed, and Nx affected all reason about which files changed,
not which functions inside them did. Change one function in a file with forty
tests and all forty run, because the tool has no finer unit to reason about.
Runtime-coverage products go finer. Wallaby and Datadog Test Impact Analysis select at close to function precision, but they cost money and need your test process instrumented before they produce anything. Nothing free and local selects at function granularity from a plain diff. testless is for that gap: static analysis, run locally, at zero cost.
The approach
testless commits to one guarantee: the selected set always contains every
test truly impacted by a change. Over-approximate, never under. When the
analysis cannot prove a definition is unaffected, it selects it anyway, and
when indexing hits an unparseable file, a corrupt cache, or any internal
error, the output is not a smaller selection. It is run_all, with a reason
attached. A selector that silently skips a test is worse than no selector, so
that failure mode is designed out rather than patched in later.
tree-sitter parses each file into a graph of files, defs (functions, methods,
classes, and a file’s top-level code as module_init), and tests, connected
by contains edges for nesting, imports edges for file-to-file
dependencies, and calls/reads edges for the references inside a def’s
body. A structural differ fingerprints each def’s signature and body
separately, so a formatting-only or comment-only edit produces zero seeds. A
reverse-reachability walk then follows those edges backward from a changed
def to every test that could observe it: caller chains, reader chains, and
container nesting, widening to a module’s transitive importers when its
top-level code changes. Where resolution cannot prove a single target,
typically an unresolved dynamic call, the walk widens to every def matching
that name rather than guessing. Each language plugs into one shared
Language trait, so adding a language means writing a grammar and a
resolver, not rewriting the engine. Tests get id chains rather than bare
names: a TypeScript test resolves as ["add", "handles negatives"], one
entry per describe/it level, and a Go test follows t.Run subtest
nesting. Re-indexing hashes each file with blake3 and reuses the prior
extraction when the hash has not changed, cached in .testless/ with
bincode, so a second run skips reparsing untouched files.
Status
The tool does the thing it is for: testless select --from <rev> prints the
tests a change impacts, and --format args renders them as ready-to-run
vitest run, go test, or cargo test invocations. Exit code 0 means a
real selection; exit code 2 means testless fell back to run_all, with a
reason. testless changes --from <rev> shows the classified seeds a step
before the walk turns them into tests.
TypeScript/JavaScript, Go, and Rust are all supported behind the same
Language trait, each with its own resolver crate: lang-ts, lang-go,
lang-rust, plus core for the shared engine and cli for the binary. CI
indexes and selects against testless’s own repository on every push,
including a warm-cache reindex that should reparse nothing and a selection
run on a clean checkout that should come back empty. testless is on
crates.io at 0.5.0, with release-please cutting versions and prebuilt
binaries for Linux and macOS on x86_64 and aarch64.
An edge-case catalog in the design spec tracks sixty-four cases the
resolvers have to survive: barrel re-exports, computed test names,
table-driven subtests, circular imports, Go’s implicit interface
satisfaction, generics, dot imports, build tags, and more. Each gets a
fixture and a snapshot of the expected selection. Go’s implicit interface
satisfaction is not modeled yet, there is still no why command to explain
a selection, and SCIP-sharpened call resolution, which would replace a lot
of today’s name-based widening with real type information, is planned but
not built. The launch post covers where this
started and what changed since.
Install
cargo install testless # compile from source
The binary is testless. TypeScript/JavaScript, Go, and Rust are the three
supported languages today. Licensed MIT.