TestPrune.Core 7.0.1

dotnet add package TestPrune.Core --version 7.0.1
                    
NuGet\Install-Package TestPrune.Core -Version 7.0.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="7.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TestPrune.Core" Version="7.0.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 7.0.1
                    
#r "nuget: TestPrune.Core, 7.0.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@7.0.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=7.0.1
                    
Install as a Cake Addin
#tool nuget:?package=TestPrune.Core&version=7.0.1
                    
Install as a Cake Tool

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

  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.

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.Attributes for 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

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 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 (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
Loading failed