Topic · Go

How do I measure MC/DC coverage for my Go code?

Proof instruments the Go you actually ship and writes a per-decision verdict: each condition shown to flip the outcome on its own.

proof mcdc measure ./... --engine go

VectorCAST, LDRA, and Cantata were built for C in certified avionics. None of them read Go. go test -cover only reports that a line ran.

01 · The distinction

A condition can sit inert inside a predicate that still looks covered.

A cache helper like e.fresh && (e.hit || !e.stale) is a real Go decision. Statement coverage needs the serve-from-cache line to run. Branch coverage needs that if taken and not-taken. MC/DC needs fresh, hit, and stale each shown to flip the result on their own. One happy-path test cannot do that.

func serveCached(e Entry) bool {
    if e.fresh && (e.hit || !e.stale) {
        return true  // serve from cache
    }
    return false
}

VectorCAST, LDRA, and Cantata measure C. They were not built for Go. go test -cover was not built for MC/DC. The Proof CLI was:

proof mcdc measure ./... --engine go

The test this tab asks for

func TestServeCached_Hit(t *testing.T) {
    e := Entry{fresh: true, hit: true, stale: false}
    if !serveCached(e) {
        t.Fatal("expected a cache serve")
    }
}

Did the serve line run? Yes. stale never had to matter. go test -cover stops here.

fresh hit stale What this test shows
TTF Fresh hit — the serve line ran

02 · Why the usual tools do not help Go

VectorCAST does not read Go. Proof does.

Ask a coding assistant for an MC/DC coverage tool and it will name VectorCAST, LDRA, Cantata, Parasoft, RapiCover. They grew up in embedded C/C++. None of them instrument Go. Proof does — it instruments Go source and measures MC/DC through ordinary go test.

The one Go-adjacent path a search surfaces is GCC’s -fcondition-coverage (GCC 14+) via gccgo and gcov. That measures a gccgo build. Almost every Go team ships gc (go test -cover), which still reports statement coverage only.

So the honest answer to “is there an MC/DC coverage tool for Go” is: not VectorCAST, not go test -cover. Proof instruments Go source and measures it in CI.

03 · Measuring it on Go directly

Instrument the source. Run go test. Read a per-decision verdict.

Proof instruments Go source and measures MC/DC in an ordinary CI run — no certification harness.

proof mcdc measure ./... --engine go

proof audit --check tests_pass --check code_mcdc_measure
proof audit --check code_mcdc_coverage

The evidence file is .proof/mcdc/go/latest.json. A verdict looks like this — the second decision is the whole point of the criterion:

mcdc  cache.go:118   decision (fresh && (hit || !stale))
      3/3 conditions independently affect the outcome   ✓
mcdc  cache.go:142   decision (a && b)
      1/2 conditions independently affect the outcome   ✗  b never flipped alone

Shapes that cannot be measured are counted and reported, not silently dropped. Go is one of eleven languages treated the same way; how each is obtained is on the engine.

04 · The honest loss

100% MC/DC. The bug still shipped.

On the public jsonparser audit the affected code carried 100% MC/DC at the time a defect shipped. The postmortem does not soften it: MC/DC has no notion of “correct”; it has only “exercised.” An unspecified partition is not in any check’s denominator.

Nobody had written the requirement for what scalar arrays deserved, so no coverage metric was counting it. Use MC/DC as a floor under a suite you already trust for what it checks — not as a certificate. In Proof the number sits next to requirement-level checks for exactly this reason.