Skip to main content
Coverage reports live exactly where mehen’s source walk refuses to look: coverage/, target/, build/, and TestResults/ are gitignored in any healthy repository, and .nyc_output/ is hidden. Bare --coverage (or --coverage=auto) therefore runs a dedicated discovery pass with the inverse policy — every ignore rule off, hidden entries visible — while staying strictly bounded and deterministic. (A configured [coverage] discover = false opts the scan out even under the flag; configured reports still load.)
Discovery also runs without the flag when something asks for coverage: a coverage.* metric selector, a configured coverage.* threshold, or an opting-in [coverage] section in mehen.toml. --coverage=off disables it unconditionally; explicit paths (--coverage=lcov.info) skip discovery entirely.

Three input tiers

  1. Explicit reports--coverage=<path> (repeatable) or reports = […] under [coverage]. These are your statement of intent: a missing or unparsable explicit report is a hard error, because an explicit gate input that silently disappears is a broken CI gate.
  2. Tool-config introspection — mehen reads declarative tool configs that say where reports get written:
    • the c8/nyc JSON rc family (.c8rc, .c8rc.json, .nycrc, .nycrc.json — first found, in c8’s own precedence order): reports-dir/report-dir;
    • pyproject.toml: [tool.coverage.xml] output and [tool.coverage.lcov] output (coverage.py);
    • phpunit.xml / phpunit.xml.dist: <clover|cobertura outputFile="…"> and the legacy <log type="coverage-clover" target="…"> — PHPUnit writes no coverage file unless configured, so this is the only zero-config path for PHP;
    • tarpaulin.toml / .tarpaulin.toml (cargo-tarpaulin): the union of out formats and output-dir values across run profiles and the reserved [report] table. Tarpaulin’s file names are fixed (cobertura.xml, lcov.info), so introspection matters exactly when output-dir redirects them into territory the scan prunes (e.g. target/cov/).
    Executable configs — jest.config.ts, vitest.config.ts, .simplecov, Gradle DSLs, Pester scripts — are never executed and never regex-scraped. Their values are routinely computed (env vars, imported constants), so extraction would silently be wrong; their tools’ default output locations are already covered by the scan below.
  3. Artifact scan — well-known report names and locations, matched relative to each discovery root (the enclosing repository work dir, so reports at the repo root are found even when you analyze ./src): lcov.info, coverage.info, *.lcov, coverage.out / cover.out / coverage.txt / profile.cov / c.out / *.coverprofile, coverage-final.json, .nyc_output/*.json, jacoco.xml, jacocoTestReport.xml, site/jacoco/*.xml, reports/jacoco/**/*.xml, reports/kover/*.xml, coverage.xml, clover.xml, cobertura.xml, coverage.cobertura.xml. Every match is confirmed by content sniffing before it is believed.

Bounded by construction

Build directories are enormous, so the walk carries explicit bounds — each converts a pathological repository from “hangs” into “warns”: An extra-patterns entry whose first component names a pruned directory lifts that directory for the run — the escape hatch for exotic layouts:

Deterministic selection

When several candidates survive, selection is order-independent by construction:
  • Same directory, several formats — one Jest run writes lcov.info + coverage-final.json + clover.xml into coverage/; they describe the same test run, so only the highest-priority format is parsed and the rest are recorded as superseded.
  • TestResults/<run>/ re-runs — coverlet writes each dotnet test run into a fresh GUID directory; sibling runs holding the same report name keep only the newest (by mtime, lexicographic tie-break). This is the only place mtimes are trusted — a fresh CI clone stamps every file with clone time, so a global newest-wins rule would be meaningless.
  • Same file found twice (scanned and named by a tool config) — recorded once, attributed to the config.
Everything else merges (union + saturating-max — see supported formats), and discovery never fails a run: unreadable directories, malformed configs, and cap overruns degrade to warnings.

Staleness

A report generated before the code it describes attributes hits to the wrong lines. When a discovered report’s mtime predates the newest HEAD commit across the discovery roots, mehen warns (the report is still used — the heuristic has false positives around rebases and cherry-picks, and silently dropping your only report would be worse). Disable with stale-warning = false under [coverage].

Configuration reference

Flag/file precedence is per mode, not blanket: --coverage=off disables coverage regardless of configuration, and --coverage=<path> uses exactly the supplied reports — but bare --coverage/=auto honors a configured discover = false (it forces ingestion of configured reports, not the scan). Unknown keys and wrong types are rejected at load time with a caret into the TOML source, like every other mehen.toml mistake.

See also

References