Skip to content

Commit 3b5ca71

Browse files
authored
feat: print a report (#2)
* print a report * add asciinema * update test * fix
1 parent 65e39df commit 3b5ca71

File tree

8 files changed

+106
-21
lines changed

8 files changed

+106
-21
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
uses: jaxxstorm/action-install-gh-release@v2.1.0
2727
with:
2828
repo: aspect-build/aspect-cli
29-
tag: 2025.40-21-g705e63e
29+
tag: 2025.42.9
3030
asset-name: aspect-cli
3131
platform: unknown_linux
3232
arch: x86_64

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@ Replacement for the 'bazel coverage' command:
44
- forget having a Bazel-specific, Java-based, unmaintained LCOV merger and 'combined report' - that never should have been Bazel's job
55
- developers really need git-aware diff to show "incremental coverage" - which of the lines I added or modified are covered by tests?
66
- ability to add other enforcement thresholds
7+
- clean up the coverage .dat files in the output tree after reporting
8+
9+
## Demo
10+
11+
[![asciicast](https://asciinema.org/a/pQCJWxWxONi01bBZcdRgLxEHx.svg)](https://asciinema.org/a/pQCJWxWxONi01bBZcdRgLxEHx)

coverage.axl

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,53 @@ Implement a 'coverage' task that wraps a test command.
44
Gathers the coverage.dat files to power common developer workflows.
55
"""
66

7+
load("./runnable.axl", "runnable")
8+
79
# buildifier: disable=function-docstring
810
def impl(ctx) -> int:
911
out = ctx.std.io.stdout
1012
err = ctx.std.io.stderr
11-
build = ctx.bazel.build(
13+
14+
lcov_build = ctx.bazel.build(
15+
"@lcov",
16+
events = True,
17+
bazel_flags = ["--build_runfile_links"],
18+
)
19+
20+
lcov = runnable(ctx)
21+
for event in lcov_build.events():
22+
lcov.event(event)
23+
24+
test = ctx.bazel.build(
1225
"//...",
1326
events = True,
1427
bazel_flags = [
1528
"--isatty=" + str(int(out.is_tty)),
1629
"--collect_code_coverage",
1730
],
1831
bazel_verb = "test"
19-
);
32+
)
2033

2134
coverage_dat_files = []
22-
for event in build.events():
35+
for event in test.events():
2336
if event.kind == "test_result":
24-
coverage_dat_files.extend([f.file.removeprefix("file://") for f in event.payload.test_action_output if f.file.endswith("coverage.dat")])
37+
coverage_dat_files.extend([
38+
f.file.removeprefix("file://")
39+
for f in event.payload.test_action_output
40+
if f.file.endswith("coverage.dat")
41+
])
2542
# TODO: populate a GITHUB_OUTPUT variable so we can use https://github.com/codecov/codecov-action
2643
if event.kind == "progress":
2744
out.write(event.payload.stdout)
2845
err.write(event.payload.stderr)
29-
30-
if event.kind == "build_finished_id":
31-
for file in coverage_dat_files:
32-
out.write(file + "\n")
33-
out.write(ctx.std.fs.read_to_string(file) + "\n")
3446

35-
build.wait()
47+
test.wait()
48+
3649
# TODO: get the delta of changed files from VCS, and render 'incremental' coverage as the default presentation
50+
exit = lcov.spawn(ctx, ["--list"] + coverage_dat_files).wait()
51+
52+
if not exit.success:
53+
err.write("\x1b[0;31mERROR\x1b[0m: process exited with code %d\n" % exit.code)
3754

3855
# Async: Cleanup disk space consumed by the .dat files.
3956
ctx.std.process.command("rm").arg("-f").args(coverage_dat_files).spawn()

example/.aspect/runnable.axl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../runnable.axl

example/MODULE.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
bazel_dep(name = "aspect_rules_py", version = "1.6.3")
22
bazel_dep(name = "rules_python", version = "1.6.3")
3+
bazel_dep(name = "lcov", version = "2.3.2")
34

45
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
56
python.toolchain(

example/MODULE.bazel.lock

Lines changed: 13 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,8 @@ It should locate the coverage.dat files produced by the Python coverage library.
1313
output="$(aspect coverage //src:my_test)"
1414

1515
# Verify that it produces the expected output - half the functions are covered
16-
echo "${output}" | grep -q "FNF:2" || {
17-
echo >&2 "Functions Found should be 2 '${output}'"
18-
exit 1
19-
}
20-
echo "${output}" | grep -q "FNH:1" || {
21-
echo >&2 "Functions Hit should be 1 '${output}'"
16+
echo "${output}" | grep -q "50.0%" || {
17+
echo >&2 "Functions should be 50% covered: '${output}'"
2218
exit 1
2319
}
2420
~~~

runnable.axl

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Copied from https://github.com/aspect-extensions/run/blob/main/runnable.axl
2+
TODO(alexeagle): depend on that AXL module instead
3+
"""
4+
def _process_bes(state: struct, event: bazel.build.build_event.BuildEvent):
5+
filesets = state.filesets
6+
target_fileset = state.target_fileset
7+
target = state.target
8+
if event.kind == "named_set_of_files":
9+
filesets[event.id.id] = event.payload.files
10+
elif event.kind == "target_completed":
11+
if event.id.label == target:
12+
for og in event.payload.output_group:
13+
if og.name == "default":
14+
target_fileset[event.id.label] = og.file_sets
15+
16+
def _determine_entrypoint(state: struct) -> str | None:
17+
filesets = state.filesets
18+
target_fileset = state.target_fileset
19+
if state.target not in target_fileset:
20+
return None
21+
targetfs = target_fileset[state.target][0].id
22+
files = filesets[targetfs]
23+
entrypoint = None
24+
for file in files:
25+
if len(file.path_prefix):
26+
entrypoint = file.file.removeprefix("file://")
27+
break
28+
return entrypoint
29+
30+
def _spawn(ctx: task_context, state: struct, args: list[str]) -> std.process.child:
31+
entrypoint = _determine_entrypoint(state)
32+
runfiles = entrypoint + ".runfiles"
33+
return ctx.std.process.command(entrypoint)\
34+
.current_dir(runfiles) \
35+
.env("RUNFILES_DIR", runfiles) \
36+
.env("JAVA_RUNFILES", runfiles) \
37+
.env("RUNFILES_MANIFEST_FILE", runfiles + "_manifest") \
38+
.env("BUILD_WORKSPACE_DIRECTORY", "TODO") \
39+
.env("BUILD_WORKING_DIRECTORY", ctx.std.env.current_dir()) \
40+
.args(args) \
41+
.spawn()
42+
43+
def runnable(ctx) -> struct:
44+
state = struct(
45+
ctx = ctx,
46+
target = "@@lcov+//:lcov", # TODO(alexeagle): we can assume there's only one target
47+
filesets = {},
48+
target_fileset = {}
49+
)
50+
51+
return struct(
52+
event = lambda event: _process_bes(state, event),
53+
spawn = lambda ctx, args: _spawn(ctx, state, args),
54+
)
55+
56+
spawn = _spawn

0 commit comments

Comments
 (0)