# Check Flow Coverage

> Check which C source lines are not covered by Python flow tests. Use this when you want to ensure your C code changes are exercised by end-to-end Python tests. For Rust coverage, use /check-rust-coverage instead.

- Skill: `redisearch/check-flow-coverage` (Agent Skill)
- Install (CLI): `npx skillmds@latest add redisearch/check-flow-coverage`
- Raw SKILL.md: https://api.skillmd.com/api/skills/redisearch/check-flow-coverage/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: RediSearch (https://skillmd.com/u/redisearch)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/redisearch/check-flow-coverage

---


# Check Flow Coverage

Determine which C source lines in specified files are not covered by Python flow (end-to-end)
tests and report the results.

This skill uses gcov/lcov instrumentation and only works with C source files. For Rust
coverage, use the `/check-rust-coverage` skill instead.

## Arguments
- `<path>`: Path to a C source file.
- `<path 1> <path 2>`: Multiple C source file paths.

Paths can be relative to the repository root. E.g. `src/module.c` or `src/query.c`.

Arguments provided: `$ARGUMENTS`

## Instructions

If no arguments were provided (`$ARGUMENTS` is empty), stop and ask the user to provide one or
more source file paths. Example usage: `/check-flow-coverage src/module.c src/query.c`

If the user provides a Rust file path (e.g., under `src/redisearch_rs/`), stop and redirect
them to use `/check-rust-coverage` instead — this skill's gcov/lcov pipeline does not capture
Rust coverage data.

Follow these steps in order. Do NOT skip ahead — each step depends on the previous one.

### Step 1: Ensure a coverage build exists

Generate a unique marker path so parallel invocations don't collide. This marker is reused
in later steps as a freshness check, so it must always be created regardless of whether a
rebuild is needed:

```bash
COV_MARKER=$(mktemp /tmp/cov_start_marker.XXXXXX)
echo "Using marker: $COV_MARKER"
```

**Note:** Shell variables do not persist between tool calls. Capture the printed marker path
from this command's output and substitute it literally (e.g., `/tmp/cov_start_marker.a1b2c3`)
in all subsequent commands that reference `$COV_MARKER`.

Check if the coverage build output exists **and** was built with coverage instrumentation.
The build directory is platform-specific (`bin/linux-x64-debug-cov/`,
`bin/macos-aarch64-debug-cov/`, `bin/macos-x86_64-debug-cov/`, …) — use a glob so the
check works regardless of host:

```bash
ls bin/*-debug-cov/search-community/redisearch.so 2>/dev/null && \
find bin/*-debug-cov -name '*.gcno' -print -quit 2>/dev/null
```

Both checks must pass — the `.so` must exist AND `.gcno` files must be present (these are
generated by `--coverage` and are required for gcov to work). If either is missing, rebuild.
`build.sh` output can be very large (>60KB) and will get truncated, so save the full log to
a file and only show the tail:

```bash
./build.sh COV=1 FORCE 2>&1 | tee /tmp/build_cov.log | tail -20
```

This compiles C code with `--coverage` (gcov). The build flavor is `debug-cov` and artifacts
go under `bin/<platform>-debug-cov/`.

### Step 2: Run Python tests with coverage

**IMPORTANT**: Always run the **full** test suite for the coverage measurement.
Do NOT use a pre-existing `flow_standalone.info` — it may be stale or from a partial run,
which would cause you to report false coverage gaps. Do NOT use targeted tests (`TEST=...`)
in this step.

Reuse the `COV_MARKER` from Step 1 as the freshness marker.
(`COV=1` automatically resets gcov counters and captures the baseline via `prepare_coverage_capture`
before running tests, so no manual `lcov` prep is needed.)

Run all flow tests with coverage enabled. Pipe output through `tail` to avoid truncation.
Coverage is captured automatically after the run completes, even if some tests fail.
**The full test suite may take 10+ minutes — you MUST set a timeout of at least 600000ms (10 min)
on this command, otherwise it will be killed prematurely at the default 2-minute timeout.**

```bash
PYTEST_LOG=$(mktemp /tmp/pytest_cov.XXXXXX.log)
echo "Using log: $PYTEST_LOG"
# IMPORTANT: Use timeout of 600000ms for this command
./build.sh RUN_PYTEST COV=1 2>&1 | tee "$PYTEST_LOG" | tail -80
```

After the run, verify that the coverage file was produced **and** is newer than the start
marker. Substitute the literal marker path from Step 1:

```bash
if [ -f bin/flow_standalone.info ] && [ bin/flow_standalone.info -nt /tmp/cov_start_marker.XXXXXX ]; then
    echo "FRESH — coverage file updated successfully"
    ls -la bin/flow_standalone.info
else
    echo "STALE or MISSING — coverage capture failed"
    ls -la bin/flow_standalone.info 2>/dev/null || echo "File does not exist"
fi
rm -f /tmp/cov_start_marker.XXXXXX
```

