coverage: emit a real baseline for packages with no tests (#4669)
**What type of PR is this?**
> Bug fix
**What does this PR do? Why is it needed?**
rules_go doesn't pass baseline coverage files, so Bazel falls back to
`BaselineCoverageAction`, which writes only the path and
`end_of_record`. An untested package lands in the report as `LF:0 /
LH:0`, which is also what a file with genuinely nothing to cover looks
like. Consumers can't distinguish them, so 0/0 gets scored as 100% and a
package with no tests scores better than one with partial tests.
This runs `cmd/cover` over the target's own `srcs`, reads back the block
position table it appends, and emits LCOV with every coverable line at
count 0, handed to Bazel via `baseline_coverage_files`. For an untested
library:
```diff
SF:src/untested.go
-FNF:0
-FNH:0
-LF:0
+DA:4,0
+DA:5,0
+DA:6,0
+DA:7,0
+LF:4
LH:0
end_of_record
```
**Which issues(s) does this PR fix?**
Fixes #4668
**Other notes for review**
It's 879 lines, but only 85 touch existing files. The rest is one
builder action, one `.bzl`, and the tests.
The choices most likely to draw questions:
- The line set comes from `cmd/cover` rather than a separate AST pass,
so the baseline agrees with measured output by construction rather than
by approximation. `cover -pkgcfg` with `EmitMetaFile` is the mechanism
`go test -cover` uses for a package with no test files, which is exactly
this situation, and `covdata textfmt` decodes the result into the same
text profile `bzltestutil` already consumes. I tried an AST generator
first, and against a file whose real coverage run gives `LF:21` it
emitted 14 lines, missing `case` clauses, continuation lines of
multi-line statements, and closing braces.
- `covdata` is built from the SDK's own sources in the same action that
already builds `pack` that way, since Go 1.25 dropped both prebuilt
binaries from the distribution to shrink it. The shared Go build cache
makes the extra build roughly a second, and a `covdata` compiled from
its own SDK decodes whatever that SDK emits.
- This adds an optional `covdata` attribute to `go_toolchain`. Custom
toolchain definitions that omit it simply emit no baseline, so nothing
breaks for them.
- `Local: true` makes `cmd/cover` record each source under the
exec-root-relative name it was given, so no path mapping round trip is
needed. It changes only the recorded name, not the block positions.
- An empty meta-data file is the signal that a package has no coverable
lines, which is structural rather than reconstructed from the sources.
An absent one fails the action, so a future change to `EmitMetaFile`
cannot quietly report `LF:0` everywhere.
- LCOV writing is hand-rolled, and the profile pattern is duplicated
from `bzltestutil`, because `builder` is a `go_tool_binary` limited to
the standard library and `bzltestutil` imports `coverdata`.
- `go_test` is deliberately untouched, since emitting baselines for
`_test.go` files would regress results by scoring test files. Embedded
libraries are unaffected, as the `go_library` they come from emits its
own baseline from its own `srcs`. The gap this leaves is a non-test
`.go` file that lives only in a `go_test`'s `srcs`, which gets no
baseline. Happy to close that by filtering test files out of the test's
own srcs if you'd prefer.
- Sources come from `ctx.files.srcs`, not `go_info.srcs`, to match
`source_attributes = ["srcs"]`. `go_info.srcs` would pull in the
generated coverage shim and embedded srcs, adding bogus and duplicate
records.
- Files with nothing to cover still get an `SF:` record with `LF:0`, so
the set of files in the report is unchanged. The zero is now measured
rather than assumed.
- cgo files get baselines like any other source. `compilepkg`
instruments them before cgo rewrites them, so a measured run reports
positions in the unprocessed file, which are the ones this action reads
back.
- The action is path mapped under exactly the condition `compilepkg`
uses, and skips it for cgo and for targets tagged `local`. The two have
to agree, since a generated source under
`--experimental_output_paths=strip` would otherwise get a stripped path
in the baseline and an unstripped one from the compile action, leaving
the zeros in a record measured data never displaces.
This is guarded on
`bazel_features.rules.instrumented_files_info_has_baseline_coverage_files`,
and no dependency bump is needed since the current 1.36.0 pin already
has that flag. On Bazel 7 and 8 the kwarg isn't passed and behaviour is
unchanged. The `polyfill_bazel_features.bzl` value is a constant
`False`, since Bazel 9 is also the release that removed WORKSPACE
support.
Nothing runs on ordinary builds. The action is gated on
`go.coverage_enabled` and `go.coverage_instrumented`, so
`--instrumentation_filter` is respected too.
The `go_bazel_test` covers an untested library, an untested binary, an
untested cgo library, a declaration-only file, a build-constrained file,
and that measured coverage isn't displaced. It runs in CI on the Bazel 9
legs, and on Bazel 8 the `bazel_features` gate makes it incompatible so
a `//...` pattern skips it. A unit test covers the profile reader. I
also ran this across a ~680-file Go monorepo: same set of files in the
report, measured coverage byte-identical, and 21 files that had been
reporting 100% now report 0%.
---------
Co-authored-by: Logan Rosen <loganrosen@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Fabian Meumertzheim <fabian@meumertzhe.im>
17 files changed