add support for code coverage
diff --git a/kotlin/builder/BUILD b/kotlin/builder/BUILD
index 7a104a5..0c8703b 100644
--- a/kotlin/builder/BUILD
+++ b/kotlin/builder/BUILD
@@ -21,6 +21,12 @@
     "@com_github_jetbrains_kotlin//:kotlin-script-runtime",
 ]
 
+ASM_DEPS = [
+    "@io_bazel_rules_kotlin_org_ow2_asm_asm_commons//jar",
+    "@io_bazel_rules_kotlin_org_ow2_asm_asm//jar",
+    "@io_bazel_rules_kotlin_org_ow2_asm_asm_tree//jar",
+]
+
 # Common depset for the builder.
 COMMON_DEPS = [
     "//kotlin/builder/proto:deps",
@@ -38,6 +44,7 @@
     "@com_github_jetbrains_kotlin//:kotlin-stdlib",
     "@com_github_jetbrains_kotlin//:kotlin-stdlib-jdk7",
     "@com_github_jetbrains_kotlin//:kotlin-stdlib-jdk8",
+    "@io_bazel_rules_kotlin_org_jacoco_org_jacoco_core//jar",
 ]
 
 # The compiler library, this is co-located in the kotlin compiler classloader.
@@ -73,8 +80,9 @@
     name = "builder",
     main_class = "io.bazel.kotlin.builder.KotlinBuilder",
     visibility = ["//visibility:public"],
-    runtime_deps = [
+    runtime_deps = ASM_DEPS + [
         ":builder_lib",
+        "@com_github_jetbrains_kotlin//:kotlin-reflect",
     ],
     data = [
         ":compiler_lib.jar"
@@ -115,6 +123,7 @@
         "//third_party/jvm/com/google/truth",
         "//third_party/jvm/junit",
     ],
+    runtime_deps = ASM_DEPS
 )
 
 java_test(
diff --git a/kotlin/builder/integrationtests/KotlinBuilderActionTests.java b/kotlin/builder/integrationtests/KotlinBuilderActionTests.java
index a26c1a1..bc08407 100644
--- a/kotlin/builder/integrationtests/KotlinBuilderActionTests.java
+++ b/kotlin/builder/integrationtests/KotlinBuilderActionTests.java
@@ -1,5 +1,6 @@
 package io.bazel.kotlin.builder;
 
+import io.bazel.kotlin.builder.mode.jvm.actions.JacocoProcessor;
 import io.bazel.kotlin.builder.mode.jvm.actions.KotlinCompiler;
 import org.junit.Test;
 
@@ -11,4 +12,14 @@
     assertFileExists(DirectoryType.CLASSES, "something/AClass.class");
     assertFileDoesNotExist(outputs().getJar());
   }
+
+  @Test
+  public void testCoverage() {
+    addSource("AClass.kt", "package something;" + "class AClass{}");
+    instance(KotlinCompiler.class).compile(builderCommand());
+    instance(JacocoProcessor.class).instrument(builderCommand());
+    assertFileExists(DirectoryType.CLASSES, "something/AClass.class");
+    assertFileExists(DirectoryType.CLASSES, "something/AClass.class.uninstrumented");
+    assertFileDoesNotExist(outputs().getJar());
+  }
 }
diff --git a/kotlin/builder/integrationtests/KotlinBuilderTestCase.java b/kotlin/builder/integrationtests/KotlinBuilderTestCase.java
index f1cc005..015310c 100644
--- a/kotlin/builder/integrationtests/KotlinBuilderTestCase.java
+++ b/kotlin/builder/integrationtests/KotlinBuilderTestCase.java
@@ -29,6 +29,10 @@
   private String label = null;
   private Path inputSourceDir = null;
 
+  protected void setPostProcessor(String postProcessor) {
+    builder.setInfo(builder.getInfo().toBuilder().setPostProcessor(postProcessor));
+  }
+
   @Before
   public void setupNext() {
     resetTestContext("a_test_" + counter.incrementAndGet());
@@ -159,6 +163,11 @@
     assertFileExists(file.toString());
   }
 
+  void assertFileDoesNotExist(DirectoryType dir, String filePath) {
+    Path file = DirectoryType.select(dir, builderCommand()).resolve(filePath);
+    assertFileDoesNotExist(file.toString());
+  }
+
   void assertFileDoesNotExist(String filePath) {
     assertWithMessage("file exisst: " + filePath).that(new File(filePath).exists()).isFalse();
   }
diff --git a/kotlin/builder/integrationtests/KotlinBuilderTests.java b/kotlin/builder/integrationtests/KotlinBuilderTests.java
index cb5aa62..053abe0 100644
--- a/kotlin/builder/integrationtests/KotlinBuilderTests.java
+++ b/kotlin/builder/integrationtests/KotlinBuilderTests.java
@@ -21,6 +21,7 @@
     addSource("AClass.kt", "package something;" + "class AClass{}");
     runCompileTask();
     assertFileExists(DirectoryType.CLASSES, "something/AClass.class");
+    assertFileDoesNotExist(DirectoryType.CLASSES, "something/AClass.class.uninstrumented");
   }
 
   @Test
@@ -33,6 +34,15 @@
     assertFileExists(outputs().getJar());
   }
 
+  @Test
+  public void testCoverage() {
+    setPostProcessor("jacoco");
+    addSource("AClass.kt", "package something;" + "class AClass{}");
+    runCompileTask();
+    assertFileExists(DirectoryType.CLASSES, "something/AClass.class");
+    assertFileExists(DirectoryType.CLASSES, "something/AClass.class.uninstrumented");
+  }
+
   private void runCompileTask() {
     KotlinModel.BuilderCommand command = builderCommand();
     for (DirectoryType directoryType : DirectoryType.values()) {
diff --git a/kotlin/builder/proto/BUILD b/kotlin/builder/proto/BUILD
index 8bc369b..4682b52 100644
--- a/kotlin/builder/proto/BUILD
+++ b/kotlin/builder/proto/BUILD
@@ -35,7 +35,7 @@
 #    name="%s_java_proto" % lib,
 #    deps=["%s_proto" % lib],
 #    ) for lib in _PROTO_LIBS]
-#
+
 
 java_import(
     name = "deps",
diff --git a/kotlin/builder/proto/jars/libkotlin_model_proto-speed.jar b/kotlin/builder/proto/jars/libkotlin_model_proto-speed.jar
index 943c343..d04c1b2 100755
--- a/kotlin/builder/proto/jars/libkotlin_model_proto-speed.jar
+++ b/kotlin/builder/proto/jars/libkotlin_model_proto-speed.jar
Binary files differ
diff --git a/kotlin/builder/proto/kotlin_model.proto b/kotlin/builder/proto/kotlin_model.proto
index d1e4ced..22760fd 100644
--- a/kotlin/builder/proto/kotlin_model.proto
+++ b/kotlin/builder/proto/kotlin_model.proto
@@ -87,6 +87,8 @@
 
         // Jars that the kotlin compiler will allow package private access to.
         repeated string friend_paths = 10;
+
+        string post_processor = 11;
     }
 
     // Directories used by the builder.
diff --git a/kotlin/builder/src/io/bazel/kotlin/builder/BuildCommandBuilder.kt b/kotlin/builder/src/io/bazel/kotlin/builder/BuildCommandBuilder.kt
index 54302c1..335cd02 100644
--- a/kotlin/builder/src/io/bazel/kotlin/builder/BuildCommandBuilder.kt
+++ b/kotlin/builder/src/io/bazel/kotlin/builder/BuildCommandBuilder.kt
@@ -123,6 +123,7 @@
                 }
                 passthroughFlags = argMap.optionalSingle("--kotlin_passthrough_flags")
                 addAllFriendPaths(argMap.mandatory("--kotlin_friend_paths"))
+                postProcessor = argMap.optionalSingle("--post_processor") ?: ""
                 toolchainInfoBuilder.commonBuilder.apiVersion = argMap.mandatorySingle("--kotlin_api_version")
                 toolchainInfoBuilder.commonBuilder.languageVersion = argMap.mandatorySingle("--kotlin_language_version")
                 toolchainInfoBuilder.jvmBuilder.jvmTarget = argMap.mandatorySingle("--kotlin_jvm_target")
diff --git a/kotlin/builder/src/io/bazel/kotlin/builder/mode/jvm/KotlinJvmCompilationExecutor.kt b/kotlin/builder/src/io/bazel/kotlin/builder/mode/jvm/KotlinJvmCompilationExecutor.kt
index db1b42d..8f1f7d0 100644
--- a/kotlin/builder/src/io/bazel/kotlin/builder/mode/jvm/KotlinJvmCompilationExecutor.kt
+++ b/kotlin/builder/src/io/bazel/kotlin/builder/mode/jvm/KotlinJvmCompilationExecutor.kt
@@ -26,6 +26,7 @@
 import io.bazel.kotlin.builder.mode.jvm.actions.JavaCompiler
 import io.bazel.kotlin.builder.mode.jvm.actions.KotlinCompiler
 import io.bazel.kotlin.builder.mode.jvm.actions.OutputJarCreator
+import io.bazel.kotlin.builder.mode.jvm.actions.JacocoProcessor
 import io.bazel.kotlin.builder.mode.jvm.utils.KotlinCompilerOutputSink
 import io.bazel.kotlin.model.KotlinModel.BuilderCommand
 import java.io.File
@@ -45,7 +46,8 @@
     private val outputSink: KotlinCompilerOutputSink,
     private val javaCompiler: JavaCompiler,
     private val jDepsGenerator: JDepsGenerator,
-    private val outputJarCreator: OutputJarCreator
+    private val outputJarCreator: OutputJarCreator,
+    private val jacocoProcessor: JacocoProcessor
 ) : KotlinJvmCompilationExecutor {
     override fun compile(command: BuilderCommand): Result {
         val context = Context()
@@ -53,6 +55,11 @@
             runAnnotationProcessors(command)
         }
         compileClasses(context, commandWithApSources)
+        if (command.info.postProcessor == "jacoco") {
+          context.execute("instrument class files") {
+            jacocoProcessor.instrument(commandWithApSources)
+          }
+        }
         context.execute("create jar") {
             outputJarCreator.createOutputJar(commandWithApSources)
         }
diff --git a/kotlin/builder/src/io/bazel/kotlin/builder/mode/jvm/actions/JacocoProcessor.kt b/kotlin/builder/src/io/bazel/kotlin/builder/mode/jvm/actions/JacocoProcessor.kt
new file mode 100644
index 0000000..abd0c9f
--- /dev/null
+++ b/kotlin/builder/src/io/bazel/kotlin/builder/mode/jvm/actions/JacocoProcessor.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+package io.bazel.kotlin.builder.mode.jvm.actions
+
+import io.bazel.kotlin.builder.KotlinToolchain
+import org.jacoco.core.instr.Instrumenter
+import org.jacoco.core.runtime.OfflineInstrumentationAccessGenerator
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.IOException
+import java.nio.file.FileVisitResult
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.nio.file.SimpleFileVisitor
+import java.nio.file.attribute.BasicFileAttributes
+import io.bazel.kotlin.model.KotlinModel
+import com.google.devtools.build.lib.view.proto.Deps
+import com.google.inject.ImplementedBy
+import com.google.inject.Inject
+
+@ImplementedBy(DefaultJacocoProcessor::class)
+interface JacocoProcessor {
+    fun instrument(command: KotlinModel.BuilderCommand)
+}
+
+class DefaultJacocoProcessor @Inject constructor(
+    val compiler: KotlinToolchain.KotlincInvoker
+) : JacocoProcessor {
+    override fun instrument(command: KotlinModel.BuilderCommand) {
+        val classDir = Paths.get(command.directories.classes)
+        val instr = Instrumenter(OfflineInstrumentationAccessGenerator())
+
+        // Runs Jacoco instrumentation processor over all .class files.
+        Files.walkFileTree(
+            classDir,
+            object : SimpleFileVisitor<Path>() {
+                override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
+                    if (!file.fileName.toString().endsWith(".class")) {
+                        return FileVisitResult.CONTINUE
+                    }
+
+                    val uninstrumentedCopy = Paths.get(file.toString() + ".uninstrumented")
+                    Files.move(file, uninstrumentedCopy)
+                    BufferedInputStream(Files.newInputStream(uninstrumentedCopy)).use { input ->
+                        BufferedOutputStream(Files.newOutputStream(file)).use { output ->
+                            instr.instrument(input, output, file.toString())
+                        }
+                    }
+                    return FileVisitResult.CONTINUE
+                }
+            })
+    }
+}
diff --git a/kotlin/internal/compile.bzl b/kotlin/internal/compile.bzl
index e08429a..94b68ea 100644
--- a/kotlin/internal/compile.bzl
+++ b/kotlin/internal/compile.bzl
@@ -68,6 +68,10 @@
     if len(plugin_info.annotation_processors) > 0:
         args += [ "--kt-plugins", plugin_info.to_json() ]
 
+    # Post-process class files with the Jacoco offline instrumenter, if needed.
+    if ctx.coverage_instrumented() or ctx.attr.internal_coverage_instrumented:
+        args += [ "--post_processor", "jacoco" ]
+
     # Declare and write out argument file.
     args_file = ctx.actions.declare_file(ctx.label.name + ".jar-2.params")
     ctx.actions.write(args_file, "\n".join(args))
@@ -144,7 +148,7 @@
         transitive_runtime_jars = my_transitive_runtime_jars
     )
 
-def _make_providers(ctx, java_info, module_name, transitive_files=depset(order="default")):
+def _make_providers(ctx, java_info, module_name, transitive_files=depset(order="default"), extra_runfiles=[]):
     kotlin_info=kt.info.KtInfo(
         srcs=ctx.files.srcs,
         module_name = module_name,
@@ -159,9 +163,13 @@
         ),
     )
 
+    files = [ctx.outputs.jar]
+    if hasattr(ctx.outputs, "executable"):
+        files.append(ctx.outputs.executable)
     default_info = DefaultInfo(
-        files=depset([ctx.outputs.jar]),
+        files=depset(files),
         runfiles=ctx.runfiles(
+            files=extra_runfiles + [ctx.outputs.jar],
             transitive_files=transitive_files,
             collect_default=True
         ),
@@ -170,6 +178,11 @@
     return struct(
         kt=kotlin_info,
         providers=[java_info,default_info,kotlin_info],
+        instrumented_files = struct(
+            extensions = ['.kt'],
+            source_attributes = ['srcs'],
+            dependency_attributes = ['deps', 'runtime_deps'],
+        )
     )
 
 def _compile_action(ctx, rule_kind, module_name, friend_paths=depset(), src_jars=[]):
diff --git a/kotlin/internal/rules.bzl b/kotlin/internal/rules.bzl
index 79ee8b1..95e441d 100644
--- a/kotlin/internal/rules.bzl
+++ b/kotlin/internal/rules.bzl
@@ -77,21 +77,7 @@
 
 def kt_jvm_library_impl(ctx):
     module_name=utils.derive_module_name(ctx)
-    return compile.make_providers(
-        ctx,
-        compile.compile_action(ctx, "kt_jvm_library", module_name),
-        module_name,
-  )
-
-def kt_jvm_binary_impl(ctx):
-    module_name=utils.derive_module_name(ctx)
-    java_info = compile.compile_action(ctx, "kt_jvm_binary", module_name)
-    utils.actions.write_launcher(
-        ctx,
-        java_info.transitive_runtime_jars,
-        ctx.attr.main_class,
-        ctx.attr.jvm_flags
-    )
+    java_info = compile.compile_action(ctx, "kt_jvm_library", module_name)
     return compile.make_providers(
         ctx,
         java_info,
@@ -99,8 +85,42 @@
         depset(
             order = "default",
             transitive=[java_info.transitive_runtime_jars],
-            direct=[ctx.executable._java]
-        ),
+        )
+    )
+
+def _kt_jvm_runnable_impl(ctx, rule_kind, module_name, launcher_jvm_flags=[], friend_paths=depset()):
+    java_info = compile.compile_action(ctx, rule_kind, module_name, friend_paths)
+
+    transitive_runtime_jars = java_info.transitive_runtime_jars
+    if rule_kind == "kt_jvm_test":
+        transitive_runtime_jars += ctx.files._bazel_test_runner
+    if ctx.configuration.coverage_enabled or ctx.attr.internal_coverage_enabled:
+        transitive_runtime_jars += ctx.files._jacocorunner
+
+    extra_runfiles = utils.actions.write_launcher(
+        ctx,
+        transitive_runtime_jars,
+        main_class = ctx.attr.main_class,
+        jvm_flags = launcher_jvm_flags + ctx.attr.jvm_flags,
+    )
+    transitive_files = depset(
+        order = "default",
+        transitive=[transitive_runtime_jars],
+        direct=[ctx.executable._java],
+    )
+    return compile.make_providers(
+        ctx,
+        java_info,
+        module_name,
+        transitive_files,
+        extra_runfiles,
+    )
+
+def kt_jvm_binary_impl(ctx):
+    return _kt_jvm_runnable_impl(
+        ctx,
+        "kt_jvm_binary",
+        utils.derive_module_name(ctx)
     )
 
 def kt_jvm_junit_test_impl(ctx):
@@ -117,24 +137,10 @@
             friend_paths += [j.path for j in friends[0][JavaInfo].compile_jars]
             module_name = friends[0][kt.info.KtInfo].module_name
 
-    java_info = compile.compile_action(ctx, "kt_jvm_test", module_name,friend_paths)
-
-    transitive_runtime_jars = java_info.transitive_runtime_jars + ctx.files._bazel_test_runner
-    launcherJvmFlags = ["-ea", "-Dbazel.test_suite=%s"% ctx.attr.test_class]
-
-    utils.actions.write_launcher(
+    return _kt_jvm_runnable_impl(
         ctx,
-        transitive_runtime_jars,
-        main_class = ctx.attr.main_class,
-        jvm_flags = launcherJvmFlags + ctx.attr.jvm_flags,
+        rule_kind = "kt_jvm_test",
+        module_name = module_name,
+        launcher_jvm_flags = ["-ea", "-Dbazel.test_suite=%s" % ctx.attr.test_class],
+        friend_paths=friend_paths
     )
-    return compile.make_providers(
-        ctx,
-        java_info,
-        module_name,
-        depset(
-            order = "default",
-            transitive=[transitive_runtime_jars],
-            direct=[ctx.executable._java]
-        ),
-    )
\ No newline at end of file
diff --git a/kotlin/internal/utils.bzl b/kotlin/internal/utils.bzl
index fd34a66..724e558 100644
--- a/kotlin/internal/utils.bzl
+++ b/kotlin/internal/utils.bzl
@@ -214,20 +214,51 @@
     jvm_flags = " ".join([ctx.expand_location(f, ctx.attr.data) for f in jvm_flags])
     template = ctx.attr._java_stub_template.files.to_list()[0]
 
+    workspace_prefix = ctx.workspace_name + "/"
+    substitutions = {
+        "%classpath%": classpath,
+        "%javabin%": "JAVABIN=${RUNPATH}" + ctx.executable._java.short_path,
+        "%jvm_flags%": jvm_flags,
+        "%workspace_prefix%": workspace_prefix,
+    }
+
+    extra_runfiles = []
+    if ctx.configuration.coverage_enabled or ctx.attr.internal_coverage_enabled:
+        metadata = ctx.new_file("coverage_runtime_classpath/%s/runtime-classpath.txt" % ctx.attr.name)
+        extra_runfiles.append(metadata)
+        # We replace '../' to get a runtime-classpath.txt as close as possible to the one
+        # produced by java_binary.
+        metadata_entries = [rjar.short_path.replace("../", "external/") for rjar in rjars]
+        ctx.file_action(metadata, content="\n".join(metadata_entries))
+        substitutions += {
+            "%java_start_class%": "com.google.testing.coverage.JacocoCoverageRunner",
+            # %set_jacoco_main_class% and %set_jacoco_java_runfiles_root% are not
+            # taken into account, so we cram everything with %set_jacoco_metadata%.
+            "%set_jacoco_metadata%": "\n".join([
+                "export JACOCO_METADATA_JAR=${JAVA_RUNFILES}/" + workspace_prefix + metadata.short_path,
+                "export JACOCO_MAIN_CLASS=" + main_class,
+                "export JACOCO_JAVA_RUNFILES_ROOT=${JAVA_RUNFILES}/" + workspace_prefix,
+            ]),
+            "%set_jacoco_main_class%": "",
+            "%set_jacoco_java_runfiles_root%": "",
+        }
+    else:
+        substitutions += {
+            "%java_start_class%": main_class,
+            "%set_jacoco_metadata%": "",
+            "%set_jacoco_main_class%": "",
+            "%set_jacoco_java_runfiles_root%": "",
+        }
+
     ctx.actions.expand_template(
         template = template,
         output = ctx.outputs.executable,
-        substitutions = {
-            "%classpath%": classpath,
-            "%java_start_class%": main_class,
-            "%javabin%": "JAVABIN=${RUNPATH}" + ctx.executable._java.short_path,
-            "%jvm_flags%": jvm_flags,
-            "%set_jacoco_metadata%": "",
-            "%workspace_prefix%": ctx.workspace_name + "/",
-        },
+        substitutions = substitutions,
         is_executable = True,
     )
 
+    return extra_runfiles
+
 # EXPORT #######################################################################################################################################################
 utils = struct(
     actions = struct(
diff --git a/kotlin/kotlin.bzl b/kotlin/kotlin.bzl
index 2ecd9a5..79c6fee 100644
--- a/kotlin/kotlin.bzl
+++ b/kotlin/kotlin.bzl
@@ -176,12 +176,28 @@
         aspects = [_kt_jvm_plugin_aspect],
     ),
     "module_name": attr.string(),
+    "internal_coverage_instrumented": attr.bool(
+        default = False,
+        doc = "visible for testing",
+    ),
 }.items())
 
 _runnable_common_attr = dict(_common_attr.items() + {
     "jvm_flags": attr.string_list(
         default = [],
     ),
+    "_jacocorunner": attr.label(
+        default = Label("@bazel_tools//tools/jdk:JacocoCoverage"),
+    ),
+    "_lcov_merger": attr.label(
+        default = Label("@bazel_tools//tools/test:LcovMerger"),
+        executable = True,
+        cfg = "target",
+    ),
+    "internal_coverage_enabled": attr.bool(
+        default = False,
+        doc = "visible for testing",
+    ),
 }.items())
 
 ########################################################################################################################
diff --git a/tests/integrationtests/jvm/BUILD b/tests/integrationtests/jvm/BUILD
index 70bd38a..f86d868 100644
--- a/tests/integrationtests/jvm/BUILD
+++ b/tests/integrationtests/jvm/BUILD
@@ -33,12 +33,22 @@
     data = [  "//examples/dagger:coffee_app"]
 )
 
+kt_it_assertion_test(
+    name = "coverage_tests",
+    cases = "//tests/integrationtests/jvm/coverage:cases",
+    test_class="io.bazel.kotlin.testing.jvm.JvmCoverageFunctionalTests",
+    deps = [
+        "//tests/integrationtests/jvm/coverage:cases",
+    ],
+)
+
 test_suite(
     name = "jvm",
     tests = [
         ":basic_tests",
         ":kapt_tests",
         ":example_tests",
+        ":coverage_tests",
         "//tests/integrationtests/jvm/basic:friends_tests"
     ]
 )
diff --git a/tests/integrationtests/jvm/JvmCoverageFunctionalTests.kt b/tests/integrationtests/jvm/JvmCoverageFunctionalTests.kt
new file mode 100644
index 0000000..1459117
--- /dev/null
+++ b/tests/integrationtests/jvm/JvmCoverageFunctionalTests.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+package io.bazel.kotlin.testing.jvm
+
+import com.google.common.truth.Truth.assertWithMessage
+import io.bazel.kotlin.testing.AssertionTestCase
+import org.junit.Test
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.util.stream.Collectors
+
+class JvmCoverageFunctionalTests : AssertionTestCase("tests/integrationtests/jvm/coverage") {
+
+    @Test
+    fun testCoverage() {
+        val coverageOutputDir = Files.createTempDirectory(
+                Paths.get(System.getenv("TEST_TMPDIR")),
+                "coverage")
+        assertExecutableRunfileSucceeds(
+                "foo_test",
+                description = "Code coverage should by default be collected for :foo but not :foo_test",
+                environment = mapOf(
+                        "JAVA_COVERAGE_FILE" to coverageOutputDir.resolve("coverage.dat").toString(),
+                        "RUNPATH" to Paths.get("").toAbsolutePath().toString() + "/"))
+        val coverageReports = Files.list(coverageOutputDir)
+                .filter { it.toString().endsWith(".dat") }
+                .collect(Collectors.toList())
+        assertWithMessage("expected one and only one coverage report").that(coverageReports).hasSize(1)
+        val coverageReportLines = Files.readAllLines(coverageReports[0])
+        assertWithMessage("unexpected coverage report").that(coverageReportLines).isEqualTo(listOf(
+                "SF:simple/Foo.kt",
+                "FN:4,simple/Foo::exampleA ()Ljava/lang/String;",
+                "FNDA:1,simple/Foo::exampleA ()Ljava/lang/String;",
+                "FN:5,simple/Foo::exampleB ()Ljava/lang/String;",
+                "FNDA:0,simple/Foo::exampleB ()Ljava/lang/String;",
+                "FN:3,simple/Foo::<init> ()V",
+                "FNDA:1,simple/Foo::<init> ()V",
+                "DA:3,3",
+                "DA:4,2",
+                "DA:5,0",
+                "end_of_record"))
+    }
+}
diff --git a/tests/integrationtests/jvm/coverage/BUILD b/tests/integrationtests/jvm/coverage/BUILD
new file mode 100644
index 0000000..b906987
--- /dev/null
+++ b/tests/integrationtests/jvm/coverage/BUILD
@@ -0,0 +1,28 @@
+load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_jvm_library", "kt_jvm_test")
+
+kt_jvm_library(
+    name = "foo",
+    srcs = ["Foo.kt"],
+    internal_coverage_instrumented = True,
+)
+
+kt_jvm_test(
+    name = "foo_test",
+    srcs = ["FooTest.kt"],
+    test_class = "simple.FooTest",
+    deps = [
+        ":foo",
+        "@io_bazel_rules_kotlin_junit_junit//jar",
+    ],
+    size = "small",
+    internal_coverage_enabled = True,
+)
+
+filegroup(
+    name = "cases",
+    srcs = [
+        ":foo_test",
+    ],
+    visibility=["//tests/integrationtests:__subpackages__"],
+    testonly = True,
+)
diff --git a/tests/integrationtests/jvm/coverage/Foo.kt b/tests/integrationtests/jvm/coverage/Foo.kt
new file mode 100644
index 0000000..d4246a7
--- /dev/null
+++ b/tests/integrationtests/jvm/coverage/Foo.kt
@@ -0,0 +1,6 @@
+package simple
+
+class Foo {
+    fun exampleA() = "A"
+    fun exampleB() = "B"  // Not covered
+}
diff --git a/tests/integrationtests/jvm/coverage/FooTest.kt b/tests/integrationtests/jvm/coverage/FooTest.kt
new file mode 100644
index 0000000..44816a3
--- /dev/null
+++ b/tests/integrationtests/jvm/coverage/FooTest.kt
@@ -0,0 +1,11 @@
+package simple
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class FooTest {
+    @Test
+    fun exampleA() {
+        assertEquals("A", Foo().exampleA())
+    }
+}
diff --git a/tests/rules/AssertionTestCase.kt b/tests/rules/AssertionTestCase.kt
index 0e44ea7..965bc93 100644
--- a/tests/rules/AssertionTestCase.kt
+++ b/tests/rules/AssertionTestCase.kt
@@ -82,10 +82,16 @@
 }
 
 abstract class BasicAssertionTestCase {
-    protected fun assertExecutableRunfileSucceeds(executable: String, description: String? = null) {
+    protected fun assertExecutableRunfileSucceeds(
+        executable: String,
+        description: String? = null,
+        environment: Map<String, String> = emptyMap()
+    ) {
         ProcessBuilder().command("bash", "-c", Paths.get(executable).fileName.toString())
             .also { it.directory(executable.resolveDirectory()) }
-            .start().let {
+            .also { it.environment().putAll(environment) }
+            .also { it.inheritIO() }
+            .start().also {
                 it.waitFor(5, TimeUnit.SECONDS)
                 assert(it.exitValue() == 0) {
                     throw TestCaseFailedException(description, RuntimeException("non-zero return code: ${it.exitValue()}"))
diff --git a/tests/rules/defs.bzl b/tests/rules/defs.bzl
index 95be157..577dd52 100644
--- a/tests/rules/defs.bzl
+++ b/tests/rules/defs.bzl
@@ -16,7 +16,8 @@
 _TEST_COMMON_DEPS=[
     "//tests/rules:assertion_test_case",
     "//third_party/jvm/com/google/truth",
-    "//third_party/jvm/junit:junit"
+    "//third_party/jvm/junit:junit",
+    "@com_github_jetbrains_kotlin//:kotlin-test",
 ]
 
 def kt_it_assertion_test(name, test_class, cases=None, data = [], deps=[]):
diff --git a/third_party/dependencies.yaml b/third_party/dependencies.yaml
index c7d68ea..a08d35b 100644
--- a/third_party/dependencies.yaml
+++ b/third_party/dependencies.yaml
@@ -45,6 +45,15 @@
       modules: ["", "compiler", "producers"]
       lang: "java"
       version: "2.16"
+  org.jacoco:
+    org.jacoco.core:
+      lang: "java"
+      version: "0.7.5.201505241946"
+      exclude: ["org.ow2.asm:asm-debug-all"]
+  org.ow2.asm:
+    asm-commons:
+      lang: "java"
+      version: "6.0"
   org.jetbrains.kotlinx:
     kotlinx-coroutines:
       modules: ["core"]
diff --git a/third_party/jvm/org/jacoco/BUILD b/third_party/jvm/org/jacoco/BUILD
new file mode 100644
index 0000000..be35a9f
--- /dev/null
+++ b/third_party/jvm/org/jacoco/BUILD
@@ -0,0 +1,12 @@
+licenses(["notice"])
+java_library(
+    name = "org_jacoco_core",
+    exports = [
+        "//external:jar/io_bazel_rules_kotlin_org/jacoco/org_jacoco_core"
+    ],
+    visibility = [
+        "//visibility:public"
+    ]
+)
+
+
diff --git a/third_party/jvm/org/ow2/asm/BUILD b/third_party/jvm/org/ow2/asm/BUILD
new file mode 100644
index 0000000..d3c8dde
--- /dev/null
+++ b/third_party/jvm/org/ow2/asm/BUILD
@@ -0,0 +1,42 @@
+licenses(["notice"])
+java_library(
+    name = "asm",
+    exports = [
+        "//external:jar/io_bazel_rules_kotlin_org/ow2/asm/asm"
+    ],
+    visibility = [
+        "//visibility:public"
+    ]
+)
+
+
+
+java_library(
+    name = "asm_commons",
+    exports = [
+        "//external:jar/io_bazel_rules_kotlin_org/ow2/asm/asm_commons"
+    ],
+    runtime_deps = [
+        ":asm_tree"
+    ],
+    visibility = [
+        "//visibility:public"
+    ]
+)
+
+
+
+java_library(
+    name = "asm_tree",
+    exports = [
+        "//external:jar/io_bazel_rules_kotlin_org/ow2/asm/asm_tree"
+    ],
+    runtime_deps = [
+        ":asm"
+    ],
+    visibility = [
+        "//visibility:public"
+    ]
+)
+
+
diff --git a/third_party/jvm/workspace.bzl b/third_party/jvm/workspace.bzl
index dac3946..10df4ca 100644
--- a/third_party/jvm/workspace.bzl
+++ b/third_party/jvm/workspace.bzl
@@ -51,10 +51,14 @@
     {"artifact": "org.checkerframework:checker-compat-qual:2.3.0", "lang": "java", "sha1": "69cb4fea55a9d89b8827d107f17c985cc1a76052", "repository": "https://repo.maven.apache.org/maven2/", "name": "io_bazel_rules_kotlin_org_checkerframework_checker_compat_qual", "actual": "@io_bazel_rules_kotlin_org_checkerframework_checker_compat_qual//jar", "bind": "jar/io_bazel_rules_kotlin_org/checkerframework/checker_compat_qual"},
     {"artifact": "org.codehaus.mojo:animal-sniffer-annotations:1.14", "lang": "java", "sha1": "775b7e22fb10026eed3f86e8dc556dfafe35f2d5", "repository": "https://repo.maven.apache.org/maven2/", "name": "io_bazel_rules_kotlin_org_codehaus_mojo_animal_sniffer_annotations", "actual": "@io_bazel_rules_kotlin_org_codehaus_mojo_animal_sniffer_annotations//jar", "bind": "jar/io_bazel_rules_kotlin_org/codehaus/mojo/animal_sniffer_annotations"},
     {"artifact": "org.hamcrest:hamcrest-core:1.3", "lang": "java", "sha1": "42a25dc3219429f0e5d060061f71acb49bf010a0", "repository": "https://repo.maven.apache.org/maven2/", "name": "io_bazel_rules_kotlin_org_hamcrest_hamcrest_core", "actual": "@io_bazel_rules_kotlin_org_hamcrest_hamcrest_core//jar", "bind": "jar/io_bazel_rules_kotlin_org/hamcrest/hamcrest_core"},
+    {"artifact": "org.jacoco:org.jacoco.core:0.7.5.201505241946", "lang": "java", "sha1": "1ea906dc5201d2a1bc0604f8650534d4bcaf4c95", "repository": "https://repo.maven.apache.org/maven2/", "name": "io_bazel_rules_kotlin_org_jacoco_org_jacoco_core", "actual": "@io_bazel_rules_kotlin_org_jacoco_org_jacoco_core//jar", "bind": "jar/io_bazel_rules_kotlin_org/jacoco/org_jacoco_core"},
     {"artifact": "org.jetbrains.kotlin:kotlin-stdlib-common:1.2.41", "lang": "java", "sha1": "bf0bdac1048fd1c5c54362978dd7e06bd2230e78", "repository": "https://repo.maven.apache.org/maven2/", "name": "io_bazel_rules_kotlin_org_jetbrains_kotlin_kotlin_stdlib_common", "actual": "@io_bazel_rules_kotlin_org_jetbrains_kotlin_kotlin_stdlib_common//jar", "bind": "jar/io_bazel_rules_kotlin_org/jetbrains/kotlin/kotlin_stdlib_common"},
     {"artifact": "org.jetbrains.kotlinx:atomicfu-common:0.10.1", "lang": "java", "sha1": "4eb87291dff597f2f5bac4876fae02ef23466a39", "repository": "https://repo.maven.apache.org/maven2/", "name": "io_bazel_rules_kotlin_org_jetbrains_kotlinx_atomicfu_common", "actual": "@io_bazel_rules_kotlin_org_jetbrains_kotlinx_atomicfu_common//jar", "bind": "jar/io_bazel_rules_kotlin_org/jetbrains/kotlinx/atomicfu_common"},
     {"artifact": "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:0.23.1", "lang": "java", "sha1": "ee988a3e0a918579315ce6654f415b47fec39d36", "repository": "https://repo.maven.apache.org/maven2/", "name": "io_bazel_rules_kotlin_org_jetbrains_kotlinx_kotlinx_coroutines_core_common", "actual": "@io_bazel_rules_kotlin_org_jetbrains_kotlinx_kotlinx_coroutines_core_common//jar", "bind": "jar/io_bazel_rules_kotlin_org/jetbrains/kotlinx/kotlinx_coroutines_core_common"},
     {"artifact": "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.23.1", "lang": "java", "sha1": "fb67b623766f0b2d56697f0b8ed14450f285b8ed", "repository": "https://repo.maven.apache.org/maven2/", "name": "io_bazel_rules_kotlin_org_jetbrains_kotlinx_kotlinx_coroutines_core", "actual": "@io_bazel_rules_kotlin_org_jetbrains_kotlinx_kotlinx_coroutines_core//jar", "bind": "jar/io_bazel_rules_kotlin_org/jetbrains/kotlinx/kotlinx_coroutines_core"},
+    {"artifact": "org.ow2.asm:asm-commons:6.0", "lang": "java", "sha1": "f256fd215d8dd5a4fa2ab3201bf653de266ed4ec", "repository": "https://repo.maven.apache.org/maven2/", "name": "io_bazel_rules_kotlin_org_ow2_asm_asm_commons", "actual": "@io_bazel_rules_kotlin_org_ow2_asm_asm_commons//jar", "bind": "jar/io_bazel_rules_kotlin_org/ow2/asm/asm_commons"},
+    {"artifact": "org.ow2.asm:asm-tree:6.0", "lang": "java", "sha1": "a624f1a6e4e428dcd680a01bab2d4c56b35b18f0", "repository": "https://repo.maven.apache.org/maven2/", "name": "io_bazel_rules_kotlin_org_ow2_asm_asm_tree", "actual": "@io_bazel_rules_kotlin_org_ow2_asm_asm_tree//jar", "bind": "jar/io_bazel_rules_kotlin_org/ow2/asm/asm_tree"},
+    {"artifact": "org.ow2.asm:asm:6.0", "lang": "java", "sha1": "bc6fa6b19424bb9592fe43bbc20178f92d403105", "repository": "https://repo.maven.apache.org/maven2/", "name": "io_bazel_rules_kotlin_org_ow2_asm_asm", "actual": "@io_bazel_rules_kotlin_org_ow2_asm_asm//jar", "bind": "jar/io_bazel_rules_kotlin_org/ow2/asm/asm"},
     ]
 
 def maven_dependencies(callback = declare_maven):