If the file is stale or missing, the coverage build may not be instrumented correctly —
go back to Step 1 and rebuild with `COV=1 FORCE`.

### Step 3: Extract uncovered lines for target files

Parse the lcov info file to find coverage data for the target files:

```bash
python3 -c "
import sys, os

info_file = 'bin/flow_standalone.info'
if not os.path.exists(info_file):
    print(f'Error: {info_file} not found. Run Step 2 first to generate coverage data.')
    sys.exit(1)
target_files = sys.argv[1:]

repo_root = os.getcwd()
targets = []
for t in target_files:
    if os.path.isabs(t):
        targets.append(os.path.realpath(t))
    else:
        targets.append(os.path.realpath(os.path.join(repo_root, t)))

with open(info_file) as f:
    lines = f.readlines()

target_set = set(targets)
current_file = None
current_real = None
uncovered = {}
file_stats = {}

for line in lines:
    line = line.strip()
    if line.startswith('SF:'):
        current_file = line[3:]
        current_real = os.path.realpath(current_file)
    elif line.startswith('DA:') and current_real in target_set:
        parts = line[3:].split(',')
        line_no = int(parts[0])
        hit_count = int(parts[1])
        target = current_real
        if target not in uncovered:
            uncovered[target] = []
            file_stats[target] = [0, 0]
        file_stats[target][1] += 1
        if hit_count == 0:
            uncovered[target].append(line_no)
        else:
            file_stats[target][0] += 1

for target in targets:
    if target in uncovered:
        covered, total = file_stats[target]
        pct = (covered / total * 100) if total > 0 else 0
        print(f'\n=== {target} ({covered}/{total} lines covered, {pct:.1f}%) ===')
        if uncovered[target]:
            nums = sorted(uncovered[target])
            ranges = []
            start = nums[0]
            end = nums[0]
            for n in nums[1:]:
                if n == end + 1:
                    end = n
                else:
                    ranges.append((start, end))
                    start = n
                    end = n
            ranges.append((start, end))
            for s, e in ranges:
                if s == e:
                    print(f'  Line {s}')
                else:
                    print(f'  Lines {s}-{e}')
        else:
            print('  Fully covered!')
    else:
        print(f'\n=== {target} ===')
        print('  No coverage data found. Ensure the file is compiled into the module.')
" $ARGUMENTS
```

### Step 4: Read the uncovered lines

Read the source code at the uncovered line ranges to understand what code paths are not
exercised. For files with many scattered uncovered ranges, read large contiguous sections
(500-800 lines at a time) rather than making dozens of small reads per range — this is
faster and gives better context for understanding code flow across functions. Files over
~1500 lines will need multiple reads due to tool limits; read in 2-4 large chunks covering
all the uncovered ranges.

### Step 5: Classify and report coverage gaps

Based on the uncovered code paths, classify each gap into one of these categories:

1. **Non-testable gaps** — silently discard these without presenting them to the user:
   - **Disk-only**: Code behind `diskSpec`/`SearchDisk` checks — requires disk indexing enabled.
   - **API-only**: Code reachable only through the C module API, not through any `FT.*`
     command syntax. Tested by C++ LLAPI unit tests instead.
   - **Unreachable defensive code**: Dead code, abort branches, allocation-failure guards,
     exhaustive switch default/fallthrough arms that exist only for compiler completeness.

2. **Testable gaps** — code paths reachable via standard Redis commands
   (`FT.CREATE`, `FT.SEARCH`, `FT.AGGREGATE`, `FT.EXPLAIN`, etc.) in Python flow tests.

**Present the testable coverage gaps to the user** as a structured report, grouped by
feature or logical area. Many uncovered lines belong to the same feature spread across
multiple functions (e.g., creation, evaluation, freeing, and debug-dump of a single query
node type). Grouping them helps the user see the full picture.

For each group, include:
- Feature/area name (e.g., "Geometry queries", "Tag prefix edge cases")
- All related line ranges under that feature, with brief descriptions
- Which test file would be the natural home for new tests

This skill is **analysis only** — do not write tests or modify source code. If the user
wants to improve coverage, direct them to `/improve-flow-coverage`.

## Notes

- The coverage build uses `debug-cov` flavor with gcov instrumentation for C code.
- LTO is disabled in coverage builds.
- Coverage data only tracks C source code under `src/` and `deps/thpool/`. Test files under `tests/` are excluded.
- This skill does **not** capture Rust coverage. For Rust coverage, use the `/check-rust-coverage` skill.
- Always pipe `build.sh` output through `tee` to a file and then `tail` — the raw output can exceed 60KB and will be truncated, but the full log is useful for debugging failures.


