TestPrune.Core 0.1.0-beta.1

This is a prerelease version of TestPrune.Core.
dotnet add package TestPrune.Core --version 0.1.0-beta.1
                    
NuGet\Install-Package TestPrune.Core -Version 0.1.0-beta.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TestPrune.Core" Version="0.1.0-beta.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TestPrune.Core" Version="0.1.0-beta.1" />
                    
Directory.Packages.props
<PackageReference Include="TestPrune.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TestPrune.Core --version 0.1.0-beta.1
                    
#r "nuget: TestPrune.Core, 0.1.0-beta.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TestPrune.Core@0.1.0-beta.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TestPrune.Core&version=0.1.0-beta.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=TestPrune.Core&version=0.1.0-beta.1&prerelease
                    
Install as a Cake Tool

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.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on TestPrune.Core:

Package Downloads
TestPrune.Falco

Falco route-based integration test filtering extension for TestPrune

FsHotWatch

F# file watcher daemon — keeps FSharpChecker warm for instant re-analysis

FsHotWatch.TestPrune

FsHotWatch plugin for TestPrune test impact analysis

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0-beta.1 94 4/2/2026
0.1.0-alpha.9 40 4/2/2026
0.1.0-alpha.8 36 4/1/2026
0.1.0-alpha.7 39 3/31/2026
0.1.0-alpha.6 37 3/30/2026
0.1.0-alpha.5 40 3/27/2026
0.1.0-alpha.4 43 3/27/2026
0.1.0-alpha.3 34 3/26/2026
0.1.0-alpha.2 39 3/23/2026