TestPrune.Core
7.0.1
dotnet add package TestPrune.Core --version 7.0.1
NuGet\Install-Package TestPrune.Core -Version 7.0.1
<PackageReference Include="TestPrune.Core" Version="7.0.1" />
<PackageVersion Include="TestPrune.Core" Version="7.0.1" />
<PackageReference Include="TestPrune.Core" />
paket add TestPrune.Core --version 7.0.1
#r "nuget: TestPrune.Core, 7.0.1"
#:package TestPrune.Core@7.0.1
#addin nuget:?package=TestPrune.Core&version=7.0.1
#tool nuget:?package=TestPrune.Core&version=7.0.1
TestPrune
Run only the tests your change could have affected.
TestPrune analyzes your F# code to work out which functions depend on which, then uses that map to skip tests that couldn't have been touched by what you changed. The aim: when your suite takes minutes but you changed one function, you wait seconds.
Status: early alpha. This is a young project, substantially AI-written, and still finding its shape. Behavior and APIs shift between versions, so pin a version and expect surprises. Issues and PRs are very welcome.
Why?
When your test suite takes minutes but you only changed one function, running everything is wasteful. TestPrune builds a map of your code — which functions call which, which tests cover which code — and tries to pick just the tests that matter.
Change multiply? Ideally only the multiply tests run. Change a type
that three modules depend on? Those three modules' tests run. Add a new
file? Everything runs, just to be safe.
Quick example
Say you have a math library and some tests
(from examples/SampleSolution):
// src/SampleLib/Math.fs
module SampleLib.Math
let add x y = x + y
let multiply x y = x * y
// tests/SampleLib.Tests/MathTests.fs
[<Fact>]
let ``add returns sum`` () = Assert.Equal(5, add 2 3)
[<Fact>]
let ``multiply returns product`` () = Assert.Equal(12, multiply 3 4)
You change multiply. TestPrune works out that only
multiply returns product needs to run — and skips add returns sum.
Try the CLI
The quickest way to see it work is the test-prune CLI, a reference
implementation that wires the library up for you:
test-prune index # Build the dependency graph
test-prune run # Run only affected tests
test-prune status # Show what would run (dry-run)
test-prune dead-code # Find unreachable production code
It detects changes from your version control (jj or git), so run
index once, then run/status after each edit.
Global options: --repo <path> (repo root, default: auto-detect),
--parallelism <n> (max parallel analyses, default: processor count).
The CLI re-analyzes serially and isn't tuned for big codebases —
FSharp.Compiler.Service type-checking is slow. For real workflows,
embed TestPrune.Core in your build tooling, where you can cache and
parallelize. See the integration guide.
How it works
- Index — Parse every
.fsfile, record which functions/types exist and what they depend on. Store in SQLite. - Diff — Look at what files changed since last commit.
- Compare — Figure out which specific functions changed (added, removed, or modified).
- Walk — Follow the dependency graph from changed functions to find every test that transitively depends on them.
- Run — Execute only those tests.
If anything looks uncertain (new files, project-file changes), it falls back to running everything. Better to run too many tests than miss a broken one.
Declarative dependencies
For edges the analyzer can't see — reflection, DI-by-type, or non-F# files like snapshots, migrations, or config — declare them with marker attributes:
open TestPrune
[<DependsOn(typeof<PluginRegistry>)>] // reflection target
let registerPlugins () = ...
[<DependsOnFile("tests/snapshots/api.snap.json")>] // specific file
[<Fact>]
let ``api snapshot`` () = ...
[<DependsOnGlob("migrations/*.sql")>] // glob
type DbIntegrationTests() = ...
Glob dialect: ** crosses path segments, * stays within one, ? is
a single non-/ char. Paths are repo-relative and case-sensitive. The
attributes are metadata — no runtime behavior.
You declare the attributes yourself
TestPrune matches these by type name, read off the syntax tree. It never loads your assemblies, and the namespace is ignored — so there is nothing to install. Declare the ones you use anywhere in your own code:
namespace TestPrune // any namespace; only the type name is matched
open System
type DependsOnAttribute(target: Type) =
inherit Attribute()
member _.Target = target
type DependsOnFileAttribute(path: string) =
inherit Attribute()
member _.Path = path
type DependsOnGlobAttribute(pattern: string) =
inherit Attribute()
member _.Pattern = pattern
type CompositionRootAttribute() =
inherit Attribute()
Both spellings match, with and without the Attribute suffix — that is
what lets you write [<DependsOnFile ...>]. The attribute does have to
resolve for the compiler, so it must be declared somewhere.
This repo carries the same definitions in
src/TestPrune.Attributesfor its own tests and examples, but that package is not published to NuGet. Declaring them yourself is the supported route.
Composition roots
A routing table or DI registration block names every handler in the codebase in order to wire them up, and an integration-test fixture that boots the app depends on it. So the walk reaches every fixture-using test from every handler: change one handler, select the whole integration suite. Every edge on that path is real — the conclusion isn't. Nothing in the graph distinguishes "wires X up" from "calls X", so you say which symbol is the wiring:
[<TestPrune.CompositionRoot>]
let endpointsFor (route: Route) : HttpHandler =
match route with
| Home -> Handlers.home
| Admin -> Handlers.admin
// ... names every handler
The rule is one-directional, and both halves matter:
- Reached through it — relevance stops. The root is still reported affected, but tests reachable only by continuing past it are not selected.
- Changed itself — relevance flows on as usual. "The app is wired differently now" is what host-booting tests exist to check, so they run.
This is the one setting that makes TestPrune run fewer tests than the
graph implies, so it is the one that can hide a real failure. Annotate
only a symbol whose references are pure composition: if callers depend on
what it computes rather than on which parts it wires together,
annotating it drops real tests. Before annotating, make sure the coupling
it carried is covered some other way —
TestPrune.Falco
attributes each route to its own tests directly, which is the worked
example.
A per-project fail-safe bounds the blast radius: a marked root may narrow a test project's selection, never empty it. If the barrier leaves a project with no tests at all, that project's full selection is restored. That is a bound, not a completeness guarantee — a route with one test that names its URL and another that only clicks through the UI still drops the second. Until your browser tests name the URLs they visit, don't mark a composition root.
Packages
| Package | What it's for |
|---|---|
TestPrune.Core |
The library — use this in your build system or editor |
TestPrune.Attributes |
Consumer-side markers: [<DependsOn>], [<DependsOnFile>], [<DependsOnGlob>], [<CompositionRoot>]. Not published — the attributes are matched by name, so declare them yourself |
TestPrune.Falco |
Extension for Falco web apps (route → test mapping) |
TestPrune.Analyzers |
Opt-in F# analyzer that flags anonymous records (invisible to impact analysis) |
TestPrune |
CLI tool (reference implementation) |
Going deeper
- Integration guide — embed
TestPrune.Core: indexing, two-level caching, finding affected tests, dead-code detection, extensions, the analyzer, and dependency-change fanout. - Full documentation
- API reference
Design choices
Static analysis, not coverage. TestPrune reads your code's AST instead of instrumenting test runs. So you don't need to run tests to build the graph, and there's no flaky-coverage problem. The tradeoff: it may run a few extra tests, but it aims never to miss a broken one.
Safe by default. When in doubt, run everything. A missed broken test is much worse than running a few unnecessary ones.
Single-file storage. The dependency graph is one .test-prune.db
file. No servers, no services. Rebuilds are atomic.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- FSharp.Compiler.Service (>= 43.12.400)
- FSharp.Core (>= 10.1.400)
- Microsoft.Data.Sqlite (>= 10.0.10)
- SQLitePCLRaw.lib.e_sqlite3 (>= 3.53.3)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on TestPrune.Core:
| Package | Downloads |
|---|---|
|
FsHotWatch.TestPrune
FsHotWatch plugin for TestPrune test impact analysis |
|
|
TestPrune.Falco
Falco route-based integration test filtering extension for TestPrune |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 7.0.1 | 96 | 8/18/2026 |
| 7.0.0 | 105 | 8/17/2026 |
| 6.1.2 | 121 | 8/11/2026 |
| 6.1.1 | 146 | 7/20/2026 |
| 6.1.0 | 120 | 7/18/2026 |
| 6.0.0 | 122 | 7/14/2026 |
| 5.0.0 | 135 | 7/11/2026 |
| 4.3.0 | 205 | 6/16/2026 |
| 4.2.3 | 121 | 6/15/2026 |
| 4.2.2 | 134 | 6/12/2026 |
| 4.2.1 | 134 | 6/7/2026 |
| 4.2.0 | 110 | 6/5/2026 |
| 4.1.0 | 128 | 6/4/2026 |
| 4.0.3 | 121 | 6/2/2026 |
| 4.0.2 | 109 | 5/26/2026 |
| 4.0.1 | 120 | 5/4/2026 |
| 4.0.0 | 130 | 4/25/2026 |
| 3.0.2 | 140 | 4/22/2026 |
| 3.0.1 | 142 | 4/20/2026 |
| 3.0.0 | 114 | 4/20/2026 |