How Code Review Graph Detects Test Gaps: A Technical Deep Dive

Code Review Graph detects test gaps by checking for the absence of TESTED_BY edges linking changed production functions to test cases in the repository graph.

Code Review Graph (CRG) is an open-source tool that automates code review analysis by building a graph representation of your codebase. When analyzing pull requests, the system identifies untested changes by examining the relationships between production code and test cases stored in the graph structure.

The Core Detection Algorithm

The test gap detection logic operates during the change analysis phase after parsing a PR diff. CRG builds a list of changed functions and classes, then validates whether each production node maintains a testing relationship in the graph.

Filtering Production Code

The algorithm first isolates production code by skipping nodes marked as tests. Any node where node.is_test evaluates to True is excluded from gap detection, ensuring the analysis focuses only on implementation code that requires test coverage.

Checking for TESTED_BY Relationships

For each production function in the changed set, CRG queries the graph store for outgoing edges. The system calls store.get_edges_by_source(node.qualified_name) to retrieve all relationships originating from the function. If none of these edges have kind == "TESTED_BY", the function is flagged as untested and added to the test_gaps collection.

Implementation in code_review_graph/changes.py

The core detection logic resides in the analyze_changes() function within code_review_graph/changes.py (lines 68-78). This routine iterates through changed functions and validates their testing relationships:


# Detect test gaps: changed functions without TESTED_BY edges.

test_gaps: list[dict[str, Any]] = []
for node in changed_funcs:
    if node.is_test:
        continue
    # TESTED_BY edges are stored as source=production, target=test by the parser

    tested = store.get_edges_by_source(node.qualified_name)
    if not any(e.kind == "TESTED_BY" for e in tested):
        test_gaps.append({
            "name": _sanitize_name(node.name),
            "qualified_name": _sanitize_name(node.qualified_name),
            "file": node.file_path,
            "line_start": node.line_start,
            "line_end": node.line_end,
        })

The TESTED_BY edges are stored with the production code as the source and the test case as the target, allowing the system to trace which tests verify specific implementation functions.

Processing and Reporting Test Gaps

After collecting untested functions, CRG deduplicates entries for human-readable summaries. The processing logic (lines 97-115 in changes.py) limits the display to five unique function names to prevent overwhelming reviewers:

if test_gaps:
    # Dedup by bare name in the human summary.

    seen_names: set[str] = set()
    gap_names: list[str] = []
    for g in test_gaps:
        n = g["name"]
        if n in seen_names:
            continue
        seen_names.add(n)
        gap_names.append(n)
        if len(gap_names) >= 5:
            break
    summary_parts.append(f"  - Untested: {', '.join(gap_names)}")

This deduplication ensures that overloaded functions or multiple changes to the same function appear only once in the review summary.

Verification Through Unit Tests

The detection mechanism is validated in tests/test_changes.py (lines 70-88), which creates functions with and without TESTED_BY edges to verify accurate gap identification:

def test_analyze_detects_test_gaps(self):
    """Changed functions without TESTED_BY edges are flagged as test gaps."""
    self._add_func("untested_a", path="app.py", line_start=1, line_end=10)
    self._add_func("untested_b", path="app.py", line_start=15, line_end=25)
    self._add_func("tested_c", path="app.py", line_start=30, line_end=40)

    # Only tested_c has a test.

    self._add_func("test_c", path="test_app.py", is_test=True)
    self._add_tested_by("app.py::tested_c", "test_app.py::test_c", "test_app.py")

    result = analyze_changes(
        self.store,
        changed_files=["app.py"],
        changed_ranges={"app.py": [(1, 40)]},
    )
    gap_names = {g["name"] for g in result["test_gaps"]}
    assert "untested_a" in gap_names
    assert "untested_b" in gap_names
    assert "tested_c" not in gap_names

This test confirms that only functions lacking TESTED_BY relationships appear in the test_gaps list, while properly tested code is excluded from the results.

Practical Usage Examples

Detecting Gaps in a Pull Request

To programmatically detect test gaps for a specific set of changes, import analyze_changes and provide the changed file paths and line ranges extracted from the diff:

from code_review_graph.changes import analyze_changes

# Assume `store` is an initialized GraphStore with the repository graph loaded.

result = analyze_changes(
    store,
    changed_files=["src/app.py"],
    changed_ranges={"src/app.py": [(1, 120)]},  # line ranges from the diff

)

print("Test gaps found:")
for gap in result["test_gaps"]:
    print(f"- {gap['name']} ({gap['file']}:{gap['line_start']}-{gap['line_end']})")

Generating PR Comments

The scripts/render_pr_comment.py utility automates the creation of human-friendly PR comments that include test gap summaries:

python -m code_review_graph.scripts.render_pr_comment \
    --repo-path /path/to/repo \
    --base main \
    --head feature-branch

This script internally invokes analyze_changes, extracts the test_gaps list, and formats output such as:


🔍 2 test gap(s) – Untested: handle_user_input, save_to_db

Custom Filtering by Code Size

You can extend the basic detection to focus on significant untested changes by filtering gaps based on function size:

large_gaps = [
    g for g in result["test_gaps"]
    if (g["line_end"] - g["line_start"]) > 20
]

for g in large_gaps:
    print(f"{g['name']} spans {g['line_end'] - g['line_start'] + 1} lines")

This approach helps prioritize testing efforts for substantial untested logic blocks.

Summary

  • Test gaps are defined by missing TESTED_BY edges in the code graph between production functions and test cases.
  • Detection occurs in code_review_graph/changes.py through the analyze_changes() function, which filters test nodes and validates edge relationships.
  • The algorithm skips test code by checking the node.is_test property before evaluating coverage.
  • Results are deduplicated to five unique names for readable PR summaries while preserving full gap data in the report.
  • Unit tests in tests/test_changes.py verify that only untested functions appear in gap detection results.

Frequently Asked Questions

What constitutes a test gap in Code Review Graph?

A test gap is any changed production function or class that lacks a TESTED_BY edge connecting it to a test case in the repository graph. According to the source code in code_review_graph/changes.py, if store.get_edges_by_source(node.qualified_name) returns no edges with kind == "TESTED_BY", the node is classified as untested and added to the test_gaps list.

How does Code Review Graph distinguish between test and production code?

CRG uses the is_test boolean property on graph nodes to differentiate test files from production code. During gap detection, the algorithm explicitly checks if node.is_test: continue to skip nodes that represent test functions, ensuring analysis focuses only on implementation code that requires coverage validation.

Can Code Review Graph detect test gaps across multiple files?

Yes, the analyze_changes() function accepts a list of changed_files and a dictionary of changed_ranges mapping file paths to line number tuples. The system processes all provided files in a single analysis pass, collecting test gaps from across the entire changeset rather than examining files in isolation.

Where is the test gap detection logic located in the source code?

The primary detection logic resides in code_review_graph/changes.py between lines 68-78, within the analyze_changes() function. Additional related functionality, including the deduplication logic for gap names, appears at lines 97-115 in the same file. The test suite validating this behavior is located in tests/test_changes.py at lines 70-88.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →