Skip to content

Commit 20b679d

Browse files
committed
fix permission errors in coverage directory
1 parent e8664d5 commit 20b679d

File tree

12 files changed

+334
-43
lines changed

12 files changed

+334
-43
lines changed

build/cc_coverage/BUILD.bazel

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
2+
3+
# Custom collect_cc_coverage.sh that fixes COVERAGE_DIR permissions before running.
4+
# This works around a Bazel issue where COVERAGE_DIR is made read-only before
5+
# coverage post-processing runs with --experimental_split_coverage_postprocessing.
6+
filegroup(
7+
name = "collect_cc_coverage",
8+
srcs = ["collect_cc_coverage.sh"],
9+
visibility = ["//visibility:public"],
10+
)
11+
12+
sh_binary(
13+
name = "collect_cc_coverage_bin",
14+
srcs = ["collect_cc_coverage.sh"],
15+
visibility = ["//visibility:public"],
16+
)
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
#!/bin/bash
2+
# Custom collect_cc_coverage.sh that fixes COVERAGE_DIR permissions before running.
3+
# This works around a Bazel issue where COVERAGE_DIR is made read-only before
4+
# coverage post-processing runs with --experimental_split_coverage_postprocessing.
5+
6+
# Fix COVERAGE_DIR permissions if it exists
7+
if [ -n "$COVERAGE_DIR" ] && [ -d "$COVERAGE_DIR" ]; then
8+
chmod -R u+w "$COVERAGE_DIR" 2>/dev/null || true
9+
fi
10+
11+
# Now source and run the original collect_cc_coverage.sh from Bazel
12+
# We inline the original script here since we can't easily source external Bazel files
13+
14+
if [[ -n "$VERBOSE_COVERAGE" ]]; then
15+
set -x
16+
fi
17+
18+
# Checks if clang llvm coverage should be used instead of lcov.
19+
function uses_llvm() {
20+
if stat "${COVERAGE_DIR}"/*.profraw >/dev/null 2>&1; then
21+
return 0
22+
fi
23+
return 1
24+
}
25+
26+
# Returns 0 if gcov must be used, 1 otherwise.
27+
function uses_gcov() {
28+
[[ "$GCOV_COVERAGE" -eq "1" ]] && return 0
29+
return 1
30+
}
31+
32+
function init_gcov() {
33+
# Symlink the gcov tool such with a link called gcov. Clang comes with a tool
34+
# called llvm-cov, which behaves like gcov if symlinked in this way (otherwise
35+
# we would need to invoke it with "llvm-cov gcov").
36+
# For more details see https://llvm.org/docs/CommandGuide/llvm-cov.html.
37+
GCOV="${COVERAGE_DIR}/gcov"
38+
if [ ! -f "${COVERAGE_GCOV_PATH}" ]; then
39+
echo "GCov does not exist at the given path: '${COVERAGE_GCOV_PATH}'"
40+
exit 1
41+
fi
42+
# When using a tool from a toolchain COVERAGE_GCOV_PATH will be a relative
43+
# path. To make it work on different working directories it's required to
44+
# convert the path to an absolute one.
45+
COVERAGE_GCOV_PATH_ABS="$(cd "${COVERAGE_GCOV_PATH%/*}" && pwd)/${COVERAGE_GCOV_PATH##*/}"
46+
ln -s "${COVERAGE_GCOV_PATH_ABS}" "${GCOV}"
47+
}
48+
49+
# Computes code coverage data using the clang generated metadata found under
50+
# $COVERAGE_DIR.
51+
# Writes the collected coverage into the given output file.
52+
function llvm_coverage_lcov() {
53+
local output_file="${1}"; shift
54+
export LLVM_PROFILE_FILE="${COVERAGE_DIR}/%h-%p-%m.profraw"
55+
"${COVERAGE_GCOV_PATH}" merge -output "${output_file}.data" \
56+
"${COVERAGE_DIR}"/*.profraw
57+
58+
local object_param=""
59+
while read -r line; do
60+
if [[ ${line: -24} == "runtime_objects_list.txt" ]]; then
61+
while read -r line_runtime_object; do
62+
object_param+=" -object ${line_runtime_object}"
63+
done < "${line}"
64+
fi
65+
done < "${COVERAGE_MANIFEST}"
66+
67+
"${LLVM_COV}" export -instr-profile "${output_file}.data" -format=lcov \
68+
-ignore-filename-regex='^/tmp/.+' \
69+
${object_param} | sed 's#/proc/self/cwd/##' > "${output_file}"
70+
}
71+
72+
function llvm_coverage_profdata() {
73+
local output_file="${1}"; shift
74+
export LLVM_PROFILE_FILE="${COVERAGE_DIR}/%h-%p-%m.profraw"
75+
"${COVERAGE_GCOV_PATH}" merge -output "${output_file}" \
76+
"${COVERAGE_DIR}"/*.profraw
77+
}
78+
79+
# Generates a code coverage report in gcov intermediate text format by invoking
80+
# gcov and using the profile data (.gcda) and notes (.gcno) files.
81+
#
82+
# The profile data files are expected to be found under $COVERAGE_DIR.
83+
# The notes file are expected to be found under $ROOT.
84+
#
85+
# - output_file The location of the file where the generated code coverage
86+
# report is written.
87+
function gcov_coverage() {
88+
local output_file="${1}"; shift
89+
90+
# We'll save the standard output of each the gcov command in this log.
91+
local gcov_log="$output_file.gcov.log"
92+
93+
# Copy .gcno files next to their corresponding .gcda files in $COVERAGE_DIR
94+
# because gcov expects them to be in the same directory.
95+
while read -r line; do
96+
if [[ ${line: -4} == "gcno" ]]; then
97+
gcno_path=${line}
98+
local gcda="${COVERAGE_DIR}/$(dirname ${gcno_path})/$(basename ${gcno_path} .gcno).gcda"
99+
# If the gcda file was not found we skip generating coverage from the gcno
100+
# file.
101+
if [[ -f "$gcda" ]]; then
102+
# gcov expects both gcno and gcda files to be in the same directory.
103+
# We overcome this by copying the gcno to $COVERAGE_DIR where the gcda
104+
# files are expected to be.
105+
if [ ! -f "${COVERAGE_DIR}/${gcno_path}" ]; then
106+
mkdir -p "${COVERAGE_DIR}/$(dirname ${gcno_path})"
107+
cp "$ROOT/${gcno_path}" "${COVERAGE_DIR}/${gcno_path}"
108+
fi
109+
110+
# Extract gcov's version: the output of `gcov --version` contains the
111+
# version as a set of major-minor-patch numbers, of which we extract
112+
# the major version.
113+
# gcov --version outputs a line like:
114+
# gcov (Debian 7.3.0-5) 7.3.0
115+
# llvm-cov gcov --version outputs a line like:
116+
# LLVM version 9.0.1
117+
gcov_major_version=$("${GCOV}" --version | sed -n -E -e 's/^.*\s([0-9]+)\.[0-9]+\.[0-9]+\s?.*$/\1/p')
118+
119+
# Invoke gcov to generate a code coverage report with the flags:
120+
# -i Output gcov file in an intermediate text format.
121+
# The output is a single .gcov file per .gcda file.
122+
# No source code is required.
123+
# -o directory The directory containing the .gcno and
124+
# .gcda data files.
125+
# "${gcda"} The input file name. gcov is looking for data files
126+
# named after the input filename without its extension.
127+
# gcov produces files called <source file name>.gcov in the current
128+
# directory. These contain the coverage information of the source file
129+
# they correspond to. One .gcov file is produced for each source
130+
# (or header) file containing code which was compiled to produce the
131+
# .gcda files.
132+
# Don't generate branch coverage (-b) because of a gcov issue that
133+
# segfaults when both -i and -b are used (see
134+
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=84879).
135+
136+
# Don't generate branch coverage (-b) when using gcov 7 or earlier
137+
# because of a gcov issue that segfaults when both -i and -b are used
138+
# (see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=84879).
139+
if [[ $gcov_major_version -le 7 ]]; then
140+
"${GCOV}" -i $COVERAGE_GCOV_OPTIONS -o "$(dirname ${gcda})" "${gcda}"
141+
else
142+
"${GCOV}" -i -b $COVERAGE_GCOV_OPTIONS -o "$(dirname ${gcda})" "${gcda}"
143+
fi
144+
145+
# Check the type of output: gcov 9 or later outputs compressed JSON
146+
# files, but earlier versions of gcov, and all versions of llvm-cov,
147+
# do not. These output textual information.
148+
if stat --printf='' *.gcov.json.gz > /dev/null 2>&1; then
149+
# Concatenating JSON documents does not yield a valid document, so they are moved individually
150+
mv -- *.gcov.json.gz "$(dirname "$output_file")/$(dirname ${gcno_path})"
151+
else
152+
# Append all .gcov files in the current directory to the output file.
153+
cat -- *.gcov >> "$output_file"
154+
# Delete the .gcov files.
155+
rm -- *.gcov
156+
fi
157+
fi
158+
fi
159+
done < "${COVERAGE_MANIFEST}"
160+
}
161+
162+
function main() {
163+
init_gcov
164+
165+
# If llvm code coverage is used, we output the raw code coverage report in
166+
# the $COVERAGE_OUTPUT_FILE. This report will not be converted to any other
167+
# format by LcovMerger.
168+
# TODO(#5881): Convert profdata reports to lcov.
169+
if uses_llvm; then
170+
if [[ "${GENERATE_LLVM_LCOV}" == "1" ]]; then
171+
BAZEL_CC_COVERAGE_TOOL="LLVM_LCOV"
172+
else
173+
BAZEL_CC_COVERAGE_TOOL="PROFDATA"
174+
fi
175+
fi
176+
177+
# When using either gcov or lcov, have an output file specific to the test
178+
# and format used. For lcov we generate a ".dat" output file and for gcov
179+
# a ".gcov" output file. It is important that these files are generated under
180+
# COVERAGE_DIR.
181+
# When this script is invoked by tools/test/collect_coverage.sh either of
182+
# these two coverage reports will be picked up by LcovMerger and their
183+
# content will be converted and/or merged with other reports to an lcov
184+
# format, generating the final code coverage report.
185+
case "$BAZEL_CC_COVERAGE_TOOL" in
186+
("GCOV") gcov_coverage "$COVERAGE_DIR/_cc_coverage.gcov" ;;
187+
("PROFDATA") llvm_coverage_profdata "$COVERAGE_DIR/_cc_coverage.profdata" ;;
188+
("LLVM_LCOV") llvm_coverage_lcov "$COVERAGE_DIR/_cc_coverage.dat" ;;
189+
(*) echo "Coverage tool $BAZEL_CC_COVERAGE_TOOL not supported" \
190+
&& exit 1
191+
esac
192+
}
193+
194+
main

build/deps/build_deps.jsonc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@
5858
},
5959
{
6060
"name": "aspect_rules_js",
61-
"type": "bazel_dep"
61+
"type": "github_tarball",
62+
"owner": "anonrig",
63+
"repo": "rules_js",
64+
"branch": "yagiz/fix-postprocessing",
65+
"freeze_commit": "93f17a3810649c95f658d85ff277fe963d1a20f8"
6266
},
6367
{
6468
"name": "aspect_rules_ts",

build/deps/gen/build_deps.MODULE.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ bazel_dep(name = "aspect_rules_esbuild", version = "0.25.0")
1818

1919
# aspect_rules_js
2020
bazel_dep(name = "aspect_rules_js", version = "2.8.3")
21+
archive_override(
22+
module_name = "aspect_rules_js",
23+
integrity = "sha256-+3akxsGz4S5NKjTEPS/AafjsL+xU3GsOq2Nd3uVReps=",
24+
strip_prefix = "rules_js-93f17a3810649c95f658d85ff277fe963d1a20f8",
25+
urls = ["https://github.com/anonrig/rules_js/archive/93f17a3810649c95f658d85ff277fe963d1a20f8.tar.gz"],
26+
)
2127

2228
# aspect_rules_lint
2329
bazel_dep(name = "aspect_rules_lint", version = "1.13.0")

build/fixtures/BUILD.bazel

Lines changed: 0 additions & 1 deletion
This file was deleted.

build/fixtures/kj_test.sh

Lines changed: 0 additions & 3 deletions
This file was deleted.

build/kj_test.bzl

Lines changed: 106 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
2-
load("@rules_shell//shell:sh_test.bzl", "sh_test")
32

43
def kj_test(
54
src,
@@ -48,22 +47,118 @@ def kj_test(
4847
}),
4948
)
5049

51-
sh_test(
50+
_kj_test(
5251
name = test_name + "@",
5352
size = size,
54-
srcs = ["//build/fixtures:kj_test.sh"],
55-
data = [cross_alias] + data,
56-
args = ["$(location " + cross_alias + ")"],
53+
binary = cross_alias,
54+
data = data,
5755
tags = tags,
5856
)
5957

60-
# Tagged with no-coverage to reduce coverage CI time
61-
sh_test(
58+
_kj_test(
6259
name = test_name + "@all-autogates",
6360
size = size,
6461
env = {"WORKERD_ALL_AUTOGATES": "1"},
65-
srcs = ["//build/fixtures:kj_test.sh"],
66-
data = [cross_alias] + data,
67-
args = ["$(location " + cross_alias + ")"],
68-
tags = tags + ["no-coverage"],
62+
binary = cross_alias,
63+
data = data,
64+
tags = tags,
6965
)
66+
67+
# Shell template for kj_test - sets up coverage environment for the subprocess
68+
_SH_TEMPLATE = """#!/bin/sh
69+
set -e
70+
{env_exports}
71+
# Set up coverage for the test binary subprocess
72+
if [ -n "$COVERAGE_DIR" ]; then
73+
# Fix directory permissions for coverage post-processing
74+
# (Bazel may create COVERAGE_DIR with read-only permissions)
75+
chmod -R u+w "$COVERAGE_DIR" 2>/dev/null || true
76+
export LLVM_PROFILE_FILE="$COVERAGE_DIR/%p.profraw"
77+
export KJ_CLEAN_SHUTDOWN=1
78+
fi
79+
80+
exec {binary} "$@"
81+
"""
82+
83+
_BAT_TEMPLATE = """@echo off
84+
{env_exports}{binary} %*
85+
"""
86+
87+
def _kj_test_impl(ctx):
88+
is_windows = ctx.target_platform_has_constraint(ctx.attr._platforms_os_windows[platform_common.ConstraintValueInfo])
89+
90+
# Generate environment variable exports
91+
env_exports = ""
92+
for key, value in ctx.attr.env.items():
93+
if is_windows:
94+
env_exports += "set {}={}\n".format(key, value)
95+
else:
96+
env_exports += "export {}=\"{}\"\n".format(key, value)
97+
98+
if is_windows:
99+
executable = ctx.actions.declare_file("%s_kj_test.bat" % ctx.label.name)
100+
content = _BAT_TEMPLATE.format(binary = ctx.file.binary.short_path.replace("/", "\\"), env_exports = env_exports)
101+
else:
102+
executable = ctx.outputs.executable
103+
content = _SH_TEMPLATE.format(binary = ctx.file.binary.short_path, env_exports = env_exports)
104+
105+
ctx.actions.write(
106+
output = executable,
107+
content = content,
108+
is_executable = True,
109+
)
110+
111+
runfiles = ctx.runfiles(files = ctx.files.data + [ctx.file.binary])
112+
113+
# Merge the binary's runfiles
114+
default_runfiles = ctx.attr.binary[DefaultInfo].default_runfiles
115+
if default_runfiles:
116+
runfiles = runfiles.merge(default_runfiles)
117+
118+
# IMPORTANT: The binary must be listed in dependency_attributes to ensure
119+
# its transitive dependencies (all the C++ source files) are included in
120+
# coverage instrumentation. Without this, coverage data won't be collected.
121+
instrumented_files_info = coverage_common.instrumented_files_info(
122+
ctx,
123+
source_attributes = [],
124+
dependency_attributes = ["binary"],
125+
)
126+
127+
return [
128+
DefaultInfo(
129+
executable = executable,
130+
runfiles = runfiles,
131+
),
132+
instrumented_files_info,
133+
]
134+
135+
_kj_test = rule(
136+
implementation = _kj_test_impl,
137+
test = True,
138+
attrs = {
139+
# Implicit dependencies used by Bazel to generate coverage reports.
140+
"_lcov_merger": attr.label(
141+
default = configuration_field(fragment = "coverage", name = "output_generator"),
142+
executable = True,
143+
cfg = config.exec(exec_group = "test"),
144+
),
145+
"_collect_cc_coverage": attr.label(
146+
default = "//build/cc_coverage:collect_cc_coverage",
147+
executable = True,
148+
cfg = config.exec(exec_group = "test"),
149+
),
150+
# The test binary to run
151+
"binary": attr.label(
152+
allow_single_file = True,
153+
executable = True,
154+
cfg = "target",
155+
mandatory = True,
156+
),
157+
# Additional data files needed by the test
158+
"data": attr.label_list(allow_files = True),
159+
# Environment variables to set when running the test
160+
"env": attr.string_dict(default = {}),
161+
# Reference to Windows platform for cross-platform support
162+
"_platforms_os_windows": attr.label(default = "@platforms//os:windows"),
163+
},
164+
)

build/wd_test.bzl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ set -e
141141
142142
# Set up coverage for workerd subprocess
143143
if [ -n "$COVERAGE_DIR" ]; then
144+
# Fix directory permissions for coverage post-processing
145+
# (Bazel may create COVERAGE_DIR with read-only permissions)
146+
chmod -R u+w "$COVERAGE_DIR" 2>/dev/null || true
144147
export LLVM_PROFILE_FILE="$COVERAGE_DIR/%p.profraw"
145148
export KJ_CLEAN_SHUTDOWN=1
146149
fi
@@ -280,7 +283,7 @@ _wd_test = rule(
280283
cfg = config.exec(exec_group = "test"),
281284
),
282285
"_collect_cc_coverage": attr.label(
283-
default = "@bazel_tools//tools/test:collect_cc_coverage",
286+
default = "//build/cc_coverage:collect_cc_coverage",
284287
executable = True,
285288
cfg = config.exec(exec_group = "test"),
286289
),

0 commit comments

Comments
 (0)