testless

Author, maintainer · Since Jul 2026

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.

Terminal recording of testless selecting tests from a git diff

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.