Skip to content

Commit a92d8e6

Browse files
Kover integration [kotlin part]
This diff contains logic to integrate with Kover for code coverage, using Kover JVM agent and disabling JaCoCo instrumentation, which avoid having to re-compile application code. It used from both JVM and Android kotlin tests. How to use? Supply the version of Kover agent via toolchain (typically from jvm_rules_extrenal), and enable Kover (separate diff). Then run bazel coverage //your/kotlin/test_target. Output files are created in working module directory (along test/library explicity outputs). Please note : Notes : Because Bazel test/coverage are 'terminal' and actions or aspects can't reuse the output of these, the generation of the report is done outside bazel (typically from Bazel wrapper). The logic here will generate both raw output (*.ic file) and a metadata file ready to provide to Kover CLI, so that one can generate report simply by running : java -jar kover-cli.jar report @path_to_metadat_file <options> We could possibly generate the report by hijacking test runner shell script template and injecting this command to executed after tests are run. This is rather hacky and is likely to require changes to Bazel project. For mixed sourceset, disabling JaCoCo instrumenation is required. To do this properly, one should add an extra parameter to java_common.compile() API, which require modifying both rules_java and Bazel core. For now, we disabled JaCoCo instrumentation accross the board, you will need to cherry-pick this PR uber-common/bazel@cb9f6f0 Code in kt_android_local_test_impl.bzl needs to be kept in sync with rules_android. There is ongoing conversation with google to simply of to extend rules_android, and override pipeline's behavior without duplicating their code, we should be able to simplify this soon. fix typo
1 parent 03c3521 commit a92d8e6

File tree

5 files changed

+438
-11
lines changed

5 files changed

+438
-11
lines changed

kotlin/internal/jvm/compile.bzl

+4-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ load(
5555
"@bazel_skylib//rules:common_settings.bzl",
5656
"BuildSettingInfo",
5757
)
58+
load("//kotlin/internal/jvm:kover.bzl",
59+
_is_kover_enabled = "is_kover_enabled"
60+
)
5861

5962
# UTILITY ##############################################################################################################
6063

@@ -525,7 +528,7 @@ def _run_kt_builder_action(
525528
args.add_all("--source_jars", srcs.src_jars + generated_src_jars, omit_if_empty = True)
526529
args.add_all("--deps_artifacts", deps_artifacts, omit_if_empty = True)
527530
args.add_all("--kotlin_friend_paths", associates.jars, map_each = _associate_utils.flatten_jars)
528-
args.add("--instrument_coverage", ctx.coverage_instrumented())
531+
args.add("--instrument_coverage", ctx.coverage_instrumented() and not _is_kover_enabled(ctx))
529532
args.add("--track_class_usage", toolchains.kt.experimental_track_class_usage)
530533
args.add("--track_resource_usage", toolchains.kt.experimental_track_resource_usage)
531534
if ksp_opts:

kotlin/internal/jvm/impl.bzl

+35-5
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ load(
3131
"//kotlin/internal/utils:utils.bzl",
3232
_utils = "utils",
3333
)
34+
load("//kotlin/internal/jvm:kover.bzl",
35+
_is_kover_enabled = "is_kover_enabled",
36+
_get_kover_agent_files = "get_kover_agent_file",
37+
_create_kover_agent_actions = "create_kover_agent_actions",
38+
_create_kover_metadata_action = "create_kover_metadata_action",
39+
_get_kover_jvm_flags = "get_kover_jvm_flags",
40+
)
3441
load("//third_party:jarjar.bzl", "jarjar_action")
3542

3643
# borrowed from skylib to avoid adding that to the release.
@@ -80,7 +87,7 @@ def _write_launcher_action(ctx, rjars, main_class, jvm_flags):
8087
if java_runtime.version >= 17:
8188
jvm_flags = jvm_flags + " -Djava.security.manager=allow"
8289

83-
if ctx.configuration.coverage_enabled:
90+
if ctx.configuration.coverage_enabled and not _is_kover_enabled(ctx):
8491
jacocorunner = ctx.toolchains[_TOOLCHAIN_TYPE].jacocorunner
8592
classpath = ctx.configuration.host_path_separator.join(
8693
["${RUNPATH}%s" % (j.short_path) for j in rjars.to_list() + jacocorunner.files.to_list()],
@@ -276,12 +283,30 @@ _SPLIT_STRINGS = [
276283

277284
def kt_jvm_junit_test_impl(ctx):
278285
providers = _kt_jvm_produce_jar_actions(ctx, "kt_jvm_test")
279-
runtime_jars = depset(ctx.files._bazel_test_runner, transitive = [providers.java.transitive_runtime_jars])
280286

281287
coverage_runfiles = []
288+
coverage_inputs = []
289+
coverage_jvm_flags = []
290+
282291
if ctx.configuration.coverage_enabled:
283-
jacocorunner = ctx.toolchains[_TOOLCHAIN_TYPE].jacocorunner
284-
coverage_runfiles = jacocorunner.files.to_list()
292+
if _is_kover_enabled(ctx):
293+
kover_agent_files = _get_kover_agent_files(ctx)
294+
kover_output_file, kover_args_file = _create_kover_agent_actions(ctx, ctx.attr.name)
295+
kover_output_metadata_file = _create_kover_metadata_action(
296+
ctx,
297+
ctx.attr.name,
298+
ctx.attr.deps + ctx.attr.associates,
299+
kover_output_file
300+
)
301+
flags = _get_kover_jvm_flags(kover_agent_files, kover_args_file)
302+
303+
# add Kover agent jvm_flag, inputs and outputs
304+
coverage_jvm_flags = [flags]
305+
coverage_inputs = [depset(kover_agent_files)]
306+
coverage_runfiles = [kover_args_file, kover_output_metadata_file]
307+
else:
308+
jacocorunner = ctx.toolchains[_TOOLCHAIN_TYPE].jacocorunner
309+
coverage_runfiles = jacocorunner.files.to_list()
285310

286311
test_class = ctx.attr.test_class
287312

@@ -299,8 +324,13 @@ def kt_jvm_junit_test_impl(ctx):
299324
jvm_flags = []
300325
if hasattr(ctx.fragments.java, "default_jvm_opts"):
301326
jvm_flags = ctx.fragments.java.default_jvm_opts
327+
jvm_flags.extend(coverage_jvm_flags + ctx.attr.jvm_flags)
328+
329+
runtime_jars = depset(
330+
ctx.files._bazel_test_runner,
331+
transitive = [providers.java.transitive_runtime_jars] + coverage_inputs
332+
)
302333

303-
jvm_flags.extend(ctx.attr.jvm_flags)
304334
coverage_metadata = _write_launcher_action(
305335
ctx,
306336
runtime_jars,

kotlin/internal/jvm/kover.bzl

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Copyright 2018 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# This file contains logic to integrate with Kover for code coverage, using
16+
# Kover JVM agent and disabling JaCoCo instrumentation, which avoid having to
17+
# re-compile application code. It used from both JVM and Android kotlin tests.
18+
#
19+
#
20+
# How to use?
21+
#
22+
# Supply the version of Kover agent via toolchain (typically from jvm_rules_extrenal),
23+
# and enable Kover. Then run `bazel coverage //your/kotlin/test_target`. Output files
24+
# are created in working module directory (along test/library explicity outputs).
25+
#
26+
#
27+
# Notes :
28+
#
29+
# 1. Because Bazel test/coverage are 'terminal' and actions or aspects can't reuse the output
30+
# of these, the generation of the report is done outside bazel (typically
31+
# from Bazel wrapper). The logic here will generate both raw output (*.ic file) and
32+
# a metadata file ready to provide to Kover CLI, so that one can generate report simply by
33+
# running : `java -jar kover-cli.jar report @path_to_metadat_file <options>`
34+
#
35+
# We could possibly generate the report by hijacking test runner shell script template
36+
# and injecting this command to executed after tests are run. This is rather hacky
37+
# and is likely to require changes to Bazel project.
38+
#
39+
# 2. For mixed sourceset, disabling JaCoCo instrumenation is required. To do this properly,
40+
# one should add an extra parameter to java_common.compile() API, which require modifying both
41+
# rules_java and Bazel core. For now, we disabled JaCoCo instrumentation accross the board,
42+
# you will need to cherry-pick this PR https://github.com/uber-common/bazel/commit/cb9f6f042c64af96bbd77e21fe6fb75936c74f47
43+
#
44+
# 3. Code in `kt_android_local_test_impl.bzl` needs to be kept in sync with rules_android. There is ongoing
45+
# conversation with google to simply of to extend rules_android, and override pipeline's behavior without
46+
# duplicating their code, we should be able to simplify this soon.
47+
#
48+
49+
load(
50+
"//kotlin/internal:defs.bzl",
51+
_KtJvmInfo = "KtJvmInfo",
52+
_TOOLCHAIN_TYPE = "TOOLCHAIN_TYPE",
53+
)
54+
load("@bazel_skylib//lib:paths.bzl",
55+
_paths = "paths",
56+
)
57+
58+
def is_kover_enabled(ctx):
59+
return ctx.toolchains[_TOOLCHAIN_TYPE].experimental_kover_enabled
60+
61+
def get_kover_agent_file(ctx):
62+
""" Get the Kover agent runtime files, extracted from toolchain.
63+
64+
returns:
65+
the Kover agent runtime files
66+
"""
67+
68+
kover_agent = ctx.toolchains[_TOOLCHAIN_TYPE].experimental_kover_agent
69+
if not kover_agent:
70+
fail("Kover agent wasn't specified in toolchain.")
71+
72+
kover_agent_info = kover_agent[DefaultInfo]
73+
return kover_agent_info.files.to_list()
74+
75+
def get_kover_jvm_flags(kover_agent_files, kover_args_file):
76+
""" Compute the jvm flag used to setup Kover agent.
77+
78+
returns:
79+
the flag string to be used by test runner jvm
80+
"""
81+
82+
return "-javaagent:%s=%s" % (kover_agent_files[0].short_path, kover_args_file.short_path)
83+
84+
def create_kover_agent_actions(ctx, name):
85+
""" Generate the actions needed to emit Kover code coverage metadata file. It creates
86+
the properly populated arguments input file needed by Kover agent.
87+
88+
returns:
89+
the kover metadata output file.
90+
the kover arguments file.
91+
"""
92+
93+
# declare code coverage raw data binary output file
94+
binary_output_name = "%s-kover_report.ic" % name
95+
kover_output_file = ctx.actions.declare_file(binary_output_name)
96+
97+
# Hack: there is curently no way to indicate this file will be created Kover agent
98+
ctx.actions.run_shell(
99+
outputs = [kover_output_file],
100+
command = "touch {}".format(kover_output_file.path),
101+
)
102+
103+
# declare args file - https://kotlin.github.io/kotlinx-kover/jvm-agent/#kover-jvm-arguments-file
104+
kover_args_file = ctx.actions.declare_file(
105+
"%s-kover.args.txt" % name,
106+
)
107+
108+
# The format specified in official documentation is failing, we follow instead the format
109+
# described in the provided error output (see below).
110+
#
111+
# Failed to parse agent arguments: java.lang.IllegalArgumentException: At least 5 arguments expected but 1 found.
112+
# Expected arguments are:
113+
# 0) data file to save coverage result
114+
# 1) a flag to enable tracking per test coverage
115+
# 2) a flag to calculate coverage for unloaded classes
116+
# 3) a flag to use data file as initial coverage, also use it if several parallel processes are to write into one file
117+
# 4) a flag to run line coverage or branch coverage otherwise
118+
119+
ctx.actions.write(kover_args_file, "\n".join([
120+
"../../%s" % binary_output_name, # Kotlin compiler runs in runfiles folder, make sure file is created is correct location
121+
"true",
122+
"false",
123+
"true",
124+
"true"
125+
]))
126+
127+
return kover_output_file, kover_args_file
128+
129+
130+
def create_kover_metadata_action(
131+
ctx,
132+
name,
133+
deps,
134+
kover_output_file):
135+
""" Generate kover metadata file needed for invoking kover CLI to generate report.
136+
More info at: https://kotlin.github.io/kotlinx-kover/cli/
137+
138+
returns:
139+
the kover output metadata file.
140+
"""
141+
142+
metadata_output_name = "%s-kover_metadata.txt" % name
143+
kover_output_metadata_file = ctx.actions.declare_file(metadata_output_name)
144+
145+
srcs = []
146+
classfiles = []
147+
excludes = []
148+
149+
for dep in deps:
150+
if dep.label.package != ctx.label.package:
151+
continue
152+
153+
if InstrumentedFilesInfo in dep:
154+
for src in dep[InstrumentedFilesInfo].instrumented_files.to_list():
155+
if src.short_path.startswith(ctx.label.package + "/"):
156+
path = _paths.dirname(src.short_path)
157+
if path not in srcs:
158+
srcs.extend(["--src", path])
159+
160+
if JavaInfo in dep:
161+
for classfile in dep[JavaInfo].transitive_runtime_jars.to_list():
162+
if classfile.short_path.startswith(ctx.label.package + "/"):
163+
if classfile.path not in classfiles:
164+
classfiles.extend(["--classfiles", classfile.path])
165+
166+
for exclude in ctx.toolchains[_TOOLCHAIN_TYPE].experimental_kover_exclude:
167+
excludes.extend(["--exclude", exclude])
168+
169+
for exclude_annotation in ctx.toolchains[_TOOLCHAIN_TYPE].experimental_kover_exclude_annotation:
170+
excludes.extend(["--excludeAnnotation", exclude_annotation])
171+
172+
ctx.actions.write(kover_output_metadata_file, "\n".join([
173+
"report",
174+
kover_output_file.path,
175+
"--title",
176+
"Code-Coverage Analysis: %s" % ctx.label,
177+
] + srcs + classfiles + excludes))
178+
179+
return kover_output_metadata_file

0 commit comments

Comments
 (0)