Skip to content

Commit

Permalink
Configure output directory for dumps (#170)
Browse files Browse the repository at this point in the history
* Support output directory configuration

Fixes #127

---------

Co-authored-by: Vsevolod Tolstopyatov <qwwdfsad@gmail.com>
  • Loading branch information
fzhinkin and qwwdfsad committed Jan 19, 2024
1 parent 1d7505b commit 4367cb7
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 18 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ apiValidation {
* Flag to programmatically disable compatibility validator
*/
validationDisabled = true
/**
* A path to a subdirectory inside the project root directory where dumps should be stored.
*/
apiDumpDirectory = "api"
}
```

Expand Down Expand Up @@ -123,6 +128,11 @@ apiValidation {
* Flag to programmatically disable compatibility validator
*/
validationDisabled = false

/**
* A path to a subdirectory inside the project root directory where dumps should be stored.
*/
apiDumpDirectory = "aux/validation"
}
```

Expand Down
2 changes: 2 additions & 0 deletions api/binary-compatibility-validator.api
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
public class kotlinx/validation/ApiValidationExtension {
public fun <init> ()V
public final fun getAdditionalSourceSets ()Ljava/util/Set;
public final fun getApiDumpDirectory ()Ljava/lang/String;
public final fun getIgnoredClasses ()Ljava/util/Set;
public final fun getIgnoredPackages ()Ljava/util/Set;
public final fun getIgnoredProjects ()Ljava/util/Set;
Expand All @@ -10,6 +11,7 @@ public class kotlinx/validation/ApiValidationExtension {
public final fun getPublicPackages ()Ljava/util/Set;
public final fun getValidationDisabled ()Z
public final fun setAdditionalSourceSets (Ljava/util/Set;)V
public final fun setApiDumpDirectory (Ljava/lang/String;)V
public final fun setIgnoredClasses (Ljava/util/Set;)V
public final fun setIgnoredPackages (Ljava/util/Set;)V
public final fun setIgnoredProjects (Ljava/util/Set;)V
Expand Down
3 changes: 2 additions & 1 deletion src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

package kotlinx.validation.api

import kotlinx.validation.ApiValidationExtension
import java.io.*
import org.gradle.testkit.runner.GradleRunner
import org.intellij.lang.annotations.Language

public const val API_DIR: String = "api"
public val API_DIR: String = ApiValidationExtension().apiDumpDirectory

internal fun BaseKotlinGradleTest.test(fn: BaseKotlinScope.() -> Unit): GradleRunner {
val baseKotlinScope = BaseKotlinScope()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Copyright 2016-2024 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package kotlinx.validation.test

import kotlinx.validation.api.*
import kotlinx.validation.api.buildGradleKts
import kotlinx.validation.api.resolve
import kotlinx.validation.api.test
import org.assertj.core.api.Assertions
import org.junit.Test
import kotlin.test.assertTrue

class OutputDirectoryTests : BaseKotlinGradleTest() {
@Test
fun dumpIntoCustomDirectory() {
val runner = test {
buildGradleKts {
resolve("/examples/gradle/base/withPlugin.gradle.kts")
resolve("/examples/gradle/configuration/outputDirectory/different.gradle.kts")
}

kotlin("AnotherBuildConfig.kt") {
resolve("/examples/classes/AnotherBuildConfig.kt")
}
dir("api") {
file("letMeBe.txt") {
}
}

runner {
arguments.add(":apiDump")
}
}

runner.build().apply {
assertTaskSuccess(":apiDump")

val dumpFile = rootProjectDir.resolve("custom").resolve("${rootProjectDir.name}.api")
assertTrue(dumpFile.exists(), "api dump file ${dumpFile.path} should exist")

val expected = readFileList("/examples/classes/AnotherBuildConfig.dump")
Assertions.assertThat(dumpFile.readText()).isEqualToIgnoringNewLines(expected)

val fileInsideDir = rootProjectDir.resolve("api").resolve("letMeBe.txt")
assertTrue(fileInsideDir.exists(), "existing api directory should not be overridden")
}
}

@Test
fun validateDumpFromACustomDirectory() {
val runner = test {
buildGradleKts {
resolve("/examples/gradle/base/withPlugin.gradle.kts")
resolve("/examples/gradle/configuration/outputDirectory/different.gradle.kts")
}

kotlin("AnotherBuildConfig.kt") {
resolve("/examples/classes/AnotherBuildConfig.kt")
}
dir("custom") {
file("${rootProjectDir.name}.api") {
resolve("/examples/classes/AnotherBuildConfig.dump")
}
}

runner {
arguments.add(":apiCheck")
}
}

runner.build().apply {
assertTaskSuccess(":apiCheck")
}
}

@Test
fun dumpIntoSubdirectory() {
val runner = test {
buildGradleKts {
resolve("/examples/gradle/base/withPlugin.gradle.kts")
resolve("/examples/gradle/configuration/outputDirectory/subdirectory.gradle.kts")
}

kotlin("AnotherBuildConfig.kt") {
resolve("/examples/classes/AnotherBuildConfig.kt")
}

runner {
arguments.add(":apiDump")
}
}

runner.build().apply {
assertTaskSuccess(":apiDump")

val dumpFile = rootProjectDir.resolve("validation")
.resolve("api")
.resolve("${rootProjectDir.name}.api")

assertTrue(dumpFile.exists(), "api dump file ${dumpFile.path} should exist")

val expected = readFileList("/examples/classes/AnotherBuildConfig.dump")
Assertions.assertThat(dumpFile.readText()).isEqualToIgnoringNewLines(expected)
}
}

@Test
fun validateDumpFromASubdirectory() {
val runner = test {
buildGradleKts {
resolve("/examples/gradle/base/withPlugin.gradle.kts")
resolve("/examples/gradle/configuration/outputDirectory/subdirectory.gradle.kts")
}

kotlin("AnotherBuildConfig.kt") {
resolve("/examples/classes/AnotherBuildConfig.kt")
}
dir("validation") {
dir("api") {
file("${rootProjectDir.name}.api") {
resolve("/examples/classes/AnotherBuildConfig.dump")
}
}
}

runner {
arguments.add(":apiCheck")
}
}

runner.build().apply {
assertTaskSuccess(":apiCheck")
}
}

@Test
fun dumpIntoParentDirectory() {
val runner = test {
buildGradleKts {
resolve("/examples/gradle/base/withPlugin.gradle.kts")
resolve("/examples/gradle/configuration/outputDirectory/outer.gradle.kts")
}

kotlin("AnotherBuildConfig.kt") {
resolve("/examples/classes/AnotherBuildConfig.kt")
}

runner {
arguments.add(":apiDump")
}
}

runner.buildAndFail().apply {
Assertions.assertThat(output).contains("apiDumpDirectory (\"../api\") should be inside the project directory")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright 2016-2024 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

configure<kotlinx.validation.ApiValidationExtension> {
apiDumpDirectory = "custom"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright 2016-2024 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

configure<kotlinx.validation.ApiValidationExtension> {
apiDumpDirectory = "../api"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright 2016-2024 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

configure<kotlinx.validation.ApiValidationExtension> {
apiDumpDirectory = "validation/api"
}
7 changes: 7 additions & 0 deletions src/main/kotlin/ApiValidationExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,11 @@ public open class ApiValidationExtension {
* By default, only the `main` source set is checked.
*/
public var additionalSourceSets: MutableSet<String> = HashSet()

/**
* A path to a directory containing an API dump.
* The path should be relative to the project's root directory and should resolve to its subdirectory.
* By default, it's `api`.
*/
public var apiDumpDirectory: String = "api"
}
46 changes: 29 additions & 17 deletions src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import org.jetbrains.kotlin.gradle.dsl.*
import org.jetbrains.kotlin.gradle.plugin.*
import java.io.*

private const val API_DIR = "api" // Mirrored in functional tests

public class BinaryCompatibilityValidatorPlugin : Plugin<Project> {

override fun apply(target: Project): Unit = with(target) {
Expand Down Expand Up @@ -85,7 +83,7 @@ public class BinaryCompatibilityValidatorPlugin : Plugin<Project> {
kotlin.targets.matching {
it.platformType == KotlinPlatformType.jvm || it.platformType == KotlinPlatformType.androidJvm
}.all { target ->
val targetConfig = TargetConfig(project, target.name, dirConfig)
val targetConfig = TargetConfig(project, extension, target.name, dirConfig)
if (target.platformType == KotlinPlatformType.jvm) {
target.compilations.matching { it.name == "main" }.all {
project.configureKotlinCompilation(it, extension, targetConfig, commonApiDump, commonApiCheck)
Expand Down Expand Up @@ -130,30 +128,43 @@ public class BinaryCompatibilityValidatorPlugin : Plugin<Project> {
project: Project,
extension: ApiValidationExtension
) = configurePlugin("kotlin", project, extension) {
project.configureApiTasks(extension, TargetConfig(project))
project.configureApiTasks(extension, TargetConfig(project, extension))
}
}

private class TargetConfig constructor(
project: Project,
extension: ApiValidationExtension,
val targetName: String? = null,
private val dirConfig: Provider<DirConfig>? = null,
dirConfig: Provider<DirConfig>? = null,
) {
private val apiDirProvider = project.provider {
val dir = extension.apiDumpDirectory

val root = project.layout.projectDirectory.asFile.toPath().toAbsolutePath().normalize()
val resolvedDir = root.resolve(dir).normalize()
if (!resolvedDir.startsWith(root)) {
throw IllegalArgumentException(
"apiDumpDirectory (\"$dir\") should be inside the project directory, " +
"but it resolves to a path outside the project root.\n" +
"Project's root path: $root\nResolved apiDumpDirectory: $resolvedDir"
)
}

private val API_DIR_PROVIDER = project.provider { API_DIR }
dir
}

val apiDir = dirConfig?.map { dirConfig ->
when (dirConfig) {
DirConfig.COMMON -> apiDirProvider.get()
else -> "${apiDirProvider.get()}/$targetName"
}
} ?: apiDirProvider

fun apiTaskName(suffix: String) = when (targetName) {
null, "" -> "api$suffix"
else -> "${targetName}Api$suffix"
else -> "${targetName}Api$suffix"
}

val apiDir
get() = dirConfig?.map { dirConfig ->
when (dirConfig) {
DirConfig.COMMON -> API_DIR
else -> "$API_DIR/$targetName"
}
} ?: API_DIR_PROVIDER
}

private enum class DirConfig {
Expand All @@ -162,6 +173,7 @@ private enum class DirConfig {
* Used in single target projects
*/
COMMON,

/**
* Target-based directory, used in multitarget setups.
* E.g. for the project with targets jvm and android,
Expand All @@ -174,7 +186,7 @@ private enum class DirConfig {
private fun Project.configureKotlinCompilation(
compilation: KotlinCompilation<KotlinCommonOptions>,
extension: ApiValidationExtension,
targetConfig: TargetConfig = TargetConfig(this),
targetConfig: TargetConfig = TargetConfig(this, extension),
commonApiDump: TaskProvider<Task>? = null,
commonApiCheck: TaskProvider<Task>? = null,
useOutput: Boolean = false,
Expand Down Expand Up @@ -221,7 +233,7 @@ private fun apiCheckEnabled(projectName: String, extension: ApiValidationExtensi

private fun Project.configureApiTasks(
extension: ApiValidationExtension,
targetConfig: TargetConfig = TargetConfig(this),
targetConfig: TargetConfig = TargetConfig(this, extension),
) {
val projectName = project.name
val apiBuildDir = targetConfig.apiDir.map { layout.buildDirectory.asFile.get().resolve(it) }
Expand Down

0 comments on commit 4367cb7

Please sign in to comment.