1818# Usage (from the CONSUMER workspace):
1919# bazel coverage --config=llvm_cov //... --build_tests_only
2020# bazel run @score_tooling//coverage:generate_coverage_html -- \
21- # --yaml <path/to/coverage_justifications.yaml> \
22- # [--archive <archive-name>] [--platform <platform >] \
23- # [--testlogs-subdir <subdir>] [output-dir]
21+ # [ --yaml <path/to/coverage_justifications.yaml>] \
22+ # [--archive <archive-name>] [--archive-dir <dir >] \
23+ # [--platform <platform>] [-- testlogs-subdir <subdir>] [output-dir]
2424#
2525# Arguments:
2626# --yaml <path> Justification YAML, relative to the workspace
27- # root (required).
28- # --archive <archive-name> Also create a zip archive named <archive-name>.zip
27+ # root. When omitted, justification processing and
28+ # the effective-coverage metric are skipped and the
29+ # threshold gate applies to the RAW line coverage.
30+ # --archive <archive-name> Create a zip archive named <archive-name>.zip
2931# containing the HTML report, raw LCOV data and JUnit XMLs.
32+ # --archive-dir <dir> Assemble the same content as --archive into <dir>
33+ # WITHOUT zipping — preferred for CI artifact
34+ # uploads (actions/upload-artifact zips its input
35+ # itself; a pre-zipped file would be zipped twice).
3036# --platform <platform> Target platform for justification filtering
3137# (default: linux). Also affects the default output
3238# directory (coverage_<platform>).
3743# (default: coverage_<platform>)
3844#
3945# Environment:
40- # COVERAGE_THRESHOLD Minimum effective line coverage percentage
41- # (default: 100). The script exits non-zero when
42- # effective coverage is below this threshold.
46+ # COVERAGE_THRESHOLD Minimum line coverage percentage (default: 100).
47+ # The script exits non-zero when the gated metric
48+ # (effective coverage with --yaml, raw coverage
49+ # without) is below this threshold.
4350
4451set -euo pipefail
4552
4653ARCHIVE_NAME=" "
54+ ARCHIVE_DIR=" "
4755PLATFORM=" linux"
4856OUTPUT_DIR=" "
4957JUSTIFICATION_YAML_REL=" "
@@ -59,6 +67,10 @@ while [[ $# -gt 0 ]]; do
5967 ARCHIVE_NAME=" ${2:? --archive requires a name argument} "
6068 shift 2
6169 ;;
70+ --archive-dir)
71+ ARCHIVE_DIR=" ${2:? --archive-dir requires a directory argument} "
72+ shift 2
73+ ;;
6274 --platform)
6375 PLATFORM=" ${2:? --platform requires a platform argument (e.g. linux or qnx)} "
6476 shift 2
@@ -74,10 +86,9 @@ while [[ $# -gt 0 ]]; do
7486 esac
7587done
7688
77- if [[ -z " ${JUSTIFICATION_YAML_REL} " ]]; then
78- echo " ERROR: --yaml <path/to/coverage_justifications.yaml> is required." >&2
79- exit 1
80- fi
89+ # --yaml is optional: without it, justification processing and the effective
90+ # coverage metric are skipped and the threshold gate applies to the RAW line
91+ # coverage from llvm-cov's summary instead.
8192
8293# Set default output directory based on platform if not explicitly provided.
8394if [[ -z " ${OUTPUT_DIR} " ]]; then
@@ -126,80 +137,120 @@ fi
126137echo " Coverage report written to: ${OUTPUT_DIR} "
127138
128139# ---------------------------------------------------------------------------
129- # Run coverage justification processing.
140+ # Run coverage justification processing (only when --yaml was given) and
141+ # enforce the coverage threshold.
130142# ---------------------------------------------------------------------------
131- JUSTIFICATION_YAML=" ${BUILD_WORKSPACE_DIRECTORY} /${JUSTIFICATION_YAML_REL} "
143+ THRESHOLD=" ${COVERAGE_THRESHOLD:- 100} "
144+ JUSTIFICATION_DIR=" "
132145
133- if [[ ! -f " ${JUSTIFICATION_YAML} " ]]; then
134- echo " ERROR: ${JUSTIFICATION_YAML} not found." >&2
135- exit 1
136- fi
146+ if [[ -n " ${JUSTIFICATION_YAML_REL} " ]]; then
147+ JUSTIFICATION_YAML=" ${BUILD_WORKSPACE_DIRECTORY} /${JUSTIFICATION_YAML_REL} "
137148
138- echo " "
139- echo " Running coverage justification processing..."
140-
141- JUSTIFICATION_DIR=" ${TMPDIR_EXTRACT} /justification_report"
142- mkdir -p " ${JUSTIFICATION_DIR} "
143-
144- # Run justify.py / effective_coverage.py via nested bazel invocations from the
145- # consumer workspace. This deliberately avoids runfiles resolution across
146- # module boundaries (canonical repo names vary between Bazel versions).
147- bazel run @score_tooling//coverage:justify -- \
148- --yaml " ${JUSTIFICATION_YAML} " \
149- --source-root " ${BUILD_WORKSPACE_DIRECTORY} " \
150- --platform " ${PLATFORM} " \
151- --output " ${JUSTIFICATION_DIR} /manifest.json"
152-
153- bazel run @score_tooling//coverage:effective_coverage -- \
154- --html-dir " ${OUTPUT_DIR} " \
155- --manifest " ${JUSTIFICATION_DIR} /manifest.json" \
156- --output " ${JUSTIFICATION_DIR} /report.json"
157-
158- # Display effective coverage summary and enforce the threshold.
159- if [[ ! -f " ${JUSTIFICATION_DIR} /summary.txt" ]]; then
160- echo " ERROR: Effective coverage summary was not produced." >&2
161- exit 1
162- fi
149+ if [[ ! -f " ${JUSTIFICATION_YAML} " ]]; then
150+ echo " ERROR: ${JUSTIFICATION_YAML} not found." >&2
151+ exit 1
152+ fi
163153
164- echo " "
165- cat " ${JUSTIFICATION_DIR} /summary.txt"
154+ echo " "
155+ echo " Running coverage justification processing..."
156+
157+ JUSTIFICATION_DIR=" ${TMPDIR_EXTRACT} /justification_report"
158+ mkdir -p " ${JUSTIFICATION_DIR} "
159+
160+ # Run justify.py / effective_coverage.py via nested bazel invocations from the
161+ # consumer workspace. This deliberately avoids runfiles resolution across
162+ # module boundaries (canonical repo names vary between Bazel versions).
163+ bazel run @score_tooling//coverage:justify -- \
164+ --yaml " ${JUSTIFICATION_YAML} " \
165+ --source-root " ${BUILD_WORKSPACE_DIRECTORY} " \
166+ --platform " ${PLATFORM} " \
167+ --output " ${JUSTIFICATION_DIR} /manifest.json"
168+
169+ bazel run @score_tooling//coverage:effective_coverage -- \
170+ --html-dir " ${OUTPUT_DIR} " \
171+ --manifest " ${JUSTIFICATION_DIR} /manifest.json" \
172+ --output " ${JUSTIFICATION_DIR} /report.json"
173+
174+ # Display effective coverage summary and enforce the threshold.
175+ if [[ ! -f " ${JUSTIFICATION_DIR} /summary.txt" ]]; then
176+ echo " ERROR: Effective coverage summary was not produced." >&2
177+ exit 1
178+ fi
166179
167- # Extract effective coverage percentage for threshold check.
168- EFFECTIVE_PCT=$( grep -oP ' Effective line coverage:\s+\K[0-9.]+' \
169- " ${JUSTIFICATION_DIR} /summary.txt" 2> /dev/null || echo " 0" )
180+ echo " "
181+ cat " ${JUSTIFICATION_DIR} /summary.txt"
182+
183+ # Extract effective coverage percentage for threshold check.
184+ GATE_PCT=$( grep -oP ' Effective line coverage:\s+\K[0-9.]+' \
185+ " ${JUSTIFICATION_DIR} /summary.txt" 2> /dev/null || echo " 0" )
186+ GATE_KIND=" Effective"
187+ else
188+ # No justification YAML: gate on the raw line coverage computed from the
189+ # LCOV data. Deliberately NOT llvm-cov's text summary TOTAL — that summary
190+ # omits baseline-only files (in-scope files no test links against), which
191+ # would let untested files escape the gate. The LCOV includes them.
192+ echo " "
193+ echo " INFO: no --yaml given; justification processing skipped, gating on raw line coverage."
194+ if [[ ! -f " ${TMPDIR_EXTRACT} /lcov_report/lcov.dat" ]]; then
195+ echo " ERROR: lcov_report/lcov.dat not found in ${COVERAGE_REPORT} " >&2
196+ exit 1
197+ fi
198+ GATE_PCT=$( awk -F: ' /^LF:/ {lf += $2} /^LH:/ {lh += $2}
199+ END { if (lf > 0) printf "%.2f", lh * 100 / lf; }' \
200+ " ${TMPDIR_EXTRACT} /lcov_report/lcov.dat" )
201+ if [[ -z " ${GATE_PCT} " ]]; then
202+ echo " ERROR: could not compute raw line coverage from lcov_report/lcov.dat" >&2
203+ exit 1
204+ fi
205+ echo " Raw line coverage: ${GATE_PCT} %"
206+ GATE_KIND=" Raw"
207+ fi
170208
171209# Threshold check (default: 100%). Fails the run when below.
172- THRESHOLD=" ${COVERAGE_THRESHOLD:- 100} "
173- if ! awk " BEGIN {exit (${EFFECTIVE_PCT} >= ${THRESHOLD} ) ? 0 : 1}" ; then
174- echo " ERROR: Effective coverage ${EFFECTIVE_PCT} % is below threshold ${THRESHOLD} %" >&2
210+ if ! awk " BEGIN {exit (${GATE_PCT} >= ${THRESHOLD} ) ? 0 : 1}" ; then
211+ echo " ERROR: ${GATE_KIND} coverage ${GATE_PCT} % is below threshold ${THRESHOLD} %" >&2
175212 RC=1
176213else
177214 RC=0
178215fi
179216
180217# ---------------------------------------------------------------------------
181- # Optional: create a zip archive with the HTML report, raw LCOV data and
182- # JUnit XML test results.
218+ # Optional: assemble the HTML report, raw LCOV data, justification report and
219+ # JUnit XML test results into an artifacts tree.
220+ # --archive <name> zip the tree into <name>.zip (and remove the tree)
221+ # --archive-dir <dir> keep the tree at <dir> — preferred for CI artifact
222+ # uploads, since actions/upload-artifact zips its input
223+ # anyway (a pre-zipped file would be zipped twice)
183224# ---------------------------------------------------------------------------
184- if [[ -n " ${ARCHIVE_NAME} " ]]; then
185- mkdir -p artifacts
225+ assemble_artifacts () {
226+ local dest=" $1 "
227+ mkdir -p " ${dest} "
186228
187229 # Copy JUnit XML test results preserving directory structure.
188- find " bazel-testlogs/${TESTLOGS_SUBDIR} " -name ' test.xml' -exec cp --parents {} artifacts/ \;
230+ find " bazel-testlogs/${TESTLOGS_SUBDIR} " -name ' test.xml' -exec cp --parents {} " ${dest} / " \;
189231
190232 # Copy the HTML coverage report
191- cp -r " ${OUTPUT_DIR} " artifacts/
233+ cp -r " ${OUTPUT_DIR} " " ${dest} / "
192234
193235 # Include the LCOV .dat file from the reporter zip.
194236 if [[ -f " ${TMPDIR_EXTRACT} /lcov_report/lcov.dat" ]]; then
195- cp " ${TMPDIR_EXTRACT} /lcov_report/lcov.dat" artifacts /coverage_report.dat
237+ cp " ${TMPDIR_EXTRACT} /lcov_report/lcov.dat" " ${dest} /coverage_report.dat"
196238 fi
197239
198240 # Include the justification report (manifest + effective coverage json).
199- if [[ -d " ${JUSTIFICATION_DIR} " ]]; then
200- cp -r " ${JUSTIFICATION_DIR} " artifacts/
241+ if [[ -n " ${JUSTIFICATION_DIR} " && - d " ${JUSTIFICATION_DIR} " ]]; then
242+ cp -r " ${JUSTIFICATION_DIR} " " ${dest} / "
201243 fi
244+ }
202245
246+ if [[ -n " ${ARCHIVE_DIR} " ]]; then
247+ rm -rf " ${ARCHIVE_DIR} "
248+ assemble_artifacts " ${ARCHIVE_DIR} "
249+ echo " Coverage artifacts written to: ${ARCHIVE_DIR} /"
250+ fi
251+
252+ if [[ -n " ${ARCHIVE_NAME} " ]]; then
253+ assemble_artifacts artifacts
203254 zip -r " ${ARCHIVE_NAME} .zip" artifacts/
204255 rm -rf artifacts/
205256 echo " Coverage archive written to: ${ARCHIVE_NAME} .zip"
0 commit comments