blob: 6db6cb015b9389696d9764f681ae1280f86b41e3 [file]
#!/usr/bin/env bash
set -o errexit -o nounset -o pipefail
# Coverage behavior for `bazel test` / `bazel coverage`.
#
# Every target executes the same covered()/uncovered() code and differs only in how
# the source is provided or the test exits — see BUILD.bazel for what each exercises.
#
# //:first_party_npmpkg_test is an UNSUPPORTED scenario, kept as executable
# documentation: first-party code repackaged with npm_package runs from a copy in
# the .aspect_rules_js store with no link back to source, so coverage comes out
# empty. Use js_library linking (//:first_party_jslib_test) for first-party
# coverage. See docs/troubleshooting.md and
# https://github.com/aspect-build/rules_js/issues/2933.
#
# Coverage is checked twice: inline (the _lcov_merger runs in the test action) and
# split (--experimental_split_coverage_postprocessing, required for remote
# execution), where the merger runs as its own action without the test's runfiles.
# Both must report the same coverage: the report is generated in the test action,
# and the merger only publishes it.
readonly WORKING_TARGETS=(//:test //:expected_exit_test //:first_party_jslib_test)
readonly UNSUPPORTED_TARGETS=(//:first_party_npmpkg_test)
readonly ALL_TARGETS=("${WORKING_TARGETS[@]}" "${UNSUPPORTED_TARGETS[@]}")
testlogs="$(bazel info bazel-testlogs)"
# Prints nothing and returns 0 if the report is real; otherwise prints the reason and
# returns 1. Keyed off the function names rather than the SF: path so it is agnostic
# to how the source resolves.
coverage_is_real() {
local dat="$1"
[[ -s "$dat" ]] || {
echo "coverage.dat is missing or empty"
return 1
}
grep -q '^FNDA:1,covered$' "$dat" || {
echo "covered() not marked as executed"
return 1
}
grep -q '^FNDA:0,uncovered$' "$dat" || {
echo "uncovered() not marked as unexecuted"
return 1
}
grep -Eq '^DA:[0-9]+,[1-9]' "$dat" || {
echo "all line execution counts are zero"
return 1
}
}
assert_coverage() {
local target="$1" mode="$2" dat="$testlogs/${1#//:}/coverage.dat" reason
if reason="$(coverage_is_real "$dat")"; then
echo "PASS($target, $mode)"
else
echo "FAIL($target, $mode): $reason"
cat "$dat" 2>/dev/null || true
exit 1
fi
}
# The unsupported case must report empty coverage, the documented behavior. If it
# ever produces a real report the script fails, so the docs get revisited rather than
# the stale expectation rotting unnoticed in a green log.
check_unsupported() {
local target="$1" mode="$2" dat="$testlogs/${1#//:}/coverage.dat" reason
if reason="$(coverage_is_real "$dat")"; then
echo "UNSUPPORTED($target, $mode) now produces coverage — update docs/troubleshooting.md and https://github.com/aspect-build/rules_js/issues/2933."
exit 1
else
echo "UNSUPPORTED($target, $mode): empty coverage, as documented ($reason)"
fi
}
# The code under test executes in every scenario. If this passes but coverage below
# is empty, the failure is in coverage reporting, not test execution.
bazel test --nocache_test_results "${ALL_TARGETS[@]}"
# --nocache_test_results on the coverage runs below: they differ only in
# post-processing flags, which do not invalidate a cached test result — without it a
# later run would just republish an earlier run's coverage.dat.
#
# No --instrument_test_targets: every covered source here lives in a js_library the
# test depends on, so the default instrumentation already picks it up.
bazel coverage --nocache_test_results "${ALL_TARGETS[@]}"
for t in "${WORKING_TARGETS[@]}"; do assert_coverage "$t" inline; done
for t in "${UNSUPPORTED_TARGETS[@]}"; do check_unsupported "$t" inline; done
bazel coverage --nocache_test_results \
--experimental_split_coverage_postprocessing \
--experimental_fetch_all_coverage_outputs \
"${ALL_TARGETS[@]}"
for t in "${WORKING_TARGETS[@]}"; do assert_coverage "$t" split; done
for t in "${UNSUPPORTED_TARGETS[@]}"; do check_unsupported "$t" split; done