Skip to content

Commit

Permalink
Kover integration [kotlin part]
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
oliviernotteghem committed Oct 23, 2024
1 parent 03c3521 commit 9d84887
Show file tree
Hide file tree
Showing 5 changed files with 423 additions and 11 deletions.
5 changes: 4 additions & 1 deletion kotlin/internal/jvm/compile.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ load(
"@bazel_skylib//rules:common_settings.bzl",
"BuildSettingInfo",
)
load("//kotlin/internal/jvm:kover.bzl",
_is_kover_enabled = "is_kover_enabled"
)

# UTILITY ##############################################################################################################

Expand Down Expand Up @@ -525,7 +528,7 @@ def _run_kt_builder_action(
args.add_all("--source_jars", srcs.src_jars + generated_src_jars, omit_if_empty = True)
args.add_all("--deps_artifacts", deps_artifacts, omit_if_empty = True)
args.add_all("--kotlin_friend_paths", associates.jars, map_each = _associate_utils.flatten_jars)
args.add("--instrument_coverage", ctx.coverage_instrumented())
args.add("--instrument_coverage", ctx.coverage_instrumented() and not _is_kover_enabled(ctx))
args.add("--track_class_usage", toolchains.kt.experimental_track_class_usage)
args.add("--track_resource_usage", toolchains.kt.experimental_track_resource_usage)
if ksp_opts:
Expand Down
40 changes: 35 additions & 5 deletions kotlin/internal/jvm/impl.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ load(
"//kotlin/internal/utils:utils.bzl",
_utils = "utils",
)
load("//kotlin/internal/jvm:kover.bzl",
_is_kover_enabled = "is_kover_enabled",
_get_kover_agent_files = "get_kover_agent_file",
_create_kover_agent_actions = "create_kover_agent_actions",
_create_kover_metadata_action = "create_kover_metadata_action",
_get_kover_jvm_flags = "get_kover_jvm_flags",
)
load("//third_party:jarjar.bzl", "jarjar_action")

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

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

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

coverage_runfiles = []
coverage_inputs = []
coverage_jvm_flags = []

if ctx.configuration.coverage_enabled:
jacocorunner = ctx.toolchains[_TOOLCHAIN_TYPE].jacocorunner
coverage_runfiles = jacocorunner.files.to_list()
if _is_kover_enabled(ctx):
kover_agent_files = _get_kover_agent_files(ctx)
kover_output_file, kover_args_file = _create_kover_agent_actions(ctx, ctx.attr.name)
kover_output_metadata_file = _create_kover_metadata_action(
ctx,
ctx.attr.name,
ctx.attr.deps + ctx.attr.associates,
kover_output_file
)
flags = _get_kover_jvm_flags(kover_agent_files, kover_args_file)

# add Kover agent jvm_flag, inputs and outputs
coverage_jvm_flags = [flags]
coverage_inputs = [depset(kover_agent_files)]
coverage_runfiles = [kover_args_file, kover_output_metadata_file]
else:
jacocorunner = ctx.toolchains[_TOOLCHAIN_TYPE].jacocorunner
coverage_runfiles = jacocorunner.files.to_list()

test_class = ctx.attr.test_class

Expand All @@ -299,8 +324,13 @@ def kt_jvm_junit_test_impl(ctx):
jvm_flags = []
if hasattr(ctx.fragments.java, "default_jvm_opts"):
jvm_flags = ctx.fragments.java.default_jvm_opts
jvm_flags.extend(coverage_jvm_flags + ctx.attr.jvm_flags)

runtime_jars = depset(
ctx.files._bazel_test_runner,
transitive = [providers.java.transitive_runtime_jars] + coverage_inputs
)

jvm_flags.extend(ctx.attr.jvm_flags)
coverage_metadata = _write_launcher_action(
ctx,
runtime_jars,
Expand Down
164 changes: 164 additions & 0 deletions kotlin/internal/jvm/kover.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Copyright 2018 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This file 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. Then run `bazel coverage //your/kotlin/test_target`. Output files
# are created in working module directory (along test/library explicity outputs).
#
#
# Notes :
#
# 1. 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.
#
# 2. 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 https://github.com/uber-common/bazel/commit/cb9f6f042c64af96bbd77e21fe6fb75936c74f47
#
# 3. 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.
#

load(
"//kotlin/internal:defs.bzl",
_KtJvmInfo = "KtJvmInfo",
_TOOLCHAIN_TYPE = "TOOLCHAIN_TYPE",
)
load("@bazel_skylib//lib:paths.bzl",
_paths = "paths",
)

def is_kover_enabled(ctx):
return ctx.toolchains[_TOOLCHAIN_TYPE].experimental_kover_enabled

def get_kover_agent_file(ctx):
""" Get the Kover agent runtime files, extracted from toolchain.
returns:
the Kover agent runtime files
"""

kover_agent = ctx.toolchains[_TOOLCHAIN_TYPE].experimental_kover_agent
if not kover_agent:
fail("Kover agent wasn't specified in toolchain.")

kover_agent_info = kover_agent[DefaultInfo]
return kover_agent_info.files.to_list()

def get_kover_jvm_flags(kover_agent_files, kover_args_file):
""" Compute the jvm flag used to setup Kover agent.
returns:
the flag string to be used by test runner jvm
"""

return "-javaagent:%s=file:%s" % (kover_agent_files[0].short_path, kover_args_file.short_path)

def create_kover_agent_actions(ctx, name):
""" Generate the actions needed to emit Kover code coverage metadata file. It creates
the properly populated arguments input file needed by Kover agent.
returns:
the kover metadata output file.
the kover arguments file.
"""

# declare code coverage raw data binary output file
binary_output_name = "%s-kover_report.ic" % name
kover_output_file = ctx.actions.declare_file(binary_output_name)

# Hack: there is curently no way to indicate this file will be created Kover agent
ctx.actions.run_shell(
outputs = [kover_output_file],
command = "touch {}".format(kover_output_file.path),
)

# declare args file - https://kotlin.github.io/kotlinx-kover/jvm-agent/#kover-jvm-arguments-file
kover_args_file = ctx.actions.declare_file(
"%s-kover.args.txt" % name,
)
ctx.actions.write(
kover_args_file,
"report.file=../../%s" % binary_output_name # Kotlin compiler runs in runfiles folder, make sure file is created is correct location
)

return kover_output_file, kover_args_file


def create_kover_metadata_action(
ctx,
name,
deps,
kover_output_file):
""" Generate kover metadata file needed for invoking kover CLI to generate report.
More info at: https://kotlin.github.io/kotlinx-kover/cli/
returns:
the kover output metadata file.
"""

metadata_output_name = "%s-kover_metadata.txt" % name
kover_output_metadata_file = ctx.actions.declare_file(metadata_output_name)

srcs = []
classfiles = []
excludes = []

for dep in deps:
if dep.label.package != ctx.label.package:
continue

if InstrumentedFilesInfo in dep:
for src in dep[InstrumentedFilesInfo].instrumented_files.to_list():
if src.short_path.startswith(ctx.label.package + "/"):
path = _paths.dirname(src.short_path)
if path not in srcs:
srcs.extend(["--src", path])

if JavaInfo in dep:
for classfile in dep[JavaInfo].transitive_runtime_jars.to_list():
if classfile.short_path.startswith(ctx.label.package + "/"):
if classfile.path not in classfiles:
classfiles.extend(["--classfiles", classfile.path])

for exclude in ctx.toolchains[_TOOLCHAIN_TYPE].experimental_kover_exclude:
excludes.extend(["--exclude", exclude])

for exclude_annotation in ctx.toolchains[_TOOLCHAIN_TYPE].experimental_kover_exclude_annotation:
excludes.extend(["--excludeAnnotation", exclude_annotation])

ctx.actions.write(kover_output_metadata_file, "\n".join([
"report",
kover_output_file.path,
"--title",
"Code-Coverage Analysis: %s" % ctx.label,
] + srcs + classfiles + excludes))

return kover_output_metadata_file
Loading

0 comments on commit 9d84887

Please sign in to comment.