mcpField
F
find-untested-sources-polyglot

find-untested-sources-polyglot

github.com/dotnetTesting & quality5.8k37 installsUpdated 1d ago
Repository

About find-untested-sources-polyglot

A static test-gap analysis skill for polyglot repositories. It parses source and test files to identify likely untested sources without building or running tests, then provides a prioritized worklist for adding tests.

Key features

  1. 01Scan nine programming languages
  2. 02Parse source files with tree-sitter
  3. 03Pair tests by imports and identifiers
  4. 04List untested files and suggested paths
  5. 05Filter languages and result count

Use cases

  1. 01Choose files for new tests
  2. 02Prepare a gap list before test generation
  3. 03Check whether new tests pair with sources
  4. 04Quickly screen a polyglot repository

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/dotnet/skills --skill find-untested-sources-polyglot

After installing, type/find-untested-sources-polyglotin Claude Code to confirm it loaded.

Bundled filesscripts/find_untested_sources.py

Details

From the project README

Find Untested Sources (Polyglot)

Purpose

Coverage tools answer "which lines were executed?" — they require a green build and a passing test run, which is minutes-to-tens-of-minutes on a real repo. The question this skill answers is different and much cheaper:

Which source files have no test file referencing any of their declared symbols?

That's the question an agent asks before writing a new test — and it can be answered statically in a few seconds by parsing every recognized source file with tree-sitter, with no build, no dependency resolution, no compilation.

This is the polyglot sibling of the C# find-untested-sources skill. The output schema is intentionally compatible so the same prompt patterns can consume either tool.

When to Use

  • The repository is not exclusively C#, or you want a tool that works uniformly across multiple languages without per-language plumbing.
  • User asks "where should I add tests?", "which files have no tests?", "find untested code", "give me a test gap list", "what's the next file to test".
  • Before invoking a test-generation agent, to produce a prioritized worklist.
  • After generating tests, to verify each new test file pairs to a source file.

When Not to Use

  • C#-only repo — prefer find-untested-sources. Its Roslyn-based namespace disambiguation is strictly better than this skill's identifier overlap on duplicated short names like Settings or Context.
  • Line/branch coverage — use language-native coverage tooling.
  • Are existing tests strong? — use test-gap-analysis or assertion-quality.

Inputs

InputRequiredDefaultDescription
Repo rootYes—Directory to scan recursively.
--lang LANGNoallRestrict to a language (repeatable). One of python, typescript, tsx, javascript, go, java, rust, csharp, ruby.
--limit-untested NNo0 (no limit)Truncate the untested list to N entries.
--include-testedNooffInclude tested_sources in the payload (large).
Prerequisites
  • Python 3.10+.
  • pip install tree-sitter-language-pack (single self-contained wheel that bundles parsers for 300+ languages and the high-level process() API used here). No native build, no per-language grammar install.

Usage

# From the skill folder
python scripts/find_untested_sources.py <repo-root>

# Restrict to a language
python scripts/find_untested_sources.py <repo-root> --lang python --lang typescript

# Truncate the report (top 20 by declared API surface)
python scripts/find_untested_sources.py <repo-root> --limit-untested 20 > pairing.json

# Iterate, highest-API-surface first
$report = Get-Content pairing.json | ConvertFrom-Json
$report.untested_sources | Select-Object -First 10 path, declaration_count, suggested_test_path

Diagnostics go to stderr; JSON goes to stdout.

Output Schema

{
  "repo_root": "<absolute path>",
  "summary": {
    "source_files": 3138,
    "test_files": 761,
    "tested_source_files": 1419,
    "untested_source_files": 1719,
    "orphan_test_files": 15,
    "languages": ["csharp"]
  },
  "untested_sources": [
    {
      "path": "src/Foo/Bar.cs",
      "language": "csharp",
      "declaration_count": 8,
      "declarations": ["Bar", "BarOptions", "IBar", "..."],
      "suggested_test_path": "src/Foo/BarTests.cs"
    }
  ],
  "orphan_tests": [
    { "path": "tests/SomeIntegrationTest.cs", "language": "csharp" }
  ]
}

