TestPrune 0.1.0-alpha.4

This is a prerelease version of TestPrune.
There is a newer version of this package available.
See the version list below for details.
dotnet tool install --global TestPrune --version 0.1.0-alpha.4
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local TestPrune --version 0.1.0-alpha.4
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=TestPrune&version=0.1.0-alpha.4&prerelease
                    
nuke :add-package TestPrune --version 0.1.0-alpha.4
                    

TestPrune

Only run the tests affected by your change.

TestPrune analyzes your F# code to figure out which functions depend on which, then uses that to skip tests that couldn't possibly be affected by what you changed.

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 uses it to pick just the tests that matter.

Change multiply? 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 figures out that only multiply returns product needs to run — and skips add returns sum.

Getting started

dotnet add package TestPrune.Core

1. Index your project

First, build a dependency graph of your code. This parses every .fs file and stores the results in a local SQLite database:

let checker = FSharpChecker.Create()
let db = Database.create ".test-prune.db"
let projOptions = getScriptOptions checker fileName source |> Async.RunSynchronously

match analyzeSource checker fileName source projOptions |> Async.RunSynchronously with
| Ok result ->
    let normalized = { result with Symbols = normalizeSymbolPaths repoRoot result.Symbols }
    db.RebuildProjects([ normalized ])
| Error msg -> eprintfn $"Failed: %s{msg}"

Caching works at two levels — project and file — to skip expensive re-analysis for unchanged code:

// Project-level: skip the entire project if nothing changed
match db.GetProjectKey("MyProject") with
| Some key when key = currentKey -> () // skip
| _ ->
    // File-level: skip individual files within a changed project
    match db.GetFileKey("src/Lib.fs") with
    | Some key when key = currentFileKey ->
        // Load cached results from DB instead of re-analyzing
        let symbols = db.GetSymbolsInFile("src/Lib.fs")
        let deps = db.GetDependenciesFromFile("src/Lib.fs")
        let tests = db.GetTestMethodsInFile("src/Lib.fs")
        // ... use cached data
    | _ ->
        // File changed — run FCS analysis
        // ... analyzeSource, then db.SetFileKey(...)

    db.RebuildProjects([ combined ])
    db.SetProjectKey("MyProject", currentKey)

Cache keys can be anything that changes when source files change. Good options:

  • VCS tree hash (recommended) — jj log -r @ -T commit_id or git rev-parse HEAD gives a content-addressed hash that changes exactly when files change. Fast and correct across branch switches.
  • File metadata — path + size + mtime. The CLI uses this by default. Simple but can be wrong after git checkout (mtime updates even if content is identical).

2. Find affected tests

When you're ready to test, compare the current code against the index to find what changed, then ask which tests are affected:

match selectTests db changedFiles currentSymbolsByFile with
| RunSubset tests -> // only these tests need to run
| RunAll reason   -> // something changed that we can't analyze — run everything

RunSubset gives you a list of specific test methods. RunAll is the safe fallback for situations like .fsproj changes or brand new files where TestPrune can't be sure what's affected.

3. (Bonus) Find dead code

The same dependency graph can find code that's never reached from your entry points:

let result = findDeadCode db [ "*.main"; "*.Program.*" ] false
// result.UnreachableSymbols — functions nothing calls

By default, symbols in test files are excluded from the report. Pass true for includeTests to find dead code in your test suite too (e.g. unused test helpers):

let result = findDeadCode db [ "Tests.MyTests.*" ] true

How it works

  1. Index — Parse every .fs file, record which functions/types exist and what they depend on. Store in SQLite.
  2. Diff — Look at what files changed since last commit.
  3. Compare — Figure out which specific functions changed (added, removed, or modified).
  4. Walk — Follow the dependency graph from changed functions to find every test that transitively depends on them.
  5. 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.

Extensions

Some dependencies don't show up in code — like HTTP routes mapping to handler files. Extensions let you teach TestPrune about these:

type ITestPruneExtension =
    abstract Name: string
    abstract FindAffectedTests:
        db: Database -> changedFiles: string list -> repoRoot: string -> AffectedTest list

TestPrune.Falco is an extension for Falco web apps that maps URL routes to integration tests.

Packages

Package What it's for
TestPrune.Core The library — use this in your build system or editor
TestPrune.Falco Extension for Falco web apps (route → test mapping)
TestPrune CLI tool (reference implementation — see below)

CLI (reference implementation)

The TestPrune CLI is a reference implementation — it shows how to wire up the library, but it's not optimized for production use. In particular, FSharp.Compiler.Service analysis is inherently slow (it type-checks your entire project), so the CLI re-indexes serially and can take a while on large codebases. For real workflows, use TestPrune.Core directly in your build system where you can cache aggressively, parallelize across projects, and integrate with your existing tooling.

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
test-prune dead-code --include-tests  # Include test files in report

Documentation

Design choices

Static analysis, not coverage. TestPrune reads your code's AST instead of instrumenting test runs. This means you don't need to run tests to build the graph, and there's no flaky-coverage problem. The tradeoff: it might run a few extra tests, but it won't miss broken ones. Note that FSharp.Compiler.Service type-checking is not instant — plan on caching aggressively (see the file- and project-level caching APIs) and parallelizing across projects in your integration.

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
2.0.0 0 4/11/2026
1.0.1 76 4/7/2026
0.1.0-beta.1 63 4/2/2026
0.1.0-alpha.9 50 4/2/2026
0.1.0-alpha.8 46 4/1/2026
0.1.0-alpha.7 45 3/31/2026
0.1.0-alpha.6 43 3/30/2026
0.1.0-alpha.5 49 3/27/2026
0.1.0-alpha.4 47 3/27/2026
0.1.0-alpha.3 42 3/26/2026
0.1.0-alpha.2 42 3/23/2026