Skip to content
Developer Tools

Code Coverage

3forge Enterprise measures line and branch coverage of server-side AmiScript (procedures, triggers, timers, and user methods) using the Center debugger's per-statement hook. You get it 2 ways: as a by-product of a unit-test run, or live on a running Center via the debugger. Either way it reports the same model and can write standard cobertura / lcov / HTML files for CI.

Note

Coverage shares the debugger's per-statement hook, so it needs ami.db.debug.enabled=true (the unit-test runner additionally needs ami.db.tests.enabled=true). With the flag off, nothing is recorded.

What Is Measured

Dimension Meaning
Line Which executable source lines ran. The denominator is every executable line of the target's body, so un-run lines show as missed.
Branch For each decision point, which of its 2 arms were taken. A line whose statement ran but only exercised one arm is a partial branch.

Branch coverage is recorded for if/else, while, do/while, for, foreach, and the ternary ?:. Each decision contributes 2 arms; a target's branch percentage is the fraction of arms actually taken.

Why branch coverage catches what line coverage misses

A proc can be at 100% line coverage yet have an untested path. Consider if (x > 10) { … } with no else: a single test that makes x > 10 runs every line, so line coverage is 100%, but the false arm (the if not taken) was never exercised. Branch coverage reports that as 1/2, flagging the gap that line coverage hides.

The Report

Both entry points return the same per-target rows:

Column Meaning
TargetId center:<kind>:<name>:<slot>
Covered / Total / Pct Line coverage: covered, executable, percentage.
CoveredLines / MissedLines The line numbers behind those counts.
BranchesCovered / BranchesTotal / BranchPct Branch arms taken, total arms, percentage.
PartialBranchLines Lines where some but not all arms were taken.

For a procedure that runs a while (both arms), a for that never enters (exit arm only), and a ternary that only takes its true arm:

1
2
3
=> [{"TargetId":"center:procedure:priceLoop:script",
     "Covered":8,"Total":9,"Pct":"88.9%","CoveredLines":"1,2,3,4,5,7,10,11","MissedLines":"8",
     "BranchesCovered":4,"BranchesTotal":6,"BranchPct":"66.7%","PartialBranchLines":"7,10"}]

The while line is fully covered (both arms); the for and the ternary are partial (one arm each); line 8, the body of the for that never entered, is the one missed line.

From a Unit-Test Run

Pass coverage=true to ami_execute(op=runComponentTests):

ami_execute(op=runComponentTests, componentName="center", testFile="<path>/sample.test.amisql", coverage=true)
  • Coverage is measured for the production surface only: the procedures/triggers/timers/methods under test. Objects a test file defines for its own fixtures, and the test bodies themselves, are excluded, so the percentages reflect the code under test rather than the harness.
  • The whole production surface is enumerated, so an object no test touched shows at 0% rather than silently missing.

Ternary coverage in a test run

Ternary (?:) coverage is compiled in only when the component is running the test framework (ami.db.tests.enabled), so the ordinary derived-cell evaluation path pays nothing for it in production. In an isolated test clone (which always has the framework on) ternaries are fully counted.

Recording Coverage on a Live Center

The debugger's coverage verb records against production execution without freezing anything:

Command Effect
coverage on Start recording (keeps any existing hits).
coverage off Stop recording (hits retained).
coverage clear Drop all recorded hits.
coverage report Snapshot the per-target table above.
1
2
3
4
center_debug("coverage on")
# ... let timers / triggers / procedures run against real traffic ...
center_debug("coverage report")
center_debug("coverage off")

This measures whatever the Center actually executed while recording was on, which is useful for seeing which paths real activity exercises, as opposed to the deterministic view a test suite gives.

Report Files for CI

Give the test run an outputDir to write standard coverage files:

1
2
3
4
5
ami_execute(op=runComponentTests, componentName="center", testFile="...", coverage=true,
                                  outputDir="<path>/coverage")
=> [{"Artifact":"cobertura.xml","Path":".../cobertura.xml"},
    {"Artifact":"coverage.html","Path":".../coverage.html"},
    {"Artifact":"coverage.info","Path":".../coverage.info"}]
File Format Carries branch data as
cobertura.xml Cobertura XML (SonarQube / Codecov / GitHub / Jenkins) branch-rate and per-line branch="true" condition-coverage="P% (c/t)"
coverage.info lcov tracefile (VS Code Coverage Gutters, genhtml) BRDA per arm, with BRF/BRH totals
coverage.html Self-contained source-annotated report covered lines green, missed red, partial branches yellow with a c/t annotation and a per-target branch column

Where the files land

outputDir is resolved against the original component's permitted surface (see ami_console(op=permittedDirs)), not the throwaway test clone's, so the artifacts persist on the real component's disk after the clone is torn down. A path outside a permitted surface is rejected.

In a CI Pipeline

The CI runner drives all of this on startup. When coverage is enabled for a component, the runner derives pass/fail from a normal (non-coverage) run and generates coverage as a separate artifact pass, so the JUnit result and the coverage files are both produced and never interfere. A coverage-generation failure is logged but does not fail the build, because coverage is a report, not a test outcome.

Gotchas and Limitations

  • Dev-gated. Needs ami.db.debug.enabled=true (and ami.db.tests.enabled=true for the test runner).
  • Server-side AmiScript only: Center procedures, triggers, timers, and user methods.
  • Production surface only in a test run: test-file fixtures and test bodies are excluded from the report.
  • coverage=true returns the coverage report, not the pass/fail table: run without coverage (or let the CI runner's separate pass) for per-test status.
  • Unit Testing: the isolated runner that produces coverage deterministically.
  • Center Debugger: the coverage verb and the per-statement hook coverage rides on.