Pass --include-tested to additionally emit tested_sources (same shape as untested_sources but with a covering_tests array instead of a suggested path). Omitted by default to keep the payload small for LLM consumption.

How It Works

  1. File discovery — recursive directory walk pruning common build/vendor dirs (bin, obj, node_modules, target, dist, build, vendor, __pycache__, .venv, .git, etc.). Skips generated files (.d.ts, .g.cs, .Designer.cs, _pb2.py, *.min.js, AssemblyInfo.cs, ...).

  2. Language detection — tree_sitter_language_pack.detect_language_from_path maps the extension to one of the supported languages. Unknown extensions are skipped silently.

  3. Test-vs-source classification — per-language path heuristics:

    LanguageTest rule
    Pythonpath contains tests/ or test/; or filename starts with test_ or ends with _test.py; or conftest.py.
    JS/TS/TSXpath contains __tests__, tests, test, spec, or e2e; or filename contains .test. or .spec..
    Gofilename ends with _test.go (Go's standard convention).
    Javapath contains test or tests; or filename ends with Test.java / Tests.java.
    Rustpath contains tests/ or benches/.
    C#path contains tests/; or project segment ends with .Tests, .Test, .UnitTests, .IntegrationTests; or filename ends with Tests/Test.
    Rubypath contains spec/, test/; or filename ends with _spec.rb / _test.rb.
  4. Per-file extraction — tree_sitter_language_pack.process(text, ProcessConfig(language=lang, structure=True, imports=True, symbols=True)) returns:

    • structure — top-level declared items (functions, classes, methods, traits, ...) with their names. Used as the declared-symbol set.
    • imports — raw import statements (e.g. from foo import bar, import "pkg/util", using System.IO;, use crate::foo::Bar;).
    • symbols — flat declared-name list, unioned with structure (acts as a fallback when structure is empty, and broadens coverage when both are populated; declaration counts may exceed pure structure parsing).
  5. Pairing — for each test file, union the results of:

    • Import resolution (per language):
      • Python: from pkg.mod import x → pkg/mod.py or pkg/mod/__init__.py.
      • TS/JS: relative ./foo / ../bar → with .ts/.tsx/.js/.jsx and /index.* candidate paths.
      • Go: "path/to/pkg" → any source file whose final path segment matches pkg.go in the index.
      • Java: import a.b.C; → a/b/C.java.
      • Rust: use a::b::C; → b.rs or C.rs (best-effort, no module tree).
      • Ruby: require 'foo/bar' → foo/bar.rb.
      • C#: using maps to namespaces, not files; intentionally a no-op — falls through to identifier overlap below.
    • Identifier overlap — every word-like token in the test source is looked up in the source index of declared names (length ≥ 4 to keep noise down). Any source whose declared name appears as a token in the same-language test is paired.
  6. JSON emit — untested_sources is ordered by declaration count descending so the highest-API-surface gap appears first.

Limitations

This is a static, parse-only heuristic. It deliberately trades a small amount of accuracy for orders-of-magnitude lower cost than coverage. Known gaps:

  • Reflection / DI-resolved types that a test only references through a string name or container resolution don't appear in the identifier scan.
  • C# specifically: namespace disambiguation is the C# tool's strength; this polyglot version intentionally skips it. If you have a .NET-only repository, prefer the Roslyn-based find-untested-sources.
  • Short identifier names (< 4 chars) are dropped from the overlap index to avoid noisy pairings on names like id, db, Tag.
  • Cross-language tests (Python tests driving a Go binary, etc.) are recorded as orphan tests since same-language pairing is the rule.
  • Monorepo path aliases (TS path mapping, Java module-info) are not resolved; the suffix-match fallback may pick the wrong source if two files share a trailing path segment in different sub-projects.

For these cases, run actual coverage on the unpaired candidates the agent has already triaged.

Outputs the agent should consume

  • untested_sources[*].path — pick the next source file to test (highest declaration_count first).
  • untested_sources[*].suggested_test_path — drop-in target for the new test file using the per-language convention.
  • (With --include-tested) tested_sources[*].covering_tests — verify a newly written test file lands in the list for the intended source.
  • orphan_tests — tests that don't appear to reference any same-language source file; useful for triaging stale tests or integration-only tests.

Comments

Markdown supported