Skip to content

Commit e80675f

Browse files
author
Raymie Stata
committed
Add jacocoFullCoverageReport task - integration+unit tests
1 parent 0245a8c commit e80675f

File tree

7 files changed

+249
-3
lines changed

7 files changed

+249
-3
lines changed

build-logic/src/main/kotlin/conventions/jacoco.gradle.kts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ jacoco {
1010
toolVersion = libs.findVersion("jacoco").get().toString()
1111
}
1212

13-
tasks.named<Test>("test") {
14-
finalizedBy(tasks.named("jacocoTestReport"))
13+
tasks.withType<Test>().configureEach {
14+
// Recommended JaCoCo settings when running on JDK 17+
15+
extensions.configure(JacocoTaskExtension::class) {
16+
isIncludeNoLocationClasses = true
17+
excludes = listOf("jdk.internal.*")
18+
}
1519
}
1620

1721
tasks.named<JacocoReport>("jacocoTestReport") {
@@ -43,4 +47,4 @@ tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
4347
}
4448
}
4549
}
46-
}
50+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Add this to projects that have a companion -integration-tests project
3+
*
4+
* It will publishes JaCoCo's exec files and source jars to allow us to make
5+
* a comprehensive test-coverage report.
6+
*/
7+
8+
import org.gradle.api.attributes.Bundling
9+
import org.gradle.api.attributes.Category
10+
import org.gradle.api.attributes.LibraryElements
11+
import org.gradle.api.attributes.Usage
12+
import org.gradle.api.plugins.JavaPlugin
13+
import org.gradle.api.plugins.JavaPluginExtension
14+
import org.gradle.api.tasks.testing.Test
15+
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
16+
17+
val jacocoExecElements by configurations.creating {
18+
description = "JaCoCo exec data produced by ${project.path}"
19+
isCanBeConsumed = true
20+
isCanBeResolved = false
21+
attributes {
22+
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.VERIFICATION))
23+
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
24+
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
25+
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("jacoco-exec"))
26+
}
27+
}
28+
29+
// Make source available for integration-test's coverate report
30+
plugins.withType<JavaPlugin> {
31+
extensions.configure<JavaPluginExtension> {
32+
withSourcesJar()
33+
}
34+
}
35+
36+
// Publish each Test task's JaCoCo destination file (containing raw coverage data) as an outgoing artifact
37+
plugins.withId("jacoco") {
38+
tasks.withType<Test>().configureEach {
39+
extensions.findByType(JacocoTaskExtension::class.java)?.destinationFile?.let { execFile ->
40+
artifacts {
41+
add(jacocoExecElements.name, execFile) {
42+
builtBy(this@configureEach)
43+
type = "jacoco-exec"
44+
}
45+
}
46+
} ?: throw GradleException(
47+
"Jacoco destinationFile is null for task $path. " +
48+
"Ensure your Jacoco convention sets it before this plugin, " +
49+
"or switch to the 'set-if-null' pattern."
50+
)
51+
}
52+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
2+
import buildlogic.IntegrationCoverageExt
3+
import org.gradle.api.attributes.*
4+
import org.gradle.api.model.ObjectFactory
5+
import org.gradle.api.tasks.testing.Test
6+
import org.gradle.kotlin.dsl.*
7+
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
8+
import org.gradle.testing.jacoco.tasks.JacocoReport
9+
10+
private val FULL_REPORT_TASK_NAME = "jacocoFullCoverageReport"
11+
private val INCLUDED_BUILD_NAME = "core"
12+
13+
val ext = extensions.create(
14+
"viaductIntegrationCoverage",
15+
IntegrationCoverageExt::class,
16+
project,
17+
FULL_REPORT_TASK_NAME,
18+
objects,
19+
)
20+
21+
// Configurations we will resolve from the base project's unit-test exec data
22+
val incomingUnitExec by configurations.creating {
23+
description = "JaCoCo exec data from the base module's unit tests (included build)"
24+
isCanBeConsumed = false
25+
isCanBeResolved = true
26+
attributes {
27+
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.VERIFICATION))
28+
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
29+
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
30+
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("jacoco-exec"))
31+
}
32+
}
33+
34+
// Configuration we will resolve from the base project's compiled class files
35+
// This is a bit fragile: it's critical that _only_ the classes compiled from sources
36+
// are included -- and that they come in as class files, not a JAR file. Setting
37+
// isTransitive to false is important, as is the LibraryElements.CLASSES
38+
val baseRuntimeClasses by configurations.creating {
39+
description = "Runtime class directories of the base module (for coverage attribution)"
40+
isCanBeConsumed = false
41+
isCanBeResolved = true
42+
isTransitive = false
43+
attributes {
44+
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
45+
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
46+
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
47+
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.CLASSES))
48+
}
49+
}
50+
51+
// Configuration we will resolve from the base project's source files (optional - for better reports)
52+
val baseRuntimeSources by configurations.creating {
53+
description = "Optional sources jars of the base module (for HTML report browsing)"
54+
isCanBeConsumed = false
55+
isCanBeResolved = true
56+
attributes {
57+
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION))
58+
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
59+
attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType.SOURCES))
60+
}
61+
}
62+
63+
// helper to register either integration-test reports
64+
fun registerCoverageTask(name: String) =
65+
tasks.register<JacocoReport>(name) {
66+
group = "verification"
67+
68+
// Clear anything earlier plugins may have added
69+
// More fagility: we need to be careful because we're bringing in "exec" data
70+
// from an included build - don't want any surprises
71+
classDirectories.setFrom(emptyList<Any>())
72+
additionalClassDirs.setFrom(files())
73+
additionalSourceDirs.setFrom(files())
74+
sourceDirectories.setFrom(emptyList<Any>())
75+
76+
// Classes whose coverage is being analyzed
77+
classDirectories.setFrom(
78+
configurations.named("baseRuntimeClasses").map { cfg ->
79+
// This is a FileCollection with proper builtBy; contains only directories when using classesElements
80+
cfg.incoming.artifactView { }.files
81+
}
82+
)
83+
84+
// sources: only if a sources jar exists, decided at execution time
85+
val baseSrcTrees = configurations.named("baseRuntimeSources").map { cfg ->
86+
cfg.incoming.artifactView { isLenient = true /* <= because optional */ }.files.map { zipTree(it) }
87+
}
88+
sourceDirectories.setFrom(baseSrcTrees)
89+
90+
if (name == FULL_REPORT_TASK_NAME) {
91+
// add unit-test data to report
92+
executionData.from(
93+
configurations.named("incomingUnitExec").map { it.incoming.artifactView { }.files }
94+
)
95+
}
96+
97+
// add integration-test data to report
98+
val itExecsProvider = providers.provider {
99+
tasks.withType<Test>().mapNotNull { t ->
100+
t.extensions.findByType(JacocoTaskExtension::class.java)?.destinationFile
101+
}
102+
}
103+
executionData.from(itExecsProvider)
104+
105+
reports {
106+
xml.required.set(true)
107+
html.required.set(true)
108+
csv.required.set(false)
109+
}
110+
111+
// task dependencies
112+
dependsOn(tasks.withType<Test>()) // local ITs
113+
dependsOn(providers.provider {
114+
val inc = gradle.includedBuild(INCLUDED_BUILD_NAME)
115+
inc.task("${ext.baseProjectPath.get()}:test")
116+
})
117+
118+
doFirst {
119+
val list = executionData.files.toList()
120+
logger.info("Full coverage exec inputs (${list.size}):")
121+
list.forEach { f -> logger.info(" $f [${if (f.exists()) f.length() else -1} bytes]") }
122+
}
123+
}
124+
125+
registerCoverageTask(FULL_REPORT_TASK_NAME)
126+
registerCoverageTask("jacocoIntegrationOnlyReport")
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package buildlogic
2+
3+
import org.gradle.api.Project
4+
import org.gradle.api.model.ObjectFactory
5+
import org.gradle.kotlin.dsl.named
6+
import org.gradle.testing.jacoco.tasks.JacocoReport
7+
8+
open class IntegrationCoverageExt(
9+
private val project: Project,
10+
private val taskName: String,
11+
objects: ObjectFactory,
12+
) {
13+
internal val baseProjectPath = objects.property(String::class.java)
14+
15+
/**
16+
* Project path of base project for these integration tests (e.g., ":tenant:tenant-runtime").
17+
*/
18+
fun baseProject(path: String) {
19+
baseProjectPath.set(path)
20+
21+
// Because they come from an included build, the project dependencies here need
22+
// to be expressed using coordinates, not project-paths -- dependency-substitution
23+
// is used to translate these _back_ into (included) projects
24+
val artifactId = path.split(':').last { it.isNotEmpty() } // ":tenant:tenant-runtime" -> "tenant-runtime"
25+
val coord = "com.airbnb.viaduct:$artifactId"
26+
project.dependencies.apply {
27+
add("incomingUnitExec", coord)
28+
add("baseRuntimeClasses", coord)
29+
// Skip sources for now - they're optional and causing issues
30+
// add("baseRuntimeSources", coord)
31+
}
32+
33+
project.tasks.named<JacocoReport>(taskName).configure {
34+
description = "Unit + integration test coverage for $path"
35+
}
36+
}
37+
}

build.gradle.kts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,24 @@ tasks.register("testAndCoverage") {
8787
println("- Aggregated HTML report: build/reports/jacoco/testCodeCoverageReport/html/index.html")
8888
}
8989
}
90+
91+
// Task to run tenant runtime tests with integration coverage
92+
tasks.register("tenantRuntimeWithIntegrationCoverage") {
93+
description = "Runs tenant runtime unit tests and integration tests, then generates unified coverage"
94+
group = "verification"
95+
96+
doLast {
97+
println("Running tenant runtime integration tests...")
98+
exec {
99+
commandLine("./gradlew", ":tenant:runtime-integration-tests:test")
100+
}
101+
102+
println("Running tenant runtime unit tests and generating coverage report...")
103+
exec {
104+
commandLine("./gradlew", ":core:tenant:tenant-runtime:testWithIntegrationCoverage")
105+
}
106+
107+
println("Tenant runtime coverage with integration tests completed!")
108+
println("Coverage report available at: tenant/runtime/build/reports/jacoco/test/html/index.html")
109+
}
110+
}

tenant/runtime-integration-tests/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
plugins {
22
id("conventions.kotlin")
3+
id("jacoco-integration-tests")
34
id("test-feature-app")
45
id("conventions.kotlin-static-analysis")
56
}
67

78
viaductFeatureApp {}
89

10+
viaductIntegrationCoverage {
11+
baseProject(":tenant:tenant-runtime")
12+
}
13+
914
sourceSets {
1015
named("main") {
1116
java.setSrcDirs(emptyList<File>())

tenant/runtime/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
plugins {
22
id("conventions.kotlin")
33
id("conventions.kotlin-static-analysis")
4+
id("jacoco-integration-base")
45
`java-test-fixtures`
56
}
67

0 commit comments

Comments
 (0)