NAME
coverage-gate - run the whole four-metric coverage gate inside one environment
WHAT IT IS
The canonical entrypoint for the repository coverage gate. It drops the coverage database, runs the instrumented test suite, collects the lib/ report for statement, branch, condition and subroutine coverage, and enforces 100.0 on all four through script/check-all-metric-coverage.
WHAT IT IS FOR
It is the one command a developer, an automated round, or a continuous integration job runs to answer "does lib/ still measure 100.0 on every metric?". The documented gate and the executed gate are the same thing because there is only one of them.
WHY IT EXISTS
The gate used to be three shell lines. Each is its own interpreter with its own @INC, so the library path had to be repeated on all three, and Devel::Cover::DB::IO chooses its on-disk serialization format at BEGIN from whatever @INC makes visible - Sereal, then JSON, then Storable - without recording the choice beside the data.
On a host carrying two Devel::Cover installations whose available serializers differ, omitting the library path from one line of the chain leaves the reader unable to parse what the writer produced moments earlier. It surfaces as File is not a perl storable or Bad Sereal header: both read as a corrupt database, and the obvious response - delete it and run again - fails identically, spending another host-exclusive multi-minute suite slot every time. Two automated rounds paid that cost inside two hours, and the second did not recognise the first.
Documentation had already been written telling readers to repeat the library path, and it did not prevent the recurrence. Running the three commands as children of one process removes the hazard instead of warning about it: they inherit one environment because there is only one to inherit.
WHEN TO USE
Before claiming any change complete, and as the coverage step of every continuous integration workflow. Only one coverage run may be in flight on a host at a time, because instrumented timing-sensitive tests misread under contention.
HOW TO USE
Run it from anywhere INSIDE its own checkout, or from outside any checkout; it enters the repository root itself. It will refuse when the working directory is inside a DIFFERENT git checkout from the one it belongs to, naming both, and tell you to run that checkout's own copy instead - because otherwise it would grade a tree the caller never meant, succeed, and print a normal-looking result (DD-744). The refusal is narrow by design: it needs BOTH sides to be checkouts, so a copy running from a scratch directory is unaffected.
The resolved database is reported as an ABSOLUTE path. The configured name is relative and therefore identical whichever tree it lands in, which is precisely why a --dry-run inspection could not tell two trees apart. Give it test paths to narrow the instrumented run, --database to keep the database somewhere other than cover_db, and --dry-run to see the resolved environment and the exact commands before spending a suite slot.
It takes TWO locks, and they guard different things. The first is named after the coverage database, so two gates conflict exactly when they would share a database rather than merely when they run at the same time - a gate given its own --database contends for nothing. The second is a host lock at DD_SUITE_LOCK (default /tmp/dd-gate-host.lock), the same path run-suite takes, because a full suite running underneath a coverage pass invalidates the coverage verdict and until DD-734 nothing stopped it: both tools locked, on different files, so neither could block the other.
The host lock is not taken when HARNESS_ACTIVE is set. That is not an escape hatch, it is what keeps the gate able to pass the suite that runs it: nine test files invoke this script, and under run-suite - which holds the host lock for the whole run - every one of them would otherwise be refused by its own suite. That is the DD-526 failure, in which a repository-wide lock made the gate refuse the very tests driving it. A gate under a harness is a test fixture; a gate reached from coverage-run is a real run, and HARNESS_ACTIVE is unset there.
Exit statuses are the interface:
0 - statement, branch, condition and subroutine are all 100.0.
1 - a genuine shortfall; the failing metrics are named.
2 - the gate could not run, or could not read its report.
3 - the coverage instrument could not read its own database.
WHAT USES IT
The test, release-cpan and release-github workflows, the contributor testing guide, and t/148-coverage-gate-entrypoint.t, which is its acceptance contract.
EXAMPLES
Example 1:
perl script/coverage-gate
Run the full gate over the whole suite.
Example 2:
perl script/coverage-gate --dry-run
Print the resolved interpreter, library path, serializer module and the three commands, and run none of them. Use this to confirm the instrument before committing a host-exclusive slot to it.
Example 3:
perl script/coverage-gate --database /tmp/scratch-db t/107-all-metric-coverage-gate.t
Collect coverage for a focused set of tests into a scratch database, leaving the repository's own cover_db untouched.