Skip to content

Commit 20d6827

Browse files
committed
Add incremental validation
Validation is modified when using `useIncrementalValidation=true` to not fail when encountering additions, which are known to be backwards compatible. Instead spits out warnings to allow developers commiting the file early with the changes they have made. The use case is to pair it with CI-run :check (:apiDump) task and commit updated API after merge - considering all steps are automated. In cases where the API is not compatible, the task fails similarly to `useIncrementalValidation=false` or simply _the default_. It's acknowledged that using `useIncrementalValidation=true` might cause issues when building upon a feature (or PR) in which cases the .api file might be left out in its previous state, not commited to the VCS. In which case the developer might prefer leaving the option in the default state (false). To the naked eye it might be apparent that checking only removals (leading `-`) is naive, but since the api diff add two distinct lines for changes (one leading `-`, other `+`), this naive approach proves working for all considered use-cases.
1 parent 5db925e commit 20d6827

15 files changed

+322
-15
lines changed

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ apiValidation {
9090
*/
9191
validationDisabled = true
9292
93+
/**
94+
* Flag to allow incremental validation, ie. apiCheck task will not fail for incremental changes.
95+
*/
96+
useIncrementalValidation = true
97+
9398
/**
9499
* A path to a subdirectory inside the project root directory where dumps should be stored.
95100
*/
@@ -129,6 +134,11 @@ apiValidation {
129134
*/
130135
validationDisabled = false
131136

137+
/**
138+
* Flag to allow incremental validation, ie. apiCheck task will not fail for incremental changes.
139+
*/
140+
useIncrementalValidation = true
141+
132142
/**
133143
* A path to a subdirectory inside the project root directory where dumps should be stored.
134144
*/

api/binary-compatibility-validator.api

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class kotlinx/validation/ApiValidationExtension {
99
public final fun getPublicClasses ()Ljava/util/Set;
1010
public final fun getPublicMarkers ()Ljava/util/Set;
1111
public final fun getPublicPackages ()Ljava/util/Set;
12+
public final fun getUseIncrementalValidation ()Z
1213
public final fun getValidationDisabled ()Z
1314
public final fun setAdditionalSourceSets (Ljava/util/Set;)V
1415
public final fun setApiDumpDirectory (Ljava/lang/String;)V
@@ -19,6 +20,7 @@ public class kotlinx/validation/ApiValidationExtension {
1920
public final fun setPublicClasses (Ljava/util/Set;)V
2021
public final fun setPublicMarkers (Ljava/util/Set;)V
2122
public final fun setPublicPackages (Ljava/util/Set;)V
23+
public final fun setUseIncrementalValidation (Z)V
2224
public final fun setValidationDisabled (Z)V
2325
}
2426

@@ -49,9 +51,11 @@ public class kotlinx/validation/KotlinApiCompareTask : org/gradle/api/DefaultTas
4951
public fun <init> (Lorg/gradle/api/model/ObjectFactory;)V
5052
public final fun getApiBuildDir ()Ljava/io/File;
5153
public final fun getDummyOutputFile ()Ljava/io/File;
54+
public final fun getIncremental ()Z
5255
public final fun getNonExistingProjectApiDir ()Ljava/lang/String;
5356
public final fun getProjectApiDir ()Ljava/io/File;
5457
public final fun setApiBuildDir (Ljava/io/File;)V
58+
public final fun setIncremental (Z)V
5559
public final fun setNonExistingProjectApiDir (Ljava/lang/String;)V
5660
public final fun setProjectApiDir (Ljava/io/File;)V
5761
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright 2016-2024 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.validation.test
7+
8+
import kotlinx.validation.api.*
9+
import org.junit.*
10+
11+
class IncrementalTest : BaseKotlinGradleTest() {
12+
13+
@Test
14+
fun `fails when removing source lines`() {
15+
val runner = test {
16+
buildGradleKts {
17+
resolve("/examples/gradle/base/withPlugin.gradle.kts")
18+
resolve("/examples/gradle/configuration/incremental/incremental.gradle.kts")
19+
}
20+
kotlin("IncrementalBase.kt") {
21+
resolve("/examples/classes/IncrementalRemoval.kt")
22+
}
23+
apiFile(rootProjectDir.name) {
24+
resolve("/examples/classes/Incremental.dump")
25+
}
26+
runner {
27+
arguments.add(":apiCheck")
28+
}
29+
}
30+
runner.buildAndFail().apply {
31+
assertTaskFailure(":apiCheck")
32+
}
33+
}
34+
35+
@Test
36+
fun `fails when modifying source lines`() {
37+
val runner = test {
38+
buildGradleKts {
39+
resolve("/examples/gradle/base/withPlugin.gradle.kts")
40+
resolve("/examples/gradle/configuration/incremental/incremental.gradle.kts")
41+
}
42+
kotlin("IncrementalBase.kt") {
43+
resolve("/examples/classes/IncrementalModification.kt")
44+
}
45+
apiFile(rootProjectDir.name) {
46+
resolve("/examples/classes/Incremental.dump")
47+
}
48+
runner {
49+
arguments.add(":apiCheck")
50+
}
51+
}
52+
runner.buildAndFail().apply {
53+
assertTaskFailure(":apiCheck")
54+
}
55+
}
56+
57+
@Test
58+
fun `succeeds when adding source lines`() {
59+
val runner = test {
60+
buildGradleKts {
61+
resolve("/examples/gradle/base/withPlugin.gradle.kts")
62+
resolve("/examples/gradle/configuration/incremental/incremental.gradle.kts")
63+
}
64+
kotlin("IncrementalBase.kt") {
65+
resolve("/examples/classes/IncrementalAddition.kt")
66+
}
67+
apiFile(rootProjectDir.name) {
68+
resolve("/examples/classes/Incremental.dump")
69+
}
70+
runner {
71+
arguments.add(":apiCheck")
72+
}
73+
}
74+
runner.build().apply {
75+
assertTaskSuccess(":apiCheck")
76+
}
77+
}
78+
79+
@Test
80+
fun `does not dump when removing source lines`() {
81+
val runner = test {
82+
buildGradleKts {
83+
resolve("/examples/gradle/base/withPlugin.gradle.kts")
84+
resolve("/examples/gradle/configuration/incremental/incremental.gradle.kts")
85+
}
86+
kotlin("IncrementalBase.kt") {
87+
resolve("/examples/classes/IncrementalRemoval.kt")
88+
}
89+
apiFile(rootProjectDir.name) {
90+
resolve("/examples/classes/Incremental.dump")
91+
}
92+
runner {
93+
arguments.add(":apiCheck")
94+
}
95+
}
96+
runner.buildAndFail().apply {
97+
assertTaskFailure(":apiCheck")
98+
assertTaskNotRun(":apiDump")
99+
}
100+
}
101+
102+
@Test
103+
fun `does not dump when modifying source lines`() {
104+
val runner = test {
105+
buildGradleKts {
106+
resolve("/examples/gradle/base/withPlugin.gradle.kts")
107+
resolve("/examples/gradle/configuration/incremental/incremental.gradle.kts")
108+
}
109+
kotlin("IncrementalBase.kt") {
110+
resolve("/examples/classes/IncrementalModification.kt")
111+
}
112+
apiFile(rootProjectDir.name) {
113+
resolve("/examples/classes/Incremental.dump")
114+
}
115+
runner {
116+
arguments.add(":apiCheck")
117+
}
118+
}
119+
runner.buildAndFail().apply {
120+
assertTaskFailure(":apiCheck")
121+
assertTaskNotRun(":apiDump")
122+
}
123+
}
124+
125+
@Ignore
126+
@Test
127+
fun `updates dump when adding source lines`() {
128+
val runner = test {
129+
buildGradleKts {
130+
resolve("/examples/gradle/base/withPlugin.gradle.kts")
131+
resolve("/examples/gradle/configuration/incremental/incremental.gradle.kts")
132+
}
133+
kotlin("IncrementalBase.kt") {
134+
resolve("/examples/classes/IncrementalAddition.kt")
135+
}
136+
apiFile(rootProjectDir.name) {
137+
resolve("/examples/classes/Incremental.dump")
138+
}
139+
runner {
140+
arguments.add(":apiCheck")
141+
}
142+
}
143+
runner.build().apply {
144+
assertTaskSuccess(":apiCheck")
145+
assertTaskSuccess(":apiDump")
146+
}
147+
}
148+
149+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
public final class foo/Incremental {
2+
public fun <init> (Ljava/lang/Object;)V
3+
public final fun component1 ()Ljava/lang/Object;
4+
public final fun copy (Ljava/lang/Object;)Lfoo/Incremental;
5+
public static synthetic fun copy$default (Lfoo/Incremental;Ljava/lang/Object;ILjava/lang/Object;)Lfoo/Incremental;
6+
public fun equals (Ljava/lang/Object;)Z
7+
public final fun getId ()Ljava/lang/Object;
8+
public fun hashCode ()I
9+
public final fun integer ()I
10+
public fun toString ()Ljava/lang/String;
11+
}
12+
13+
public final class foo/IncrementalBaseKt {
14+
public static final fun getId ()Ljava/lang/Object;
15+
public static final fun sum ([I)I
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2016-2024 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package foo
7+
8+
data class Incremental(val id: Any) {
9+
fun integer() = 42
10+
fun double() = 4.2
11+
}
12+
13+
fun sum(vararg integers: Int) = integers.sum()
14+
fun mus(vararg integers: Int) = integers.sum()
15+
16+
val id: Any = 42
17+
val id2: Any = 24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright 2016-2024 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package foo
7+
8+
data class Incremental(val id: Any) {
9+
fun integer() = 42
10+
}
11+
12+
fun sum(vararg integers: Int) = integers.sum()
13+
14+
val id: Any = 42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright 2016-2024 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package foo
7+
8+
data class Incremental(val id: String) {
9+
fun integer() = 42.0
10+
}
11+
12+
fun sumOfInts(vararg integers: Int) = integers.sum()
13+
14+
val id: Int = 42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Copyright 2016-2024 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package foo
7+
8+
class Incremental
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Copyright 2016-2024 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
configure<kotlinx.validation.ApiValidationExtension> {
7+
useIncrementalValidation = true
8+
}

src/main/kotlin/ApiValidationExtension.kt

+10-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ public open class ApiValidationExtension {
1212
*/
1313
public var validationDisabled: Boolean = false
1414

15+
/**
16+
* Replaces hard errors during validation with warnings if only additions have been made since the last public API
17+
* snapshot. This can prove useful when publishing incremental API changes without additional human interaction.
18+
*/
19+
public var useIncrementalValidation: Boolean = false
20+
1521
/**
1622
* Fully qualified package names that are not consider public API.
1723
* For example, it could be `kotlinx.coroutines.internal` or `kotlinx.serialization.implementation`.
@@ -36,17 +42,17 @@ public open class ApiValidationExtension {
3642
public var ignoredClasses: MutableSet<String> = HashSet()
3743

3844
/**
39-
* Fully qualified names of annotations that can be used to explicitly mark public declarations.
45+
* Fully qualified names of annotations that can be used to explicitly mark public declarations.
4046
* If at least one of [publicMarkers], [publicPackages] or [publicClasses] is defined,
41-
* all declarations not covered by any of them will be considered non-public.
47+
* all declarations not covered by any of them will be considered non-public.
4248
* [ignoredPackages], [ignoredClasses] and [nonPublicMarkers] can be used for additional filtering.
4349
*/
4450
public var publicMarkers: MutableSet<String> = HashSet()
4551

4652
/**
47-
* Fully qualified package names that contain public declarations.
53+
* Fully qualified package names that contain public declarations.
4854
* If at least one of [publicMarkers], [publicPackages] or [publicClasses] is defined,
49-
* all declarations not covered by any of them will be considered non-public.
55+
* all declarations not covered by any of them will be considered non-public.
5056
* [ignoredPackages], [ignoredClasses] and [nonPublicMarkers] can be used for additional filtering.
5157
*/
5258
public var publicPackages: MutableSet<String> = HashSet()

src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt

+1
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ private fun Project.configureCheckTasks(
274274
isEnabled = apiCheckEnabled(projectName, extension) && apiBuild.map { it.enabled }.getOrElse(true)
275275
group = "verification"
276276
description = "Checks signatures of public API against the golden value in API folder for $projectName"
277+
incremental = extension.useIncrementalValidation
277278
compareApiDumps(apiReferenceDir = apiCheckDir.get(), apiBuildDir = apiBuildDir.get())
278279
dependsOn(apiBuild)
279280
}
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2016-2024 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.validation
7+
8+
import org.gradle.api.logging.Logger
9+
10+
internal class IncrementalVerification(private val subject: String, private val logger: Logger): Verification {
11+
override fun verify(diffSet: Collection<String>) {
12+
var containsAdditions = false
13+
var containsRemovals = false
14+
out@ for (diff in diffSet) {
15+
for (line in diff.split("\n")) {
16+
when {
17+
line.startsWith("+++") || line.startsWith("---") -> continue
18+
line.startsWith("-") -> containsRemovals = true
19+
line.startsWith("+") -> containsAdditions = true
20+
}
21+
if (containsRemovals) break@out
22+
}
23+
}
24+
check(!containsRemovals) {
25+
val diffText = diffSet.joinToString("\n\n")
26+
"Incremental API check failed for project $subject.\n$diffText\n\n You can run :$subject:apiDump task to overwrite API declarations. These changes likely break compatibility with existing consumers using library '$subject', consider incrementing major version code for your next release"
27+
}
28+
if (containsAdditions) {
29+
logger.warn("API is incrementally compatible with previous version, however is not identical to the API file provided.")
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)