Code Coverage in EK9

EK9 automatically collects code coverage data during test execution. Coverage is always enforced - if your code falls below the 80% threshold, the test runner returns exit code 12 regardless of whether tests pass.

The -t6 quality report: the Quality at a Glance band, complexity budget tracks, and the coverage detail beneath

Threshold Enforcement

Unlike other test runners that treat coverage as optional, EK9 treats coverage as a first-class quality gate. Coverage probes are inserted at compile time at all control flow points:

Coverage is always collected and enforced when running tests with -t. If coverage falls below the 80% threshold, the test runner returns exit code 12 regardless of whether a coverage report was requested.

$ ek9 -t main.ek9
[OK] PASS myapp.tests::AllTests [Assert] (5ms)

Summary: 1 passed, 0 failed (1 total)

❌ E83001: Code coverage 44.0% is below required threshold 80%
   Methods:  46.0% (23/50)
   Lines:    44.0% (37/84)
   Branches: 41.2% (14/34)

   Uncovered items:
     27 method(s)
     20 branch(es)

   Use -t4 or -t5 for detailed coverage report.

$ echo $?
12

This ensures CI/CD pipelines catch insufficient coverage even when developers don't explicitly request coverage reports. Tests passing is not enough - code must be tested.

Coverage Output Formats

Use -tC to display coverage results. The format follows the test output format:

ek9 -t -tC main.ek9         # Human-readable coverage summary
ek9 -t0 -tC main.ek9        # Terse coverage: Coverage: 87.5% (7/8)
ek9 -t2 -tC main.ek9        # JSON output to files
ek9 -t3 -tC main.ek9        # JUnit XML + JaCoCo XML to files

File Output for Machine Formats

When using machine formats (-t2 or -t3) with -tC, results are written to files in the .ek9/ directory:

Flags Test Results Coverage Results
-t2 -tC .ek9/test-results.json .ek9/coverage.json
-t3 -tC .ek9/test-results.xml (JUnit) .ek9/coverage.xml (JaCoCo)

A human-readable summary with file paths is printed to stdout:

Summary: 3 passed, 0 failed (3 total)
Types: 3 assert
Duration: 13ms

Test results written to: .ek9/test-results.json
Coverage results written to: .ek9/coverage.json

The JaCoCo XML format (.ek9/coverage.xml) is compatible with coverage tools like SonarQube, Codecov, and Coveralls.

Coverage JSON Format

{
  "coverage": {
    "methods": { "percentage": 100.00, "covered": 3, "total": 3 },
    "lines": { "percentage": 100.00, "covered": 5, "total": 5 },
    "branches": { "percentage": 100.00, "covered": 5, "total": 5 },
    "overall": 100.00,
    "probesHit": 5,
    "probesTotal": 5,
    "modules": {
      "my.module.tests": { "coverage": 100.00, "hit": 3, "total": 3 },
      "my.module": { "coverage": 100.00, "hit": 2, "total": 2 }
    }
  }
}

JaCoCo XML Format

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE report PUBLIC "-//JACOCO//DTD Report 1.1//EN" "report.dtd">
<report name="EK9 Coverage">
  <counter type="METHOD" missed="0" covered="3"/>
  <counter type="LINE" missed="0" covered="5"/>
  <counter type="BRANCH" missed="0" covered="5"/>
  <package name="my/module/tests">
    <counter type="BRANCH" missed="0" covered="3"/>
  </package>
</report>

Verbose Coverage (-t4, -t5)

For detailed analysis of what's covered and what's not:

ek9 -t4 main.ek9       # Verbose human coverage with item lists
ek9 -t5 main.ek9       # Same as -t4, plus writes coverage-verbose.json

The verbose format shows per-module breakdowns with specific items:

Coverage Report
==============

Overall Coverage: 75.0% (6/8 probes hit)

Module: myapp.core (100.0% - 4/4)
  Methods (2/2):
    [HIT] myapp.core::add
    [HIT] myapp.core::multiply
  Branches (2/2):
    [HIT] myapp.core::add:12 (IF_TRUE)
    [HIT] myapp.core::add:12 (IF_FALSE)

Module: myapp.utils (50.0% - 2/4)
  Methods (1/2):
    [HIT] myapp.utils::formatNumber
    [MISS] myapp.utils::parseNumber
  Branches (1/2):
    [HIT] myapp.utils::formatNumber:8 (IF_TRUE)
    [MISS] myapp.utils::formatNumber:8 (IF_FALSE)

With -t5, a detailed JSON file is written to .ek9/coverage-verbose.json containing the same information in machine-readable format. See JSON Output Schema for the complete field reference.

Code Quality Metrics

EK9's coverage system goes beyond simple line and branch counting. Every function is analyzed for code quality metrics that help identify complex, hard-to-maintain code.

For AI Assistants: See Metrics Reference for complete threshold tables, recommended actions, and workflow templates for iterative quality improvement.

Available Metrics

Metric Range Description
Cyclomatic Complexity (CC) 1-50+ Number of linearly independent paths through code. Each if, switch case, for, while, and exception handler adds 1. Target: <10 per function.
Cognitive Complexity 0-50+ Mental effort required to understand code. Penalizes nesting, breaks in linear flow, and boolean complexity. Better reflects human comprehension than CC. Target: <15.
Nesting Depth 0-10+ Maximum depth of nested control structures. Deep nesting indicates code that should be refactored. Target: <4.
Statement Count 0-100+ Number of executable statements. Large functions are harder to test and maintain. Target: <20 per function.
ARI Readability 1-12+ Automated Readability Index measuring lexical complexity (identifier length and statement density). Lower = simpler to read. File-level metric.

Compile-Time Enforcement: Many of these metrics are enforced at compile-time with hard limits for BOTH production and test code. See Code Quality for complete documentation of thresholds, error codes, and the rationale behind each limit.

Where Metrics Appear

Quality metrics are integrated throughout the coverage reports:

Complexity Badges in Source Views

Each function in source views displays a complexity badge:

isNumeric()                                      CC:6
  -> text as String
  <- result as Boolean: false
  ...

Badge colors indicate severity:

Hover over badges to see full metrics: CC:6 Cog:1 Nest:1

Readability (ARI) Scores

The Automated Readability Index measures lexical complexity - how easy it is to read the words in your code (not understand the logic). Lower scores indicate simpler identifier names and statement structure.

ARI is shown per-file on module detail pages:

📖 Lexical Complexity
   formatters.ek9      6
   stringHelpers.ek9   6
   validators.ek9      3

Score interpretation:

Note: ARI is an informational metric with no compile-time enforcement. Domain-specific terminology often requires longer identifiers. See ARI Readability for complete documentation and domain considerations.

Input Variety

Coverage answers one question: did this line run? It cannot tell you whether the line ran with input that mattered, or whether anything checked the result. A project can sit at 100% coverage while no test asserts anything at all — and coverage will report that as a perfect score.

Input variety measures the other two questions. For every construct the compiler works out a floor: the value-classes its parameters can meaningfully take. Then the test run records which of those were actually reached, and which had their result checked.

The floor is 1-wise: each value-class needs to be driven at least once, not every combination of them. A function taking a three-member enumeration and a Boolean has a floor of seven (4 + 3), not twelve — so saturating variety is achievable rather than combinatorially hopeless.

Value-classes and why the parameter type matters

How well a construct can be measured depends entirely on what its parameter types say. An enumeration declares its own value-classes: its members, plus unset. Boolean has exactly three — true, false, unset. These are known exactly.

A String is different: its meaningful values have to be guessed at, because the type does not say what they are. Constructs taking one are reported separately as approximate rather than folded in with measured ones — and that is a work list, not an apology. Replacing a bare String parameter with an enumeration or a constrained type moves the construct into the measured group and makes the type carry the meaning, which is what EK9 asks for everywhere else.

The verification target: a complete green outer ring for 100% coverage, a half-length red ring inside it for 49.8% driven, and an empty innermost ring for 0% asserted

The three rings on a project at 100% coverage that asserts nothing: the outer ring complete, the middle ring half-filled, and the centre empty. Every ring answers for itself on hover, so the counts behind a segment are readable without leaving the chart.

A wide gap between driven and asserted is the signal to look for: it means code is being exercised as scaffolding for other tests, with nothing verifying what it did.

Change Risk (HEX)

HEX fuses the metrics above into one question: how costly is a change here likely to be? It is the product of two factors:

Both matter, and neither is sufficient alone. Complex code that is thoroughly verified is not especially risky to change; trivial code that nothing checks is not especially risky either. Risk is where the two meet.

HEX never gates a build. Coverage is the enforced threshold; HEX is an indicator, for a human or an AI deciding where to be careful. It exists to counter the idea that a coverage percentage on its own says whether code is safe to change.

Reading the score

A score of 0–100 with bands at 25 and 50 (low / moderate / high). Read the typical score and the worst single construct together: a calm average over a dangerous outlier means the area is fine but that one construct will bite, and the average will reassure you about it wrongly. A worst case close to the average means the weakness is systemic, and fixing one construct will not move it.

A low score means low change-risk, not "well tested". A trivial construct scores low because there is little to go wrong, not because it is guarded. This is why the report shows the two factors separately on every construct: hazard 0% × exposure 100% and hazard 20% × exposure 0% both fuse to a score of zero, and they are opposite situations. The first is untested trivial code; the second is genuinely guarded code.

How much of your project could be measured

Constructs are reported in three groups, because a score over a known input space and a score over a guessed one are different measurements and averaging them produces a number that means neither:

HTML Coverage Reports (-t6)

Generate comprehensive interactive HTML reports:

ek9 -t6 main.ek9       # Generate HTML coverage report
ek9 -t6p main.ek9      # HTML report with profiling (flame graph, hot function table)

This creates a full coverage website in .ek9/coverage/ with multiple interconnected views.

Report Structure

.ek9/coverage/
├── index.html              # Dashboard with project overview
├── modules/                # Module detail pages
│   ├── myapp_core.html
│   └── myapp_utils.html
├── files/                  # Source file views
│   ├── __core_ek9.html     # Full source with highlighting
│   └── summary/            # File summary views
│       └── __core_ek9.html
├── coverage.css            # Styles with dark mode support
├── coverage.js             # Interactive features
├── EK9.png                 # Branding
└── ek9favicon.png

Dashboard (index.html)

Every page — project, module and file — opens with the same band, so the reading is the same wherever you have drilled to: what the tests verify, and what that means for changing this code.

Quality at a Glance

Quality at a Glance: a verification target of three concentric rings beside the HEX risk profile sunburst

The rings and the bars point in opposite directions on purpose. A ring is a target to fill — full is good. A budget bar is a ceiling to stay under — empty is good.

The rest of the dashboard

Complexity budget tracks

Complexity and cognitive complexity as budget bars, each showing the maximum as fill, the average as a marker, and the remaining headroom

One track per metric, with the maximum as the bar and the average as a marker on the same track — because their relationship is the finding. A wide gap means one outlier is dragging the maximum while the rest of the code is calm, so there is a single construct to go and fix. An average close to the maximum means it is systemic, and fixing one construct will not move it. The remaining headroom is stated in words beside each track.

These limits are enforced: pass them and the code does not compile (E11010 complexity, E11021 cognitive complexity). Readability is shown alongside as a reported value with no bar, because no error fires on it — it is measured and stated, never enforced.

Module and File Pages

Click any module to see the same band scoped to it, then the detail:

A file page carries the same rows. It states its HIGH count rather than repeating the charts: over a handful of constructs a chart restates what the rows already say, and each row is already carrying that construct's score and both its factors.

Source Code Views

Source code view with coverage highlighting, complexity badges, and profiling badges

The most detailed view shows syntax-highlighted source with coverage data overlaid:

Interactive Features

Navigation Flow

Dashboard (index.html)
    │
    ├─╺ Attention Panel ╺ Module Page
    │
    └─╺ Module List ╺ Module Page (modules/myapp_utils.html)
                          │
                          ├─╺ Source File List ╺ File Summary
                          │                         │
                          │                         └─╺ Source View (files/__utils_ek9.html)
                          │
                          └─╺ Function List ╺ Source View (with line anchor)

Breadcrumb navigation allows moving back up: Source → Module → Dashboard

See Also