20.2 coverage — Code Coverage

**Measure which lines of your code are actually exercised by shadow tests.**

NanoLang Mascot

Coverage in NanoLang is a build-system-level tool rather than a NanoLang module — there is no import statement required. When you compile with coverage instrumentation enabled, the NanoLang compiler generates C code annotated with gcov/llvm-cov counters. After running the shadow tests, you read the resulting coverage data to see which lines were hit.

How Coverage Works in NanoLang

The compilation pipeline is:


.nano source
    ↓  nanoc (with --coverage flag, if supported)
.c source (with __gcov_* instrumentation)
    ↓  clang/gcc (with -fprofile-arcs -ftest-coverage)
.o + .gcno files
    ↓  run the binary (shadow tests execute here)
.gcda files
    ↓  gcov or llvm-cov report
coverage report

Because shadow tests run at compile time (before your program's main executes), the instrumented binary is run as part of the build, and coverage data is collected automatically.

Running Tests with Coverage

Coverage is typically enabled through the project Makefile. A common workflow:


# Clean previous coverage data
make clean-coverage

# Build and run shadow tests with coverage instrumentation
make test-coverage

# Generate the HTML report
make coverage-report

# Open the report
open coverage/index.html

If your project does not yet have coverage targets, you can add them manually. The essential compiler flags are:


# Compile with coverage
clang -fprofile-arcs -ftest-coverage -o myprogram myprogram.c

# Run the program (shadow tests fire automatically)
./myprogram

# Generate gcov report
gcov myprogram.c

# Or use llvm-cov for prettier output
llvm-cov gcov myprogram.c

Reading the Coverage Report

A gcov report for a NanoLang file looks like this:


        -:    0:Source:myfile.nano.c
        -:    1:/* Generated by nanoc */
        -:    2:#include "nanolang_runtime.h"
       100:    5:int nl_clamp(int value, int lo, int hi) {
       100:    6:    if (value < lo) return lo;
        42:    7:    if (value > hi) return hi;
       100:    8:    return value;
        -:    9:}
    #####:   11:int nl_unused_function(int x) {
    #####:   12:    return x * 2;
        -:   13:}

The left column shows:

  • - — non-executable line (comments, braces, declarations)
  • A **count** — the line was executed that many times
  • ##### — the line was **never executed** (this is what you want to eliminate)

What Good Coverage Looks Like

For a module with thorough shadow tests you should expect:

  • **Function coverage:** 100% — every function should have a shadow test
  • **Line coverage:** 90–100% — most lines should be executed by at least one test
  • **Branch coverage:** 80–100% — both sides of if/else should be tested

The lines most likely to be uncovered are:

  • Error handling paths (e.g. "this should never happen" branches)
  • Defensive else branches required by NanoLang syntax but never taken
  • Code that is only reachable from main or other I/O-driven paths

Improving Coverage

If coverage shows uncovered lines, the fix is usually to add more assertions to the relevant shadow test:

**Before (low coverage):**


fn grade(score: int) -> string {
    if (>= score 90) { return "A" }
    if (>= score 80) { return "B" }
    if (>= score 70) { return "C" }
    return "F"
}

shadow grade {
    assert (== (grade 95) "A")   # Only tests the "A" path
}

**After (full coverage):**


shadow grade {
    assert (== (grade 95) "A")
    assert (== (grade 85) "B")
    assert (== (grade 75) "C")
    assert (== (grade 60) "F")
    assert (== (grade 90) "A")   # boundary
    assert (== (grade 80) "B")   # boundary
    assert (== (grade 70) "C")   # boundary
}

Coverage and the Required else Branch

NanoLang requires else clauses in certain positions. Sometimes the else branch is a semantic no-op:


if (some_condition) {
    (do_something)
} else {
    (print "")   # required no-op
}

Coverage tools will report this (print "") as uncovered if some_condition is always true in your tests. This is acceptable — document it with a comment so reviewers understand it is intentional:


} else {
    (print "")   # required no-op: condition always true in test scenarios
}

Branch Coverage vs Line Coverage

Line coverage tells you whether a line was reached. Branch coverage tells you whether both the true and false branches of every condition were taken. In NanoLang, achieving full branch coverage typically requires:

1. Testing every if condition with an input that makes it true

2. Testing every if condition with an input that makes it false

3. Testing while loops with zero iterations (loop never entered) and at least one iteration


fn sum_positive(arr: array<int>) -> int {
    let mut total: int = 0
    let mut i: int = 0
    while (< i (array_length arr)) {
        let v: int = (array_get arr i)
        if (> v 0) {
            set total (+ total v)
        } else {
            (print "")   # skip negatives
        }
        set i (+ i 1)
    }
    return total
}

shadow sum_positive {
    # Empty array: while loop never entered
    assert (== (sum_positive []) 0)

    # Array with only positives: else branch never taken
    assert (== (sum_positive [1, 2, 3]) 6)

    # Array with mixed: both branches taken
    assert (== (sum_positive [1, -2, 3, -4]) 4)

    # Array with only negatives: if branch never taken
    assert (== (sum_positive [-1, -2]) 0)
}

Coverage with Property Tests

Property tests (from the proptest module) also contribute to coverage. When proptest generates 100 random inputs and runs your property function, every executed line in the called functions is counted. This means a well-written property test can drive up branch coverage significantly without requiring you to enumerate all branches manually.


from "modules/proptest/proptest.nano" import forall_int, int_range, prop_pass,
                                             report_passed, PropertyReport

fn prop_sum_positive_nonneg(seed: int) -> string {
    let arr: array<int> = [seed, (- 0 seed), (+ seed 1)]
    let result: int = (sum_positive arr)
    if (>= result 0) {
        return (prop_pass)
    } else {
        return (prop_fail "negative sum")
    }
}

shadow prop_sum_positive_nonneg {
    let report: PropertyReport = (forall_int "sum_positive_nonneg"
                                              (int_range -50 50)
                                              prop_sum_positive_nonneg)
    assert (report_passed report)
}

Coverage Gaps to Investigate

Some coverage gaps are worth investigating rather than ignoring:

  • **An entire function is uncovered** — its shadow test is missing or broken
  • **A large block is uncovered** — there may be a logic error making the block unreachable
  • **Only one branch of a critical condition is tested** — the other branch may contain a bug

Some gaps are acceptable:

  • No-op else branches required by the language
  • main function bodies (often not meaningful to unit-test)
  • Logging and diagnostic output paths

---

**Previous:** 20.1 proptest

**Next:** 20.3 Best Practices