See a real run

One real change to Spectre.Console (commit 01027ef), from coverage map to job summary. Every output on this page is copied from that run, not written by hand.

The change

A one-line refactor in the Rule widget. It changes no behaviour, but it is a code change, so a normal pipeline would run all 787 tests in 141 classes on every target framework.

// src/Spectre.Console/Widgets/Rule.cs, GetLineWithoutTitle
 var text = border.GetPart(BoxBorderPart.Top).Repeat(maxWidth);
+var style = Style ?? Spectre.Console.Style.Plain;

 return
 [
-    new Segment(text, Style ?? Spectre.Console.Style.Plain),
+    new Segment(text, style),
     Segment.LineBreak
 ];

The run

The map was recorded on main with td record. The pull request then runs:

$ td test --base main -- -f net10.0td: 1 changed file(s), 1 affected test project(s).Tests to run:  filtered src/Spectre.Console.Tests/Spectre.Console.Tests.csproj    (14 of 141 test class(es) run or reference the changed code, 0.6 s of 3.0 s (80% saved))Recorded test time: 0.6 s of 3.4 s (83% saved).Passed!  - Failed:     0, Passed:   156, Skipped:     0, Total:   156 - Spectre.Console.Tests.dll (net10.0)td summary:  passed  src/Spectre.Console.Tests/Spectre.Console.Tests.csproj  (14 class(es), 4.6 s)1 project(s) run, 0 failed, 0 skipped.Recorded test time: 0.6 s of 3.4 s (83% saved).

156 tests in 14 classes ran instead of 787 in 141. The Spectre.Console.Ansi.Tests project was not even built: nothing it depends on changed.

Were those the right 14?

To check, we made GetLineWithoutTitle throw and ran the whole suite. Exactly 14 classes failed, and they are the 14 TestDetta selected: AlignTests+Center, +Left, +Right, CanvasTests, ColumnsTests, ExceptionTests, LayoutTests, PadderTests, PanelTests, RecorderTests, RowsTests, RuleTests, TableTests and TextPathTests. Most of them never mention Rule: they reach it through layouts and panels, which is what the coverage map knows and a name search would not.

The job summary

With --summary-file "$GITHUB_STEP_SUMMARY" (the GitHub Action sets it for you) every pull request gets this summary: what ran, why each class ran, and what was skipped.

TestDetta test selection

1 changed file(s) compared with main.

  • 1 of 1 affected test project(s) ran, 0 skipped.
  • Recorded test time: 0.6 s of 3.4 s (83% saved).
  • All selected tests passed.
  • src/Spectre.Console.Tests/Spectre.Console.Tests.csproj: passed, 14 test class(es), recorded time 0.6 s of 3.0 s (80% saved)
    • why: 14 of 141 test class(es) run or reference the changed code
    • classes:
      • AlignTests+Center: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
      • AlignTests+Left: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
      • AlignTests+Right: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
      • CanvasTests: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
      • ColumnsTests: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
      • ExceptionTests: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
      • LayoutTests: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
      • PadderTests: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
      • PanelTests: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
      • RecorderTests: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
      • RowsTests: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
      • RuleTests: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
      • TableTests: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
      • TextPathTests: ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)
    • skipped: 127 class(es) that ran none of the changed code and name none of it

Class names are shortened here; the summary prints them in full (Spectre.Console.Tests.Unit.RuleTests).

Asking why

Every decision has a reason you can read. -v writes the trail to stderr:

[DBG] CodeChangeDetector: src/Spectre.Console/Widgets/Rule.cs: Modified Spectre.Console.Rule GetLineWithoutTitle(RenderOptions,int)
[DBG] CodeChangeDetector: src/Spectre.Console/Widgets/Rule.cs: 1 changed symbol(s), affects whole project: False
[DBG] TestSelector: Spectre.Console.Rule GetLineWithoutTitle(RenderOptions,int) is affected because it changed
[DBG] TestSelector: Spectre.Console.Rule Render(RenderOptions,int) is affected because GetLineWithoutTitle
[DBG] TestSelector: Spectre.Console.Rule GetLineWithoutTitle(RenderOptions,int): test classes taken from coverage

And --format json gives every class's decision, with its evidence and a time estimate, for scripts and dashboards:

{
  "project": "src/Spectre.Console.Tests/Spectre.Console.Tests.csproj",
  "mode": "Filtered",
  "reason": "14 of 141 test class(es) run or reference the changed code",
  "estimate": {
    "fullSeconds": 3,
    "selectedSeconds": 0.6,
    "savedSeconds": 2.4
  },
  "decisions": [
    {
      "class": "Spectre.Console.Tests.Unit.AlignTests+Center",
      "selected": true,
      "kind": "coverage",
      "evidence": "ran Spectre.Console.Rule.GetLineWithoutTitle(RenderOptions,int), which changed (map 01027efcd6c4)"
    },
    {
      "class": "Spectre.Console.Tests.PublicApiTests",
      "selected": false,
      "kind": "skipped",
      "evidence": "ran none of the changed code and names none of it (map 01027efcd6c4)"
    },
    "…139 more"
  ]
}

Running the tests your own way

td affected --format commands prints the dotnet test call TestDetta would make, so you can run it in your own script:

dotnet test src/Spectre.Console.Tests/Spectre.Console.Tests.csproj --filter 'FullyQualifiedName~Spectre.Console.Tests.Unit.AlignTests+Center.|FullyQualifiedName~Spectre.Console.Tests.Unit.AlignTests+Left.|FullyQualifiedName~Spectre.Console.Tests.Unit.AlignTests+Right.|FullyQualifiedName~Spectre.Console.Tests.Unit.CanvasTests.|FullyQualifiedName~Spectre.Console.Tests.Unit.ColumnsTests.|FullyQualifiedName~Spectre.Console.Tests.Unit.ExceptionTests.|FullyQualifiedName~Spectre.Console.Tests.Unit.LayoutTests.|FullyQualifiedName~Spectre.Console.Tests.Unit.PadderTests.|FullyQualifiedName~Spectre.Console.Tests.Unit.PanelTests.|FullyQualifiedName~Spectre.Console.Tests.Unit.RecorderTests.|FullyQualifiedName~Spectre.Console.Tests.Unit.RowsTests.|FullyQualifiedName~Spectre.Console.Tests.Unit.RuleTests.|FullyQualifiedName~Spectre.Console.Tests.Unit.TableTests.|FullyQualifiedName~Spectre.Console.Tests.Unit.TextPathTests.'

Across 40 repositories

One change is one data point. In the evaluation corpus, members of 40 open-source libraries are changed one at a time; for a change to one member, TestDetta ran a median of 28% of the test classes, and a quarter or less in 18 of the 40 repositories. Libraries where nearly every test touches the core (Scriban, Markdig) gain little; layered code gains most. Every failing class was checked against the selection: 3 were missed out of 2,941 failing changes, and each was fixed.

Share of test classes run per change, by repository Round 1 of the evaluation corpus: for each of 40 repositories, the mean share of its test classes TestDetta selected for a change to one member. Median 28%. 0% 25% 50% 75% 100% opentelemetry-dotnet: 6% of test classes run on average, over 79 failing changesopentelemetry-dotnet6% polly: 7% of test classes run on average, over 65 failing changespolly7% roslynator: 8% of test classes run on average, over 80 failing changesroslynator8% spectre: 9% of test classes run on average, over 80 failing changesspectre9% swashbuckle: 12% of test classes run on average, over 80 failing changesswashbuckle12% nsec: 13% of test classes run on average, over 79 failing changesnsec13% aspnetcore-odata: 13% of test classes run on average, over 79 failing changesaspnetcore-odata13% guardclauses: 13% of test classes run on average, over 24 failing changesguardclauses13% mimekit: 14% of test classes run on average, over 79 failing changesmimekit14% autofixture: 16% of test classes run on average, over 80 failing changesautofixture16% communitytoolkit: 18% of test classes run on average, over 76 failing changescommunitytoolkit18% libgit2sharp: 19% of test classes run on average, over 57 failing changeslibgit2sharp19% castle-core: 19% of test classes run on average, over 80 failing changescastle-core19% serilog: 20% of test classes run on average, over 79 failing changesserilog20% mediator: 21% of test classes run on average, over 80 failing changesmediator21% fastendpoints: 23% of test classes run on average, over 80 failing changesfastendpoints23% fluid: 23% of test classes run on average, over 79 failing changesfluid23% carter: 24% of test classes run on average, over 80 failing changescarter24% lamar: 26% of test classes run on average, over 80 failing changeslamar26% awesomeassertions: 27% of test classes run on average, over 81 failing changesawesomeassertions27% nsubstitute: 29% of test classes run on average, over 3 failing changesnsubstitute29% immediate-handlers: 29% of test classes run on average, over 43 failing changesimmediate-handlers29% open-xml-sdk: 30% of test classes run on average, over 80 failing changesopen-xml-sdk30% fluentvalidation: 32% of test classes run on average, over 80 failing changesfluentvalidation32% unitsnet: 34% of test classes run on average, over 80 failing changesunitsnet34% vogen: 36% of test classes run on average, over 77 failing changesvogen36% nodatime: 43% of test classes run on average, over 80 failing changesnodatime43% graphql-dotnet: 45% of test classes run on average, over 80 failing changesgraphql-dotnet45% newtonsoft-json: 47% of test classes run on average, over 80 failing changesnewtonsoft-json47% autofac: 49% of test classes run on average, over 77 failing changesautofac49% scrutor: 57% of test classes run on average, over 80 failing changesscrutor57% meziantou-analyzer: 63% of test classes run on average, over 80 failing changesmeziantou-analyzer63% automapper: 67% of test classes run on average, over 80 failing changesautomapper67% protobuf-net: 78% of test classes run on average, over 80 failing changesprotobuf-net78% yamldotnet: 78% of test classes run on average, over 80 failing changesyamldotnet78% cliwrap: 81% of test classes run on average, over 69 failing changescliwrap81% nlog: 82% of test classes run on average, over 65 failing changesnlog82% mapperly: 89% of test classes run on average, over 79 failing changesmapperly89% markdig: 98% of test classes run on average, over 80 failing changesmarkdig98% scriban: 100% of test classes run on average, over 71 failing changesscriban100%
Mean share of test classes run per change, evaluation round 1. Hover a row for the number of changes behind it.