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:
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):
- 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. |
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:
| 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(andami.db.tests.enabled=truefor 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=truereturns the coverage report, not the pass/fail table: run without coverage (or let the CI runner's separate pass) for per-test status.
Related¶
- Unit Testing: the isolated runner that produces coverage deterministically.
- Center Debugger: the
coverageverb and the per-statement hook coverage rides on.