1 /*
<lambda>null2  * Copyright 2020 Google LLC
3  * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  * http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 package com.google.devtools.ksp.gradle
18 
19 import com.google.common.truth.Truth.assertThat
20 import com.google.devtools.ksp.gradle.processor.TestSymbolProcessorProvider
21 import com.google.devtools.ksp.gradle.testing.DependencyDeclaration.Companion.module
22 import com.google.devtools.ksp.gradle.testing.KspIntegrationTestRule
23 import com.google.devtools.ksp.gradle.testing.PluginDeclaration
24 import com.google.devtools.ksp.processing.CodeGenerator
25 import com.google.devtools.ksp.processing.Dependencies
26 import com.google.devtools.ksp.processing.Resolver
27 import com.google.devtools.ksp.processing.SymbolProcessor
28 import com.google.devtools.ksp.symbol.KSAnnotated
29 import com.google.devtools.ksp.symbol.KSClassDeclaration
30 import org.junit.Rule
31 import org.junit.Test
32 import org.junit.rules.TemporaryFolder
33 import java.io.File
34 
35 class SourceSetConfigurationsTest {
36     @Rule
37     @JvmField
38     val tmpDir = TemporaryFolder()
39 
40     @Rule
41     @JvmField
42     val testRule = KspIntegrationTestRule(tmpDir)
43 
44     @Test
45     fun configurationsForJvmApp() {
46         testRule.setupAppAsJvmApp()
47         testRule.appModule.addSource("Foo.kt", "class Foo")
48         val result = testRule.runner()
49             .withArguments(":app:dependencies")
50             .build()
51         val configurations = result.output.lines().map { it.split(' ').first() }
52 
53         assertThat(configurations).containsAtLeast("ksp", "kspTest")
54     }
55 
56     @Test
57     fun configurationsForAndroidApp() {
58         testRule.setupAppAsAndroidApp()
59         testRule.appModule.addSource("Foo.kt", "class Foo")
60         val result = testRule.runner()
61             .withArguments(":app:dependencies")
62             .build()
63         val configurations = result.output.lines().map { it.split(' ').first() }
64 
65         assertThat(configurations).containsAtLeast(
66             "ksp",
67             "kspAndroidTest",
68             "kspAndroidTestDebug",
69             "kspAndroidTestRelease",
70             "kspDebug",
71             "kspRelease",
72             "kspTest",
73             "kspTestDebug",
74             "kspTestRelease"
75         )
76     }
77 
78     @Test
79     fun configurationsForMultiplatformApp() {
80         testRule.setupAppAsMultiplatformApp(
81             """
82                 kotlin {
83                     jvm { }
84                     android(name = "foo") { }
85                     js(BOTH) { browser() }
86                     androidNativeX86 { }
87                     androidNativeX64(name = "bar") { }
88                 }
89 
90                 tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
91                     kotlinOptions.freeCompilerArgs += "-Xuse-deprecated-legacy-compiler"
92                 }
93             """.trimIndent()
94         )
95         testRule.appModule.addMultiplatformSource("commonMain", "Foo.kt", "class Foo")
96         val result = testRule.runner()
97             .withArguments(":app:dependencies")
98             .build()
99         val configurations = result.output.lines().map { it.split(' ').first() }
100 
101         assertThat(configurations).containsAtLeast(
102             // jvm target:
103             "kspJvm",
104             "kspJvmTest",
105             // android target, named foo:
106             "kspFoo",
107             "kspFooAndroidTest",
108             "kspFooAndroidTestDebug",
109             "kspFooAndroidTestRelease",
110             "kspFooDebug",
111             "kspFooRelease",
112             "kspFooTest",
113             "kspFooTestDebug",
114             "kspFooTestRelease",
115             // js target:
116             "kspJs",
117             "kspJsTest",
118             // androidNativeX86 target:
119             "kspAndroidNativeX86",
120             "kspAndroidNativeX86Test",
121             // androidNative64 target, named bar:
122             "kspBar",
123             "kspBarTest"
124         )
125     }
126 
127     @Test
128     fun configurationsForMultiplatformApp_doesNotCrossCompilationBoundaries() {
129         // Adding a ksp dependency on jvmParent should not leak into jvmChild compilation,
130         // even if the source sets depend on each other. This works because we use
131         // KotlinCompilation.kotlinSourceSets instead of KotlinCompilation.allKotlinSourceSets
132         testRule.setupAppAsMultiplatformApp(
133             """
134                 kotlin {
135                     jvm("jvmParent") { }
136                     jvm("jvmChild") { }
137                 }
138             """.trimIndent()
139         )
140         testRule.appModule.addMultiplatformSource("commonMain", "Foo.kt", "class Foo")
141         testRule.appModule.buildFileAdditions.add(
142             """
143                 kotlin {
144                     sourceSets {
145                         this["jvmChildMain"].dependsOn(this["jvmParentMain"])
146                     }
147                 }
148                 dependencies {
149                     add("kspJvmParent", "androidx.room:room-compiler:2.4.2")
150                 }
151                 tasks.register("checkConfigurations") {
152                     doLast {
153                         // child has no dependencies, so task is not created.
154                         val parent = tasks.findByName("kspKotlinJvmParent")
155                         val child = tasks.findByName("kspKotlinJvmChild")
156                         require(parent != null)
157                         require(child == null)
158                     }
159                 }
160             """.trimIndent()
161         )
162         testRule.runner()
163             .withArguments(":app:checkConfigurations")
164             .build()
165     }
166 
167     @Test
168     fun registerGeneratedSourcesToAndroid() {
169         testRule.setupAppAsAndroidApp()
170         testRule.appModule.dependencies.addAll(
171             listOf(
172                 module("ksp", testRule.processorModule),
173                 module("kspTest", testRule.processorModule),
174                 module("kspAndroidTest", testRule.processorModule)
175             )
176         )
177         testRule.appModule.buildFileAdditions.add(
178             """
179             tasks.register("printSources") {
180                 fun logVariantSources(variants: DomainObjectSet<out com.android.build.gradle.api.BaseVariant>) {
181                     variants.all {
182                         println("VARIANT:" + this.name)
183                         val baseVariant = (this as com.android.build.gradle.internal.api.BaseVariantImpl)
184                         val variantData = baseVariant::class.java.getMethod("getVariantData").invoke(baseVariant)
185                             as com.android.build.gradle.internal.variant.BaseVariantData
186                         variantData.extraGeneratedSourceFolders.forEach {
187                             println("SRC:" + it.relativeTo(buildDir).path)
188                         }
189                         variantData.allPreJavacGeneratedBytecode.forEach {
190                             println("BYTE:" + it.relativeTo(buildDir).path)
191                         }
192                     }
193                 }
194                 doLast {
195                     logVariantSources(android.applicationVariants)
196                     logVariantSources(android.testVariants)
197                     logVariantSources(android.unitTestVariants)
198                 }
199             }
200             """.trimIndent()
201         )
202         val result = testRule.runner().withDebug(true).withArguments(":app:printSources").build()
203 
204         data class SourceFolder(
205             val variantName: String,
206             val path: String
207         )
208 
209         fun String.normalizePath() = replace(File.separatorChar, '/')
210         // parse output to get variant names and sources
211         // variant name -> list of sources
212         val variantSources = mutableListOf<SourceFolder>()
213         lateinit var currentVariantName: String
214         result.output.lines().forEach { line ->
215             when {
216                 line.startsWith("VARIANT:") -> {
217                     currentVariantName = line.substring("VARIANT:".length)
218                 }
219                 line.startsWith("SRC:") -> {
220                     variantSources.add(
221                         SourceFolder(
222                             variantName = currentVariantName,
223                             path = line.normalizePath()
224                         )
225                     )
226                 }
227 
228                 line.startsWith("BYTE:") -> {
229                     variantSources.add(
230                         SourceFolder(
231                             variantName = currentVariantName,
232                             path = line.normalizePath()
233                         )
234                     )
235                 }
236             }
237         }
238         assertThat(
239             variantSources.filter {
240                 // there might be more, we are only interested in ksp
241                 it.path.contains("ksp")
242             }
243         ).containsExactly(
244             SourceFolder(
245                 "debug", "SRC:generated/ksp/debug/java"
246             ),
247             SourceFolder(
248                 "release", "SRC:generated/ksp/release/java"
249             ),
250             SourceFolder(
251                 "debugAndroidTest", "SRC:generated/ksp/debugAndroidTest/java"
252             ),
253             SourceFolder(
254                 "debugUnitTest", "SRC:generated/ksp/debugUnitTest/java"
255             ),
256             SourceFolder(
257                 "releaseUnitTest", "SRC:generated/ksp/releaseUnitTest/java"
258             ),
259             SourceFolder(
260                 "debug", "SRC:generated/ksp/debug/kotlin"
261             ),
262             SourceFolder(
263                 "release", "SRC:generated/ksp/release/kotlin"
264             ),
265             SourceFolder(
266                 "debugAndroidTest", "SRC:generated/ksp/debugAndroidTest/kotlin"
267             ),
268             SourceFolder(
269                 "debugUnitTest", "SRC:generated/ksp/debugUnitTest/kotlin"
270             ),
271             SourceFolder(
272                 "releaseUnitTest", "SRC:generated/ksp/releaseUnitTest/kotlin"
273             ),
274             // TODO byte sources seems to be overridden by tmp/kotlin-classes/debug
275             //  assert them as well once fixed
276         )
277     }
278 
279     @Test
280     fun configurationsForAndroidApp_withBuildFlavorsMatchesKapt() {
281         testRule.setupAppAsAndroidApp()
282         testRule.appModule.buildFileAdditions.add(
283             """
284             android {
285                 flavorDimensions("version")
286                 productFlavors {
287                     create("free") {
288                         dimension = "version"
289                         applicationId = "foo.bar"
290                     }
291                     create("paid") {
292                         dimension = "version"
293                         applicationId = "foo.baz"
294                     }
295                 }
296             }
297             """.trimIndent()
298         )
299         testRule.appModule.plugins.add(PluginDeclaration.kotlin("kapt", testRule.testConfig.kotlinBaseVersion))
300         testRule.appModule.addSource("Foo.kt", "class Foo")
301         val result = testRule.runner()
302             .withArguments(":app:dependencies")
303             .build()
304 
305         // kaptClasspath_* seem to be intermediate configurations that never run.
306         val configurations = result.output.lines().map { it.split(' ').first() }
307         val kaptConfigurations = configurations.filter {
308             it.startsWith("kapt") && !it.startsWith("kaptClasspath_")
309         }
310         val kspConfigurations = configurations.filter {
311             it.startsWith("ksp")
312         }
313         assertThat(kspConfigurations).containsExactlyElementsIn(
314             kaptConfigurations.map {
315                 it.replace("kapt", "ksp")
316             }
317         )
318         assertThat(kspConfigurations).isNotEmpty()
319     }
320 
321     @Test
322     fun kspForTests_jvm() {
323         kspForTests(androidApp = false, useAndroidTest = false)
324     }
325 
326     @Test
327     fun kspForTests_android_androidTest() {
328         kspForTests(androidApp = true, useAndroidTest = true)
329     }
330 
331     @Test
332     fun kspForTests_android_junit() {
333         kspForTests(androidApp = true, useAndroidTest = false)
334     }
335 
336     private fun kspForTests(androidApp: Boolean, useAndroidTest: Boolean) {
337         if (androidApp) {
338             testRule.setupAppAsAndroidApp()
339         } else {
340             testRule.setupAppAsJvmApp()
341         }
342         if (useAndroidTest) {
343             check(androidApp) {
344                 "cannot set use android test w/o android app"
345             }
346         }
347 
348         testRule.appModule.addSource(
349             "App.kt",
350             """
351             @Suppress("app")
352             class InApp {
353             }
354             """.trimIndent()
355         )
356         val testSource = """
357                 @Suppress("test")
358                 class InTest {
359                     val impl = InTest_Impl()
360                 }
361         """.trimIndent()
362         if (useAndroidTest) {
363             testRule.appModule.addAndroidTestSource("InTest.kt", testSource)
364         } else {
365             testRule.appModule.addTestSource("InTest.kt", testSource)
366         }
367 
368         class Processor(val codeGenerator: CodeGenerator) : SymbolProcessor {
369             override fun process(resolver: Resolver): List<KSAnnotated> {
370                 resolver.getSymbolsWithAnnotation(Suppress::class.qualifiedName!!)
371                     .filterIsInstance<KSClassDeclaration>()
372                     .forEach {
373                         if (it.simpleName.asString() == "InApp") {
374                             error("should not run on the app sources")
375                         }
376                         val genClassName = "${it.simpleName.asString()}_Impl"
377                         codeGenerator.createNewFile(Dependencies.ALL_FILES, "", genClassName).use {
378                             it.writer().use {
379                                 it.write("class $genClassName")
380                             }
381                         }
382                     }
383                 return emptyList()
384             }
385         }
386 
387         class Provider : TestSymbolProcessorProvider({ env -> Processor(env.codeGenerator) })
388 
389         testRule.addProvider(Provider::class)
390         if (useAndroidTest) {
391             testRule.appModule.dependencies.add(
392                 module("kspAndroidTest", testRule.processorModule)
393             )
394             testRule.runner().withArguments(":processor:assemble", ":app:assembleAndroidTest", "--stacktrace").build()
395         } else {
396             testRule.appModule.dependencies.add(
397                 module("kspTest", testRule.processorModule)
398             )
399             testRule.runner().withArguments(":app:test", "--stacktrace").build()
400         }
401     }
402 }
403