Skip to content

feat(cli): add wheels coverage command with CRAP reporting - #3434

Merged
bpamiri merged 7 commits into
developfrom
peter/wheels-coverage-command
Aug 28, 2026
Merged

feat(cli): add wheels coverage command with CRAP reporting#3434
bpamiri merged 7 commits into
developfrom
peter/wheels-coverage-command

Conversation

@bpamiri

@bpamiri bpamiri commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a wheels coverage command that closes the CRAP loop started with the complexity gate and the debug-bar Complexity panel.

What it does

wheels coverage (CLI, from an app root with a running server):

  1. Instruments app/ with function-level coverage counters (server.__wheels_cov), backing up originals to .coverage-backup/ and reverting automatically when done (even on error). Backups are byte-exact — UTF-8 BOMs survive the round-trip (verified against pmx's app/lib/oauth2/oauth2.cfc).
  2. Runs the app test suite against the running server with ?coverage=true. vendor/wheels/tests/app-runner.cfm resets the counter map and dumps it to /tmp/wheels-app-coverage.json in a finally so it never breaks test output.
  3. Collects + combines coverage with per-file cyclomatic complexity into a CRAP ranking (complexity^2 x (1 - coverage)^3 + complexity), sorted worst-first, with --top N control (default 15). Failures throw Wheels.CoverageFailed (non-zero exit).

Details

  • New: cli/lucli/services/coverage/CoverageService.cfc (instrument/revert/run/collect/analyze/report)
  • cli/lucli/Module.cfc: public coverage() + parseCoverageArgs() (ArgSpec --top, --test-db); hidden from MCP tools (stateful — mutates app files while running)
  • vendor/wheels/tests/app-runner.cfm: coverage reset + best-effort JSON dump
  • Changelog fragment added

Also fixed along the way (all found by live runs)

  • Complexity-gate baseline matching (tools/code-quality/cfml-complexity.py): matched by exact file:line:function, so inserting lines in a file made unchanged over-threshold functions look "new" and failed CI. Now matches by file:function with a 250-line window for same-named functions; regression + new-hotspot detection verified.
  • Lucee ORO regex: (?m)(?<!:)// throws Sequence (?<...) not recognized on Lucee's ORO fallback. Replaced with :// masking + plain //[^\r\n]* in both the new service and the shipped debug-bar CodeComplexity.cfc (which carried the same latent pattern).
  • BOM preservation: see above — instrument/revert is now binary-exact.

Verified

  • Standalone service tests (instrument/analyze/report/revert, BOM round-trip) on Lucee 7 and RustCFML
  • Full wheels coverage e2e on the wheels repo demo app (3,717 counters instrumented and reverted on pmx, working-tree verified byte-identical afterwards)
  • Local CLI spec suite: 0 failures attributable to this change (2 environmental Docker errors in deploy specs)
  • Complexity gate: PASS on both trees; regression and new-function detection verified

Adds a ERROR:  command:

- instruments app/ with function-level coverage counters (server.__wheels_cov),
  backing up originals and reverting automatically
- runs the app test suite against the running server (?coverage=true)
- combines collected coverage with per-file cyclomatic complexity into a
  CRAP ranking (complexity^2 x (1 - coverage)^3 + complexity)
- app-runner.cfm dumps the counter map to /tmp/wheels-app-coverage.json when
  ?coverage=true (reset + best-effort dump so it never breaks test output)

Pairs with the debug bar Complexity panel (static) to complete the CRAP loop.

Signed-off-by: Peter Amiri <peter@alurium.com>
The gate looked baseline entries up by exact file:line:function id, so any
edit that shifted line numbers in a file (e.g. adding a new CLI command to
Module.cfc) made unchanged over-threshold functions look 'new' and failed CI.

Match by file:function now, with a 250-line window fallback when a file
defines several same-named functions. Line shifts from unrelated edits no
longer trip the gate; real regressions and genuinely new hotspots still fail.

Signed-off-by: Peter Amiri <peter@alurium.com>
Returning the error text made ERROR:  exit 0 on failure. Throw
Wheels.CoverageFailed so the runtime maps it to a non-zero exit, matching the
typed-error convention used by every other write-side command. The finally
block still reverts instrumentation.

Signed-off-by: Peter Amiri <peter@alurium.com>
ERROR:  deliberately builds its runner URL without reload — Lucee
recompiles instrumented files from their disk timestamps anyway, and an
application restart mid-test-run is an unneeded side effect.

Signed-off-by: Peter Amiri <peter@alurium.com>
parseCoverageArgs read parsed.testDb, but ArgSpec stores dashed flags under
their literal name (parseTestArgs uses parsed["test-db"]). Live e2e run of
ERROR:  surfaced it as: key [testDb] doesn't exist.

Signed-off-by: Peter Amiri <peter@alurium.com>
Lucee routes  to its ORO fallback, which throws
"Sequence (?<...) not recognized" — surfaced by a live ERROR:  run
at $analyze. The debug-bar CodeComplexity.cfc carried the same latent pattern.

Both now mask  (so URL strings cannot be eaten) and strip line comments
with a plain  — valid in ORO, Java, and RustCFML engines. Verified
on Lucee 7 against a sample with URLs, tag comments, block comments and code.

Signed-off-by: Peter Amiri <peter@alurium.com>
Live run on the pmx app showed FileRead/FileWrite round-trips drop the
UTF-8 BOM, leaving BOM'd files (e.g. app/lib/oauth2/oauth2.cfc) with a
one-line diff after revert. Backups now store the raw bytes and revert
restores them byte-for-byte; instrumented writes re-attach the BOM via a
ByteArrayOutputStream. Verified on Lucee 7 with BOM'd and BOM-less files:
hashes identical after instrument+revert, backup dir removed.

Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri
bpamiri merged commit 97737d2 into develop Aug 28, 2026
15 checks passed
@bpamiri
bpamiri deleted the peter/wheels-coverage-command branch August 28, 2026 17:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant