Skip to content

Commit

Permalink
Fixed multiple jvm targets (#62)
Browse files Browse the repository at this point in the history
 Make the plugin work with Kotlin multiplatform for multiple targets:

* Rename tasks to include the target (except for the JVM - for backwards compatibility)
* Rather than having separate tasks where some use the original name, just have two collector tasks "apiDump" and "apiCheck" that depend on the platform-specific versions. It improves consistency, compatibility, and usability.
* Store and build .api files in subdirectories named after the target

Author: Paul de Vrieze <paul.devrieze@gmail.com>
Co-authored-by: Vsevolod Tolstopyatov <qwwdfsad@gmail.com>
  • Loading branch information
pdvrieze and qwwdfsad authored Oct 4, 2021
1 parent 31fe9bb commit 6a3180b
Show file tree
Hide file tree
Showing 8 changed files with 557 additions and 44 deletions.
34 changes: 34 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,48 @@ tasks.register<Test>("functionalTest") {
}
tasks.check { dependsOn(tasks["functionalTest"]) }

// While gradle testkit supports injection of the plugin classpath it doesn't allow using dependency notation
// to determine the actual runtime classpath for the plugin. It uses isolation, so plugins applied by the build
// script are not visible in the plugin classloader. This means optional dependencies (dependent on applied plugins -
// for example kotlin multiplatform) are not visible even if they are in regular gradle use. This hack will allow
// extending the classpath. It is based upon: https://docs.gradle.org/6.0/userguide/test_kit.html#sub:test-kit-classpath-injection

// Create a configuration to register the dependencies against
val testPluginRuntimeConfiguration = configurations.register("testPluginRuntime")

// The task that will create a file that stores the classpath needed for the plugin to have additional runtime dependencies
// This file is then used in to tell TestKit which classpath to use.
val createClasspathManifest = tasks.register("createClasspathManifest") {
val outputDir = buildDir.resolve("cpManifests")
inputs.files(testPluginRuntimeConfiguration)
.withPropertyName("runtimeClasspath")
.withNormalizer(ClasspathNormalizer::class)

outputs.dir(outputDir)
.withPropertyName("outputDir")

doLast {
outputDir.mkdirs()
file(outputDir.resolve("plugin-classpath.txt")).writeText(testPluginRuntimeConfiguration.get().joinToString("\n"))
}
}

val kotlinVersion: String by project

dependencies {
implementation(gradleApi())
implementation("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.3.0")
implementation("org.ow2.asm:asm:9.0")
implementation("org.ow2.asm:asm-tree:9.0")
implementation("com.googlecode.java-diff-utils:diffutils:1.3.0")
compileOnly("org.jetbrains.kotlin.multiplatform:org.jetbrains.kotlin.multiplatform.gradle.plugin:1.3.61")

// The test needs the full kotlin multiplatform plugin loaded as it has no visibility of previously loaded plugins,
// unlike the regular way gradle loads plugins.
add(testPluginRuntimeConfiguration.name, "org.jetbrains.kotlin.multiplatform:org.jetbrains.kotlin.multiplatform.gradle.plugin:$kotlinVersion")

testImplementation(kotlin("test-junit"))
"functionalTestImplementation"(files(createClasspathManifest))

"functionalTestImplementation"("org.assertj:assertj-core:3.18.1")
"functionalTestImplementation"(gradleTestKit())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2016-2021 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.api

import org.gradle.testkit.runner.GradleRunner
import java.io.File
import java.io.InputStreamReader


fun GradleRunner.addPluginTestRuntimeClasspath() = apply {

val cpResource = javaClass.classLoader.getResourceAsStream("plugin-classpath.txt")
?.let { InputStreamReader(it) }
?: throw IllegalStateException("Could not find classpath resource")

val pluginClasspath = pluginClasspath + cpResource.readLines().map { File(it) }
withPluginClasspath(pluginClasspath)

}
6 changes: 3 additions & 3 deletions src/functionalTest/kotlin/kotlinx/validation/api/testDsl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ internal fun BaseKotlinGradleTest.test(fn: BaseKotlinScope.() -> Unit): GradleRu
}

/**
* same as [file][FileContainer.file], but prepends "src/main/java" before given `classFileName`
* same as [file][FileContainer.file], but prepends "src/${sourceSet}/kotlin" before given `classFileName`
*/
internal fun FileContainer.kotlin(classFileName: String, fn: AppendableScope.() -> Unit) {
internal fun FileContainer.kotlin(classFileName: String, sourceSet:String = "main", fn: AppendableScope.() -> Unit) {
require(classFileName.endsWith(".kt")) {
"ClassFileName must end with '.kt'"
}

val fileName = "src/main/java/$classFileName"
val fileName = "src/${sourceSet}/kotlin/$classFileName"
file(fileName, fn)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2016-2021 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 org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.io.File

internal class MultiPlatformSingleJvmTargetTest : BaseKotlinGradleTest() {
private fun BaseKotlinScope.createProjectHierarchyWithPluginOnRoot() {
settingsGradleKts {
resolve("examples/gradle/settings/settings-name-testproject.gradle.kts")
}
buildGradleKts {
resolve("examples/gradle/base/multiplatformWithSingleJvmTarget.gradle.kts")
}
}

@Test
fun testApiCheckPasses() {
val runner = test {
createProjectHierarchyWithPluginOnRoot()
runner {
arguments.add(":apiCheck")
arguments.add("--stacktrace")
}

dir("api/") {
file("testproject.api") {
resolve("examples/classes/Subsub1Class.dump")
resolve("examples/classes/Subsub2Class.dump")
}
}

dir("src/jvmMain/kotlin") {}
kotlin("Subsub1Class.kt", "commonMain") {
resolve("examples/classes/Subsub1Class.kt")
}
kotlin("Subsub2Class.kt", "jvmMain") {
resolve("examples/classes/Subsub2Class.kt")
}

}.addPluginTestRuntimeClasspath()

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

@Test
fun testApiCheckFails() {
val runner = test {
createProjectHierarchyWithPluginOnRoot()
runner {
arguments.add("--continue")
arguments.add(":check")
arguments.add("--stacktrace")
}

dir("api/") {
file("testproject.api") {
resolve("examples/classes/Subsub2Class.dump")
resolve("examples/classes/Subsub1Class.dump")
}
}

dir("src/jvmMain/kotlin") {}
kotlin("Subsub1Class.kt", "commonMain") {
resolve("examples/classes/Subsub1Class.kt")
}
kotlin("Subsub2Class.kt", "jvmMain") {
resolve("examples/classes/Subsub2Class.kt")
}

}.addPluginTestRuntimeClasspath()

runner.buildAndFail().apply {
assertTaskFailure(":jvmApiCheck")
assertTaskNotRun(":apiCheck")
assertThat(output).contains("API check failed for project testproject")
assertTaskNotRun(":check")
}
}

@Test
fun testApiDumpPasses() {
val runner = test {
createProjectHierarchyWithPluginOnRoot()

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

dir("src/jvmMain/kotlin") {}
kotlin("Subsub1Class.kt", "commonMain") {
resolve("examples/classes/Subsub1Class.kt")
}
kotlin("Subsub2Class.kt", "jvmMain") {
resolve("examples/classes/Subsub2Class.kt")
}

}.addPluginTestRuntimeClasspath()

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

val commonExpectedApi = readFileList("examples/classes/Subsub1Class.dump")

val mainExpectedApi = commonExpectedApi + "\n" + readFileList("examples/classes/Subsub2Class.dump")
assertThat(jvmApiDump.readText()).isEqualToIgnoringNewLines(mainExpectedApi)
}
}

private val jvmApiDump: File get() = rootProjectDir.resolve("api/testproject.api")

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* Copyright 2016-2021 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 org.assertj.core.api.Assertions.assertThat
import org.gradle.testkit.runner.GradleRunner
import org.junit.Test
import java.io.File
import java.io.InputStreamReader

internal class MultipleJvmTargetsTest : BaseKotlinGradleTest() {
private fun BaseKotlinScope.createProjectHierarchyWithPluginOnRoot() {
settingsGradleKts {
resolve("examples/gradle/settings/settings-name-testproject.gradle.kts")
}
buildGradleKts {
resolve("examples/gradle/base/multiplatformWithJvmTargets.gradle.kts")
}
}

@Test
fun testApiCheckPasses() {
val runner = test {
createProjectHierarchyWithPluginOnRoot()
runner {
arguments.add(":apiCheck")
}

dir("api/jvm/") {
file("testproject.api") {
resolve("examples/classes/Subsub1Class.dump")
resolve("examples/classes/Subsub2Class.dump")
}
}

dir("api/anotherJvm/") {
file("testproject.api") {
resolve("examples/classes/Subsub1Class.dump")
}
}

dir("src/jvmMain/kotlin") {}
kotlin("Subsub1Class.kt", "commonMain") {
resolve("examples/classes/Subsub1Class.kt")
}
kotlin("Subsub2Class.kt", "jvmMain") {
resolve("examples/classes/Subsub2Class.kt")
}

}.addPluginTestRuntimeClasspath()

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

@Test
fun testApiCheckFails() {
val runner = test {
createProjectHierarchyWithPluginOnRoot()
runner {
arguments.add("--continue")
arguments.add(":check")
}

dir("api/jvm/") {
file("testproject.api") {
resolve("examples/classes/Subsub2Class.dump")
resolve("examples/classes/Subsub1Class.dump")
}
}

dir("api/anotherJvm/") {
file("testproject.api") {
resolve("examples/classes/Subsub2Class.dump")
}
}

dir("src/jvmMain/kotlin") {}
kotlin("Subsub1Class.kt", "commonMain") {
resolve("examples/classes/Subsub1Class.kt")
}
kotlin("Subsub2Class.kt", "jvmMain") {
resolve("examples/classes/Subsub2Class.kt")
}

}.addPluginTestRuntimeClasspath()

runner.buildAndFail().apply {
assertTaskNotRun(":apiCheck")
assertTaskFailure(":jvmApiCheck")
assertTaskFailure(":anotherJvmApiCheck")
assertThat(output).contains("API check failed for project testproject")
assertTaskNotRun(":check")
}
}

@Test
fun testApiDumpPasses() {
val runner = test {
createProjectHierarchyWithPluginOnRoot()

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

dir("src/jvmMain/kotlin") {}
kotlin("Subsub1Class.kt", "commonMain") {
resolve("examples/classes/Subsub1Class.kt")
}
kotlin("Subsub2Class.kt", "jvmMain") {
resolve("examples/classes/Subsub2Class.kt")
}

}.addPluginTestRuntimeClasspath()
runner.build().apply {
assertTaskSuccess(":apiDump")
assertTaskSuccess(":jvmApiDump")
assertTaskSuccess(":anotherJvmApiDump")

System.err.println(output)

val anotherExpectedApi = readFileList("examples/classes/Subsub1Class.dump")
assertThat(anotherApiDump.readText()).isEqualToIgnoringNewLines(anotherExpectedApi)

val mainExpectedApi = anotherExpectedApi + "\n" + readFileList("examples/classes/Subsub2Class.dump")
assertThat(jvmApiDump.readText()).isEqualToIgnoringNewLines(mainExpectedApi)
}
}

private val jvmApiDump: File get() = rootProjectDir.resolve("api/jvm/testproject.api")
private val anotherApiDump: File get() = rootProjectDir.resolve("api/anotherJvm/testproject.api")

}
Loading

0 comments on commit 6a3180b

Please sign in to comment.