Repository: Kotlin/binary-compatibility-validator (kotlin)

Issue:
BCV behavior for an empty module is inconsistent between JVM and KMP
When applied to K/JVM module, BCV require to create an API dump even if a module does not have any "production" sources (for instance, it's a test-only module).

At the same time, if it's KMP module, BCV simply ignores the module (instead of reporting an error and asking to create an empty dump).

KLib support complicated the matter:
- in 0.15.0-Beta.{1,2} such a module will be ignored, the same way it's done for JVM ABI validation;
- develop branch contains what should become 0.15.0-Beta.3 and KLib validation fails for an empty module (the same way it would fail for a K/JVM empty module).

BCV should validation should behave consistently for both JVM and KLib ABI as well as both K/JVM and KMP projects.
Current JVM ABI validation behavior for KMP projects seems to be incorrect as sources removal will lead to the `:apiCheck` task being skipped instead of it being executed and failed.

Pull request description:
Currently, only JVM ABI validation in KMP projects allows having no dump file. In other cases (JVM ABI validation in K/JVM project or KLib validation) require the presence of an ABI dump file. Lack of such a file is treated as a validation error.

The main motivation for having a dump files instead of simply ignoring such projects is that otherwise BCV won't be able to catch the case when all sources were removed from a project.

Fixes #243

Patch:
diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt
index ccaf4d2c..3930a340 100644
--- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt
+++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt
@@ -214,8 +214,7 @@ private fun Project.configureKotlinCompilation(
     val apiBuildDir = apiDirProvider.flatMap { f -> layout.buildDirectory.asFile.map { it.resolve(f) } }
 
     val apiBuild = task<KotlinApiBuildTask>(targetConfig.apiTaskName("Build")) {
-        // Do not enable task for empty umbrella modules
-        isEnabled = apiCheckEnabled(projectName, extension) && compilation.hasAnySources()
+        isEnabled = apiCheckEnabled(projectName, extension)
         // 'group' is not specified deliberately, so it will be hidden from ./gradlew tasks
         description =
             "Builds Kotlin API for 'main' compilations of $projectName. Complementary task and shouldn't be called manually"
@@ -562,7 +561,6 @@ private class KlibValidationPipelineBuilder(
     ): TaskProvider<KotlinKlibAbiBuildTask> {
         val projectName = project.name
         val buildTask = project.task<KotlinKlibAbiBuildTask>(targetConfig.apiTaskName("Build")) {
-            // Do not enable task for empty umbrella modules
             isEnabled = klibAbiCheckEnabled(projectName, extension)
             // 'group' is not specified deliberately, so it will be hidden from ./gradlew tasks
             description = "Builds Kotlin KLib ABI dump for 'main' compilations of $projectName. " +
@@ -631,7 +629,3 @@ private val Project.jvmDumpFileName: String
     get() = "$name.api"
 private val Project.klibDumpFileName: String
     get() = "$name.klib.api"
-
-private fun KotlinCompilation<KotlinCommonOptions>.hasAnySources(): Boolean = allKotlinSourceSets.any {
-    it.kotlin.srcDirs.any(File::exists)
-}


Test patch:
diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt
index 94a2cac4..6d050ee5 100644
--- a/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt
+++ b/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt
@@ -129,8 +129,8 @@ internal class MultiPlatformSingleJvmTargetTest : BaseKotlinGradleTest() {
         }
 
         runner.build().apply {
-            assertTaskSkipped(":jvmApiDump")
-            assertTaskUpToDate(":apiDump")
+            assertTaskSuccess(":jvmApiDump")
+            assertTaskSuccess(":apiDump")
         }
     }
 
@@ -149,9 +149,28 @@ internal class MultiPlatformSingleJvmTargetTest : BaseKotlinGradleTest() {
         }
 
         runner.build().apply {
-            assertTaskSkipped(":jvmApiCheck")
-            assertTaskUpToDate(":apiCheck")
+            assertTaskSuccess(":jvmApiCheck")
+            assertTaskSuccess(":apiCheck")
+        }
+    }
 
+    @Test
+    fun testApiCheckFailsForEmptyProjectWithoutDumpFile() {
+        val runner = test {
+            buildGradleKts {
+                resolve("/examples/gradle/base/multiplatformWithSingleJvmTarget.gradle.kts")
+            }
+
+            runner {
+                arguments.add(":apiCheck")
+            }
+        }
+
+        runner.buildAndFail().apply {
+            assertTaskFailure(":jvmApiCheck")
+            assertThat(output).contains(
+                "Expected file with API declarations 'api${File.separator}${rootProjectDir.name}.api' does not exist"
+            )
         }
     }