| /* |
| * Copyright 2020 Google LLC |
| * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.google.devtools.ksp.gradle |
| |
| import com.google.common.truth.Truth.assertThat |
| import com.google.devtools.ksp.gradle.processor.TestSymbolProcessorProvider |
| import com.google.devtools.ksp.gradle.testing.DependencyDeclaration.Companion.module |
| import com.google.devtools.ksp.gradle.testing.KspIntegrationTestRule |
| import com.google.devtools.ksp.gradle.testing.PluginDeclaration |
| import com.google.devtools.ksp.processing.CodeGenerator |
| import com.google.devtools.ksp.processing.Dependencies |
| import com.google.devtools.ksp.processing.Resolver |
| import com.google.devtools.ksp.processing.SymbolProcessor |
| import com.google.devtools.ksp.symbol.KSAnnotated |
| import com.google.devtools.ksp.symbol.KSClassDeclaration |
| import org.junit.Rule |
| import org.junit.Test |
| import org.junit.rules.TemporaryFolder |
| |
| class SourceSetConfigurationsTest { |
| @Rule |
| @JvmField |
| val tmpDir = TemporaryFolder() |
| |
| @Rule |
| @JvmField |
| val testRule = KspIntegrationTestRule(tmpDir) |
| |
| @Test |
| fun configurationsForJvmApp() { |
| testRule.setupAppAsJvmApp() |
| testRule.appModule.addSource("Foo.kt", "class Foo") |
| val result = testRule.runner() |
| .withArguments(":app:dependencies") |
| .build() |
| val configurations = result.output.lines().map { it.split(' ').first() } |
| |
| assertThat(configurations).containsAtLeast("ksp", "kspTest") |
| } |
| |
| @Test |
| fun configurationsForAndroidApp() { |
| testRule.setupAppAsAndroidApp() |
| testRule.appModule.addSource("Foo.kt", "class Foo") |
| val result = testRule.runner() |
| .withArguments(":app:dependencies") |
| .build() |
| val configurations = result.output.lines().map { it.split(' ').first() } |
| |
| assertThat(configurations).containsAtLeast( |
| "ksp", |
| "kspAndroidTest", |
| "kspAndroidTestDebug", |
| "kspAndroidTestRelease", |
| "kspDebug", |
| "kspRelease", |
| "kspTest", |
| "kspTestDebug", |
| "kspTestRelease" |
| ) |
| } |
| |
| @Test |
| fun configurationsForMultiplatformApp() { |
| testRule.setupAppAsMultiplatformApp(""" |
| kotlin { |
| jvm { } |
| android(name = "foo") { } |
| js { browser() } |
| androidNativeX86 { } |
| androidNativeX64(name = "bar") { } |
| } |
| """.trimIndent()) |
| testRule.appModule.addMultiplatformSource("commonMain", "Foo.kt", "class Foo") |
| val result = testRule.runner() |
| .withArguments(":app:dependencies") |
| .build() |
| val configurations = result.output.lines().map { it.split(' ').first() } |
| |
| assertThat(configurations).containsAtLeast( |
| // jvm target: |
| "kspJvm", |
| "kspJvmTest", |
| // android target, named foo: |
| "kspFoo", |
| "kspFooAndroidTest", |
| "kspFooAndroidTestDebug", |
| "kspFooAndroidTestRelease", |
| "kspFooDebug", |
| "kspFooRelease", |
| "kspFooTest", |
| "kspFooTestDebug", |
| "kspFooTestRelease", |
| // js target: |
| "kspJs", |
| "kspJsTest", |
| // androidNativeX86 target: |
| "kspAndroidNativeX86", |
| "kspAndroidNativeX86Test", |
| // androidNative64 target, named bar: |
| "kspBar", |
| "kspBarTest" |
| ) |
| } |
| |
| @Test |
| fun configurationsForMultiplatformApp_doesNotCrossCompilationBoundaries() { |
| // Adding a ksp dependency on jvmParent should not leak into jvmChild compilation, |
| // even if the source sets depend on each other. This works because we use |
| // KotlinCompilation.kotlinSourceSets instead of KotlinCompilation.allKotlinSourceSets |
| testRule.setupAppAsMultiplatformApp(""" |
| kotlin { |
| jvm("jvmParent") { } |
| jvm("jvmChild") { } |
| } |
| """.trimIndent()) |
| testRule.appModule.addMultiplatformSource("commonMain", "Foo.kt", "class Foo") |
| testRule.appModule.buildFileAdditions.add(""" |
| kotlin { |
| sourceSets { |
| this["jvmChildMain"].dependsOn(this["jvmParentMain"]) |
| } |
| } |
| dependencies { |
| add("kspJvmParent", "androidx.room:room-compiler:2.3.0") |
| } |
| tasks.register("checkConfigurations") { |
| doLast { |
| // child has no dependencies, so task is not created. |
| val parent = tasks.findByName("kspKotlinJvmParent") |
| val child = tasks.findByName("kspKotlinJvmChild") |
| require(parent != null) |
| require(child == null) |
| } |
| } |
| """.trimIndent()) |
| testRule.runner() |
| .withArguments(":app:checkConfigurations") |
| .build() |
| } |
| |
| @Test |
| fun registerJavaSourcesToAndroid() { |
| testRule.setupAppAsAndroidApp() |
| testRule.appModule.dependencies.addAll( |
| listOf( |
| module("ksp", testRule.processorModule), |
| module("kspTest", testRule.processorModule), |
| module("kspAndroidTest", testRule.processorModule) |
| ) |
| ) |
| testRule.appModule.buildFileAdditions.add( |
| """ |
| tasks.register("printSources") { |
| fun logVariantSources(variants: DomainObjectSet<out com.android.build.gradle.api.BaseVariant>) { |
| variants.all { |
| println("VARIANT:" + this.name) |
| val baseVariant = (this as com.android.build.gradle.internal.api.BaseVariantImpl) |
| val variantData = baseVariant::class.java.getMethod("getVariantData").invoke(baseVariant) |
| as com.android.build.gradle.internal.variant.BaseVariantData |
| variantData.extraGeneratedSourceFolders.forEach { |
| println("SRC:" + it.relativeTo(buildDir).path) |
| } |
| variantData.allPreJavacGeneratedBytecode.forEach { |
| println("BYTE:" + it.relativeTo(buildDir).path) |
| } |
| } |
| } |
| doLast { |
| logVariantSources(android.applicationVariants) |
| logVariantSources(android.testVariants) |
| logVariantSources(android.unitTestVariants) |
| } |
| } |
| """.trimIndent() |
| ) |
| val result = testRule.runner().withDebug(true).withArguments(":app:printSources").build() |
| |
| data class SourceFolder( |
| val variantName: String, |
| val path: String |
| ) |
| // parse output to get variant names and sources |
| // variant name -> list of sources |
| val variantSources = mutableListOf<SourceFolder>() |
| lateinit var currentVariantName: String |
| result.output.lines().forEach { line -> |
| when { |
| line.startsWith("VARIANT:") -> { |
| currentVariantName = line.substring("VARIANT:".length) |
| } |
| line.startsWith("SRC:") -> { |
| variantSources.add( |
| SourceFolder( |
| variantName = currentVariantName, |
| path = line |
| ) |
| ) |
| } |
| |
| line.startsWith("BYTE:") -> { |
| variantSources.add( |
| SourceFolder( |
| variantName = currentVariantName, |
| path = line |
| ) |
| ) |
| } |
| } |
| } |
| assertThat( |
| variantSources.filter { |
| // there might be more, we are only interested in ksp |
| it.path.contains("ksp") |
| } |
| ).containsExactly( |
| SourceFolder( |
| "debug", "SRC:generated/ksp/debug/java" |
| ), |
| SourceFolder( |
| "release", "SRC:generated/ksp/release/java" |
| ), |
| SourceFolder( |
| "debugAndroidTest", "SRC:generated/ksp/debugAndroidTest/java" |
| ), |
| SourceFolder( |
| "debugUnitTest", "SRC:generated/ksp/debugUnitTest/java" |
| ), |
| SourceFolder( |
| "releaseUnitTest", "SRC:generated/ksp/releaseUnitTest/java" |
| ), |
| // TODO byte sources seems to be overridden by tmp/kotlin-classes/debug |
| // assert them as well once fixed |
| ) |
| } |
| |
| @Test |
| fun configurationsForAndroidApp_withBuildFlavorsMatchesKapt() { |
| testRule.setupAppAsAndroidApp() |
| testRule.appModule.buildFileAdditions.add( |
| """ |
| android { |
| flavorDimensions("version") |
| productFlavors { |
| create("free") { |
| dimension = "version" |
| applicationId = "foo.bar" |
| } |
| create("paid") { |
| dimension = "version" |
| applicationId = "foo.baz" |
| } |
| } |
| } |
| """.trimIndent() |
| ) |
| testRule.appModule.plugins.add(PluginDeclaration.kotlin("kapt", testRule.testConfig.kotlinBaseVersion)) |
| testRule.appModule.addSource("Foo.kt", "class Foo") |
| val result = testRule.runner() |
| .withArguments(":app:dependencies") |
| .build() |
| |
| // kaptClasspath_* seem to be intermediate configurations that never run. |
| val configurations = result.output.lines().map { it.split(' ').first() } |
| val kaptConfigurations = configurations.filter { |
| it.startsWith("kapt") && !it.startsWith("kaptClasspath_") |
| } |
| val kspConfigurations = configurations.filter { |
| it.startsWith("ksp") |
| } |
| assertThat(kspConfigurations).containsExactlyElementsIn( |
| kaptConfigurations.map { |
| it.replace("kapt", "ksp") |
| } |
| ) |
| assertThat(kspConfigurations).isNotEmpty() |
| } |
| |
| @Test |
| fun kspForTests_jvm() { |
| kspForTests(androidApp = false, useAndroidTest = false) |
| } |
| |
| @Test |
| fun kspForTests_android_androidTest() { |
| kspForTests(androidApp = true, useAndroidTest = true) |
| } |
| |
| @Test |
| fun kspForTests_android_junit() { |
| kspForTests(androidApp = true, useAndroidTest = false) |
| } |
| |
| private fun kspForTests(androidApp: Boolean, useAndroidTest: Boolean) { |
| if (androidApp) { |
| testRule.setupAppAsAndroidApp() |
| } else { |
| testRule.setupAppAsJvmApp() |
| } |
| if (useAndroidTest) { |
| check(androidApp) { |
| "cannot set use android test w/o android app" |
| } |
| } |
| |
| testRule.appModule.addSource( |
| "App.kt", |
| """ |
| @Suppress("app") |
| class InApp { |
| } |
| """.trimIndent() |
| ) |
| val testSource = """ |
| @Suppress("test") |
| class InTest { |
| val impl = InTest_Impl() |
| } |
| """.trimIndent() |
| if (useAndroidTest) { |
| testRule.appModule.addAndroidTestSource("InTest.kt", testSource) |
| } else { |
| testRule.appModule.addTestSource("InTest.kt", testSource) |
| } |
| |
| class Processor(val codeGenerator: CodeGenerator) : SymbolProcessor { |
| override fun process(resolver: Resolver): List<KSAnnotated> { |
| resolver.getSymbolsWithAnnotation(Suppress::class.qualifiedName!!) |
| .filterIsInstance<KSClassDeclaration>() |
| .forEach { |
| if (it.simpleName.asString() == "InApp") { |
| error("should not run on the app sources") |
| } |
| val genClassName = "${it.simpleName.asString()}_Impl" |
| codeGenerator.createNewFile(Dependencies.ALL_FILES, "", genClassName).use { |
| it.writer().use { |
| it.write("class $genClassName") |
| } |
| } |
| } |
| return emptyList() |
| } |
| } |
| |
| class Provider : TestSymbolProcessorProvider({ env -> Processor(env.codeGenerator) }) |
| |
| testRule.addProvider(Provider::class) |
| if (useAndroidTest) { |
| testRule.appModule.dependencies.add( |
| module("kspAndroidTest", testRule.processorModule) |
| ) |
| testRule.runner().withArguments(":processor:assemble", ":app:assembleAndroidTest", "--stacktrace").build() |
| } else { |
| testRule.appModule.dependencies.add( |
| module("kspTest", testRule.processorModule) |
| ) |
| testRule.runner().withArguments(":app:test", "--stacktrace").build() |
| } |
| } |
| } |