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