feat(coverage): Go-side line coverage built on the debug hook (LCOV) - #35
Open
xepozz wants to merge 2 commits into
Open
feat(coverage): Go-side line coverage built on the debug hook (LCOV)#35xepozz wants to merge 2 commits into
xepozz wants to merge 2 commits into
Conversation
Adds line and count debug hooks, the primitive test-coverage tooling
(LuaCov-style, LCOV output) is built on. debug.sethook/gethook were already
declared in the type stub (compiler/stdlib/debug.go) but had no runtime
implementation, so any coverage instrumentation was impossible.
- hook.go: sethook/gethook + the callHook/fireHook engine. All state lives on
LState (hook, hookMask, hookCount, hookCounter, hookLastLine, inHook) — the
hot callFrame struct is left untouched, so there is no per-frame size cost on
the call path.
- vm.go: one guarded call at the dispatch-loop head; when no hook is installed
it is a single uint8 compare. Measured (benchstat, n=10, paired vs main):
every VM benchmark within noise, geomean -0.10% — no measurable overhead.
- Line events fire when the current source line changes (tracked on LState).
This is a coverage-oriented simplification of PUC's per-frame oldpc tracking:
every executed line is reported at least once; it does not re-fire a line on
a same-frame loop back-edge. Happy to switch to full per-frame semantics if
preferred.
- Reentrancy is blocked via LState.inHook so a hook cannot trigger itself; the
register top is saved/restored around the call.
- Call/return ('c'/'r') masks are rejected explicitly rather than silently
ignored — line/count only for now.
hook_test.go covers coverage collection, count firing, gethook round-trip,
nested calls, reentrancy, sethook(nil) clearing, and c/r rejection.
Full suite (2199 tests) green, go vet clean. No generated files touched.
Coverage is a consumer of the debug hook (this branch is based on the sethook/gethook branch), not a separate instrumentation path: - WIPPY_COVERAGE arms every LState's line hook via covArm (hookMask |= HookMaskLine) in newLState — no debug.sethook call needed, no separate dispatch-loop guard; - the hook's line-event path in callHook records each executing source line (numerator), reusing the hook's hookLastLine dedup; - CompileWithOptions registers every prototype's coverable lines (walking nested FunctionPrototypes' DbgSourcePositions) = exact denominator incl. never-run functions; - WriteCoverageLCOV / CoverageSummary emit a standard LCOV tracefile. No-op when WIPPY_COVERAGE is unset (covArm and covRegisterProto both bail). Full suite green, go vet clean; smoke confirms covArm arms the hook and hits are recorded with no explicit sethook.
Contributor
|
we can keep it for v1 for now since v2 already have coverage, will be reviewed, thank you |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds an opt-in line-coverage collector that emits a standard LCOV tracefile.
It is a consumer of the debug hook from #34, not a separate instrumentation path.
Enable by setting
WIPPY_COVERAGEin the environment before the process starts.The host then calls
WriteCoverageLCOV(path, filter)to write the report.Why
Line hooks are the primitive every Lua coverage tool is built on. #34 adds the
hook; this PR turns it into a working coverage collector that spans a whole
embedding — every
LState, including coroutine/child threads — which is awkwardto achieve with pure-Lua
debug.sethookwiring in a host that spawns many states.Design
coverage.go(new) — the collector:covArm,covRecordHit,covRegisterProto,WriteCoverageLCOV,CoverageSummary,CoverageEnabled.WIPPY_COVERAGEis set,covArmarms every newLStatewith a line hook (hookMask |= HookMaskLine) —no
debug.sethookcall needed. The hook's existing line-event path incallHookrecords the executing source line, reusing itshookLastLinededup.covArmis wired into bothnewLStateandnewLStateWithGlobal(thecoroutine/thread constructor), so child states are covered too.
CompileWithOptionsregisters everyprototype's
DbgSourcePositions, walking nestedFunctionPrototypes— so thedenominator includes never-executed functions, not just lines that ran.
WriteCoverageLCOV(path, filter)emitsSF/DA/LF/LH/end_of_record;CoverageSummary(filter)returns aggregate(found, hit). Collection isprocess-global (merged across all
LStates), so a host that runs many states orcoroutines gets a single report — dump it once at shutdown.
Usage
Source names in
SF:are the chunk names you pass toLoad/LoadString(e.g.
@path/to/file.lua), so the filter selects which chunks land in the report.Example output:
coverage.infois consumed as-is bygenhtml, Codecov/Coveralls, and editorcoverage plugins.
Performance
Zero hot-path cost when disabled:
covArmandcovRegisterProtobail immediatelyunless
WIPPY_COVERAGEis set, and the dispatch loop uses the hook's existingif L.hookMask != 0guard (from #34) — no additional per-instruction check. Withno hook installed and
WIPPY_COVERAGEunset, none of this runs.Scope of changes
coverage.go(new) + one line incallHook(hook.go), thecovArmcalls instate.go/state_pool.go, and onecovRegisterProtocall incompile_options.go. No generated files touched.Verification
go vetclean.and coroutines, running a workload under
WIPPY_COVERAGE, and confirming theemitted LCOV parses cleanly in
genhtmland reports plausible per-file linecoverage. A regression where coroutine/child-thread states were left unarmed
(recording only part of the executed lines) is fixed by arming
newLStateWithGlobal.Note for maintainers
This is opinionated tooling rather than a Lua-language feature. If you'd prefer to
keep coverage out of the core, I'm happy to gate it behind a build tag
(
//go:build coverage) or maintain it as a fork — just let me know.