Skip to content

Add find-untested-sources skill (C# parse-only test pairing)#733

Open
Evangelink wants to merge 1 commit into
mainfrom
dev/amauryleve/find-untested-sources
Open

Add find-untested-sources skill (C# parse-only test pairing)#733
Evangelink wants to merge 1 commit into
mainfrom
dev/amauryleve/find-untested-sources

Conversation

@Evangelink

@Evangelink Evangelink commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary

Adds find-untested-sources, a parse-only Roslyn static analyzer that maps C# source files to the test files referencing their declared types, and lists production sources with no referring test.

No Compilation, no MetadataReferences, no binding — runs in ~9 s on a 3,900-file repo (measured locally on AITestAgent + msbench fixtures).

What it produces

A deterministic JSON report:

{
  "counts": { "source_files": 3036, "test_files": 867, "untested_files": 1852, "paired_files": 1184 },
  "untested": [
    { "source": "src/Foo/Bar.cs", "decl_count": 8, "suggested_test_path": "tests/Foo.Tests/Bar/BarTests.cs" }
  ],
  "source_to_tests": { "src/Foo/Baz.cs": ["tests/Foo.Tests/BazTests.cs"] }
}
  • untested ordered by API surface (decl_count) descending → drop-in worklist for any test-generation agent.
  • suggested_test_path derived from real <ProjectReference> edges → lands in a project that already compiles against the source.

How it works

  1. Walk repo for .cs, prune bin/obj/.git/node_modules/.vs/packages/ and generated files.
  2. Classify each .csproj as test/source by name suffix (.Tests, .UnitTests, …) or SDK reference (Microsoft.NET.Test.Sdk, MSTest.Sdk, xunit, NUnit, TUnit, Microsoft.Testing.Platform).
  3. Parallel parse → record (ShortName, EnclosingNamespace, FilePath) per type declaration.
  4. Parallel scan of test files → match identifier tokens against the index, disambiguating strictly by namespace using each test file's using directives + enclosing namespace (avoids the false-positive explosion that naive identifier matching causes on common names like Settings or Context).
  5. Suggest a test path by mirroring the source under whichever test project already <ProjectReference>s the source's project.

Honest limitations

Static parse-only → known gaps documented in SKILL.md: reflection-driven tests, DI-resolved interface types not named in source, extension-method calls as instance methods, var/target-typed new()/pattern matching. For these, the skill points users at coverage-analysis.

Measurement context — honest restatement

This skill was prototyped in response to a benchmark observation: test-generation agents spend significant tokens manually pairing source ↔ test files via repeated find/grep/glob calls before writing any tests. A 5×136-instance internal experiment compared a baseline (skill installed, no doc pointer) against an arm with a pointer to the helper added to code-testing-agent's SKILL.md (the doc edit is proposed separately in #734).

After re-doing the analysis correctly (per-task mean rather than volume-weighted aggregate, with a non-.NET control bucket):

Bucket n tasks Per-task mean Δ input tokens
.NET tasks 35 −8.73 % (median −6.98 %)
non-.NET control 101 −0.13 % (median −1.43 %)

Differential ≈ 8.6 pp, Welch's t ≈ −2.02 (right at p ≈ 0.05). Pass rate neutral within noise.

Caveats you should know about

  • The helper was invoked 0/679 times in the experiment. The Copilot CLI router did not auto-load it as a sibling skill, so any token savings come from the model reading the documentation text in the loaded code-testing-agent SKILL.md, not from the helper executing. The runtime value of this skill (i.e. what happens when it actually runs) is still unmeasured.
  • The volume-weighted aggregate I originally cited (−15 %) was misleading — it was dominated by a handful of very-high-token .NET tasks.
  • Per-task variance is ~21 %; 5 runs per task is not enough to nail down per-task effects precisely. The across-task pattern (control bucket flat, .NET bucket shifted) is the clean part of the signal.

This PR ships the skill alone. The doc integration is in PR #734.

Test plan

  • dotnet run scripts/Find-UntestedSources.cs -- <repo-root> produces well-formed JSON on multiple .NET repos (AITestAgent, ocelot, eShop).
  • Output schema verified to match SKILL.md documentation.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Parse-only Roslyn analysis that maps C# source files to the test files
referencing their declared types and lists production sources with no
referring test. No Compilation, no MetadataReferences, no binding.

- File discovery prunes bin/obj/node_modules/.git and generated *.g.cs.
- Test classification by .csproj suffix or test-SDK reference.
- Source index records (ShortName, Namespace, FilePath) per parsed file.
- Test scan walks IdentifierTokens, disambiguates strictly against the
  test file's using directives + enclosing namespace.
- Suggests a test-file path by mirroring the source under the test
  project that already <ProjectReference>s the source's project.

Runs in ~9s on a ~3,900 .cs-file repo (AITestAgent + msbench fixtures).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 8, 2026 12:20
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Skill Coverage Report

Plugin Skill Covered Coverage

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new dotnet-test skill, find-untested-sources, which performs parse-only Roslyn analysis to map C# production source files to test files that reference their declared types, and emits a JSON report highlighting unpaired (“untested”) sources.

Changes:

  • Added SKILL.md documentation describing intended usage, output schema, and limitations.
  • Added scripts/Find-UntestedSources.cs, implementing the parse-only index + test-scan + JSON report pipeline.
Show a summary per file
File Description
plugins/dotnet-test/skills/find-untested-sources/SKILL.md Documents the new skill’s purpose, usage, output schema, and limitations.
plugins/dotnet-test/skills/find-untested-sources/scripts/Find-UntestedSources.cs Implements the analyzer and JSON report generation logic.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 5

Comment on lines +87 to +90
while (cur is not null && cur.Length >= root.Length)
{
var csproj = Directory.EnumerateFiles(cur, "*.csproj").FirstOrDefault();
if (csproj is not null)
Comment on lines +424 to +428
static string GetNamespaceFor(SyntaxNode node)
{
var ns = node.Ancestors().OfType<BaseNamespaceDeclarationSyntax>().FirstOrDefault();
return ns?.Name.ToString() ?? "";
}
Comment on lines +211 to +214
if (d.Namespace.Length == 0)
{
continue;
}
Comment on lines +448 to +450
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var allCsproj = Directory.EnumerateFiles(root, "*.csproj", SearchOption.AllDirectories).ToList();
foreach (var testProj in allCsproj)
Comment on lines +371 to +394
static bool IsSkippedFile(string path)
{
if (path.EndsWith(".g.cs", StringComparison.Ordinal))
{
return true;
}
if (path.EndsWith(".Designer.cs", StringComparison.Ordinal))
{
return true;
}
if (path.EndsWith(".AssemblyInfo.cs", StringComparison.Ordinal))
{
return true;
}
if (path.EndsWith(".AssemblyAttributes.cs", StringComparison.Ordinal))
{
return true;
}
if (path.EndsWith(".GlobalUsings.g.cs", StringComparison.Ordinal))
{
return true;
}
return false;
}
@github-actions github-actions Bot added the waiting-on-author PR state label label Jun 8, 2026
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

👋 @Evangelink — this PR has 5 unresolved review thread(s). When you're ready, please address the feedback and push an update; the triage bot will pick up the next state automatically. (Add the no-stale label to silence further pings.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-on-author PR state label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants