blob: 62252e0dded692ff935cfaad9922eb27cf29b4e7 [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* 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 androidx.room.compiler.processing
import androidx.room.compiler.processing.javac.JavacBasicAnnotationProcessor
import androidx.room.compiler.processing.ksp.KspBasicAnnotationProcessor
import androidx.room.compiler.processing.ksp.KspElement
import androidx.room.compiler.processing.testcode.MainAnnotation
import androidx.room.compiler.processing.testcode.OtherAnnotation
import androidx.room.compiler.processing.testcode.SingleTypeValueAnnotation
import androidx.room.compiler.processing.util.CompilationTestCapabilities
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.className
import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
import androidx.room.compiler.processing.util.compiler.compile
import com.google.common.truth.Truth.assertAbout
import com.google.common.truth.Truth.assertThat
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.testing.compile.JavaFileObjects
import com.google.testing.compile.JavaSourcesSubjectFactory
import com.squareup.javapoet.AnnotationSpec
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.TypeName
import com.squareup.javapoet.TypeSpec
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import javax.tools.Diagnostic
import kotlin.reflect.KClass
class XProcessingStepTest {
@field:Rule
@JvmField
val temporaryFolder = TemporaryFolder()
@Test
fun xProcessingStep() {
val annotatedElements = mutableMapOf<KClass<out Annotation>, String>()
val processingStep = object : XProcessingStep {
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XTypeElement> {
elementsByAnnotation[OtherAnnotation::class.qualifiedName]
?.filterIsInstance<XTypeElement>()
?.forEach {
annotatedElements[OtherAnnotation::class] = it.qualifiedName
}
elementsByAnnotation[MainAnnotation::class.qualifiedName]
?.filterIsInstance<XTypeElement>()
?.forEach {
annotatedElements[MainAnnotation::class] = it.qualifiedName
}
return emptySet()
}
override fun annotations(): Set<String> {
return setOf(
OtherAnnotation::class.qualifiedName!!,
MainAnnotation::class.qualifiedName!!
)
}
}
val mainProcessor = object : JavacBasicAnnotationProcessor() {
override fun processingSteps() = listOf(processingStep)
}
val main = JavaFileObjects.forSourceString(
"foo.bar.Main",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@MainAnnotation(
typeList = {},
singleType = Object.class,
intMethod = 3,
singleOtherAnnotation = @OtherAnnotation("y")
)
class Main {
}
""".trimIndent()
)
val other = JavaFileObjects.forSourceString(
"foo.bar.Other",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@OtherAnnotation("x")
class Other {
}
""".trimIndent()
)
assertAbout(
JavaSourcesSubjectFactory.javaSources()
).that(
listOf(main, other)
).processedWith(
mainProcessor
).compilesWithoutError()
assertThat(annotatedElements).containsExactlyEntriesIn(
mapOf(
MainAnnotation::class to "foo.bar.Main",
OtherAnnotation::class to "foo.bar.Other"
)
)
}
@Test
fun multiStepProcessing() {
val otherAnnotatedElements = mutableListOf<TypeName>()
// create a scenario where we run multi-step processing so that we can test caching
val processingStep = object : XProcessingStep {
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XTypeElement> {
// for each element annotated with Main annotation, create a class with Other
// annotation to trigger another round
elementsByAnnotation[MainAnnotation::class.qualifiedName]
?.filterIsInstance<XTypeElement>()
?.forEach {
val className = ClassName.get(it.packageName, "${it.name}_Impl")
val spec = TypeSpec.classBuilder(className)
.addAnnotation(
AnnotationSpec.builder(OtherAnnotation::class.java).apply {
addMember("value", "\"foo\"")
}.build()
)
.build()
JavaFile.builder(className.packageName(), spec)
.build()
.writeTo(env.filer)
}
elementsByAnnotation[OtherAnnotation::class.qualifiedName]
?.filterIsInstance<XTypeElement>()
?.forEach {
otherAnnotatedElements.add(it.type.typeName)
}
return emptySet()
}
override fun annotations(): Set<String> {
return setOf(
OtherAnnotation::class.qualifiedName!!,
MainAnnotation::class.qualifiedName!!
)
}
}
val main = JavaFileObjects.forSourceString(
"foo.bar.Main",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@MainAnnotation(
typeList = {},
singleType = Object.class,
intMethod = 3,
singleOtherAnnotation = @OtherAnnotation("y")
)
class Main {
}
""".trimIndent()
)
assertAbout(
JavaSourcesSubjectFactory.javaSources()
).that(
listOf(main)
).processedWith(
object : JavacBasicAnnotationProcessor() {
override fun processingSteps() = listOf(processingStep)
}
).compilesWithoutError()
assertThat(otherAnnotatedElements).containsExactly(
ClassName.get("foo.bar", "Main_Impl")
)
}
@Test
fun caching() {
val elementPerRound = mutableMapOf<Int, List<XTypeElement>>()
// create a scenario where we run multi-step processing so that we can test caching
val processingStep = object : XProcessingStep {
var roundCounter = 0
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XTypeElement> {
elementPerRound[roundCounter++] = listOf(
env.requireTypeElement("foo.bar.Main"),
env.requireTypeElement("foo.bar.Main")
)
// trigger another round
elementsByAnnotation[MainAnnotation::class.qualifiedName]
?.filterIsInstance<XTypeElement>()
?.forEach {
val className = ClassName.get(it.packageName, "${it.name}_Impl")
val spec = TypeSpec.classBuilder(className)
.addAnnotation(
AnnotationSpec.builder(OtherAnnotation::class.java).apply {
addMember("value", "\"foo\"")
}.build()
)
.build()
JavaFile.builder(className.packageName(), spec)
.build()
.writeTo(env.filer)
}
return emptySet()
}
override fun annotations(): Set<String> {
return setOf(
OtherAnnotation::class.qualifiedName!!,
MainAnnotation::class.qualifiedName!!
)
}
}
val main = JavaFileObjects.forSourceString(
"foo.bar.Main",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@MainAnnotation(
typeList = {},
singleType = Object.class,
intMethod = 3,
singleOtherAnnotation = @OtherAnnotation("y")
)
class Main {
}
""".trimIndent()
)
assertAbout(
JavaSourcesSubjectFactory.javaSources()
).that(
listOf(main)
).processedWith(
object : JavacBasicAnnotationProcessor() {
override fun processingSteps() = listOf(processingStep)
}
).compilesWithoutError()
assertThat(elementPerRound).hasSize(2)
// in each round, we should be returning the same instance of the TypeElement while
// returning different instances between different rounds
elementPerRound.values.forEach {
// test sanity, we read it twice
assertThat(it).hasSize(2)
assertThat(it[0]).isSameInstanceAs(it[1])
}
// make sure elements between different rounds are not the same instances
assertThat(elementPerRound.get(0)?.get(0))
.isNotSameInstanceAs(elementPerRound.get(1)?.get(0))
}
@Test
fun javacProcessingEnvCaching() {
// Create a scenario to test that the xProcessingEnv instance is available before processing
// and that the xProcessingEnv is the same instance across rounds.
val main = JavaFileObjects.forSourceString(
"foo.bar.Main",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@MainAnnotation(
typeList = {},
singleType = Object.class,
intMethod = 3,
singleOtherAnnotation = @OtherAnnotation("y")
)
class Main {
}
""".trimIndent()
)
val processingEnvPerRound = mutableMapOf<Int, XProcessingEnv>()
val processingStep = object : XProcessingStep {
var roundCounter = 0
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XTypeElement> {
processingEnvPerRound[roundCounter++] = env
// trigger another round
elementsByAnnotation[MainAnnotation::class.qualifiedName]
?.filterIsInstance<XTypeElement>()
?.forEach {
val className = ClassName.get(it.packageName, "${it.name}_Impl")
val spec = TypeSpec.classBuilder(className)
.addAnnotation(
AnnotationSpec.builder(OtherAnnotation::class.java).apply {
addMember("value", "\"foo\"")
}.build()
)
.build()
JavaFile.builder(className.packageName(), spec)
.build()
.writeTo(env.filer)
}
return emptySet()
}
override fun annotations(): Set<String> {
return setOf(
OtherAnnotation::class.qualifiedName!!,
MainAnnotation::class.qualifiedName!!
)
}
}
val xProcessingEnvs = mutableListOf<XProcessingEnv>()
assertAbout(
JavaSourcesSubjectFactory.javaSources()
).that(
listOf(main)
).processedWith(
object : JavacBasicAnnotationProcessor() {
override fun processingSteps(): Iterable<XProcessingStep> {
xProcessingEnvs.add(xProcessingEnv)
return listOf(processingStep)
}
}
).compilesWithoutError()
// Makes sure processingSteps() was only called once, and that the xProcessingEnv was set.
assertThat(xProcessingEnvs).hasSize(1)
assertThat(xProcessingEnvs.get(0)).isNotNull()
// Make sure there were two rounds, and processingEnv between rounds is the same instance.
assertThat(processingEnvPerRound).hasSize(2)
assertThat(xProcessingEnvs.get(0)).isSameInstanceAs(processingEnvPerRound.get(0))
assertThat(xProcessingEnvs.get(0)).isSameInstanceAs(processingEnvPerRound.get(1))
}
@Test
fun kspProcessingEnvCaching() {
val main = Source.java(
"foo.bar.Main",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@MainAnnotation(
typeList = {},
singleType = Object.class,
intMethod = 3,
singleOtherAnnotation = @OtherAnnotation("y")
)
class Main {
}
""".trimIndent()
)
val processingEnvPerRound = mutableMapOf<Int, XProcessingEnv>()
// create a scenario where we run multi-step processing so that we can test caching
val processingStep = object : XProcessingStep {
var roundCounter = 0
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XTypeElement> {
processingEnvPerRound[roundCounter++] = env
// trigger another round
elementsByAnnotation[MainAnnotation::class.qualifiedName]
?.filterIsInstance<XTypeElement>()
?.forEach {
val className = ClassName.get(it.packageName, "${it.name}_Impl")
val spec = TypeSpec.classBuilder(className)
.addAnnotation(
AnnotationSpec.builder(OtherAnnotation::class.java).apply {
addMember("value", "\"foo\"")
}.build()
)
.build()
JavaFile.builder(className.packageName(), spec)
.build()
.writeTo(env.filer)
}
return emptySet()
}
override fun annotations(): Set<String> {
return setOf(
OtherAnnotation::class.qualifiedName!!,
MainAnnotation::class.qualifiedName!!
)
}
}
val xProcessingEnvs = mutableListOf<XProcessingEnv>()
val processorProvider = object : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return object : KspBasicAnnotationProcessor(environment) {
override fun processingSteps(): Iterable<XProcessingStep> {
xProcessingEnvs.add(xProcessingEnv)
return listOf(processingStep)
}
}
}
}
compile(
workingDir = temporaryFolder.root,
arguments = TestCompilationArguments(
sources = listOf(main),
symbolProcessorProviders = listOf(processorProvider)
)
)
// Makes sure processingSteps() was only called once, and that the xProcessingEnv was set.
assertThat(xProcessingEnvs).hasSize(1)
assertThat(xProcessingEnvs.get(0)).isNotNull()
// Make sure there were two rounds, and processingEnv between rounds is the same instance.
assertThat(processingEnvPerRound).hasSize(2)
assertThat(xProcessingEnvs.get(0)).isSameInstanceAs(processingEnvPerRound.get(0))
assertThat(xProcessingEnvs.get(0)).isSameInstanceAs(processingEnvPerRound.get(1))
}
@Test
fun cachingBetweenSteps() {
val main = JavaFileObjects.forSourceString(
"foo.bar.Main",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@MainAnnotation(
typeList = {},
singleType = Object.class,
intMethod = 3,
singleOtherAnnotation = @OtherAnnotation("y")
)
class Main {}
""".trimIndent()
)
val other = JavaFileObjects.forSourceString(
"foo.bar.Other",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@OtherAnnotation("x")
class Other {
}
""".trimIndent()
)
val elementsByStep = mutableMapOf<XProcessingStep, XTypeElement>()
// create a scenario where we can test caching between steps
val mainStep = object : XProcessingStep {
override fun annotations(): Set<String> = setOf(MainAnnotation::class.qualifiedName!!)
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XTypeElement> {
elementsByStep[this] = env.requireTypeElement("foo.bar.Main")
return emptySet()
}
}
val otherStep = object : XProcessingStep {
override fun annotations(): Set<String> = setOf(OtherAnnotation::class.qualifiedName!!)
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XTypeElement> {
elementsByStep[this] = env.requireTypeElement("foo.bar.Main")
return emptySet()
}
}
assertAbout(
JavaSourcesSubjectFactory.javaSources()
).that(
listOf(main, other)
).processedWith(
object : JavacBasicAnnotationProcessor() {
override fun processingSteps() = listOf(mainStep, otherStep)
}
).compilesWithoutError()
assertThat(elementsByStep.keys).containsExactly(mainStep, otherStep)
// make sure elements between steps are the same instances
assertThat(elementsByStep[mainStep]).isSameInstanceAs(elementsByStep[otherStep])
}
@Test
fun javacReturnsUnprocessed() {
val processingStep = object : XProcessingStep {
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XElement> {
return elementsByAnnotation.values.flatten().toSet()
}
override fun annotations(): Set<String> {
return setOf(OtherAnnotation::class.qualifiedName!!)
}
}
val main = Source.java(
"foo.bar.Other",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@OtherAnnotation("y")
class Other {
}
""".trimIndent()
)
assertAbout(
JavaSourcesSubjectFactory.javaSources()
).that(
listOf(main.toJFO())
).processedWith(
object : JavacBasicAnnotationProcessor() {
override fun processingSteps() = listOf(processingStep)
}
).failsToCompile()
// processor name is 'null' because it is a local anonymous class
.withErrorContaining(
"null was unable to process 'foo.bar.Other' because not all of its dependencies " +
"could be resolved. Check for compilation errors or a circular dependency " +
"with generated code."
)
}
@Test
fun kspReturnsUnprocessed() {
CompilationTestCapabilities.assumeKspIsEnabled()
var returned: Set<XElement>? = null
val processingStep = object : XProcessingStep {
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XElement> {
return elementsByAnnotation.values
.flatten()
.toSet()
.also { returned = it }
}
override fun annotations(): Set<String> {
return setOf(OtherAnnotation::class.qualifiedName!!)
}
}
val processorProvider = object : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return object : KspBasicAnnotationProcessor(environment) {
override fun processingSteps() = listOf(processingStep)
}
}
}
val main = Source.kotlin(
"Other.kt",
"""
package foo.bar
import androidx.room.compiler.processing.testcode.*
@OtherAnnotation("y")
class Other {
}
""".trimIndent()
)
val result = compile(
workingDir = temporaryFolder.root,
arguments = TestCompilationArguments(
sources = listOf(main),
symbolProcessorProviders = listOf(processorProvider)
)
)
assertThat(returned).apply {
isNotNull()
isNotEmpty()
}
val element =
returned!!.map { (it as KspElement).declaration }.first() as KSClassDeclaration
assertThat(element.classKind).isEqualTo(ClassKind.CLASS)
assertThat(element.qualifiedName!!.asString()).isEqualTo("foo.bar.Other")
assertThat(result.success).isFalse()
// processor name is 'null' because it is a local anonymous class
assertThat(
result.diagnostics[Diagnostic.Kind.ERROR]?.map { it.msg } ?: emptyList<String>()
).containsExactly(
"null was unable to process 'Other' because not all of its dependencies could be " +
"resolved. Check for compilation errors or a circular dependency with generated " +
"code."
)
}
@Test
fun javacAnnotatedElementsByStep() {
val main = JavaFileObjects.forSourceString(
"foo.bar.Main",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@MainAnnotation(
typeList = {},
singleType = Object.class,
intMethod = 3,
singleOtherAnnotation = @OtherAnnotation("y")
)
class Main {
}
""".trimIndent()
)
val other = JavaFileObjects.forSourceString(
"foo.bar.Other",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@OtherAnnotation("x")
class Other {
}
""".trimIndent()
)
val elementsByStep = mutableMapOf<XProcessingStep, Collection<String>>()
val mainStep = object : XProcessingStep {
override fun annotations() = setOf(MainAnnotation::class.qualifiedName!!)
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XElement> {
elementsByStep[this] = elementsByAnnotation.values.flatten()
.map { (it as XTypeElement).qualifiedName }
return emptySet()
}
}
val otherStep = object : XProcessingStep {
override fun annotations() = setOf(OtherAnnotation::class.qualifiedName!!)
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XElement> {
elementsByStep[this] = elementsByAnnotation.values.flatten()
.map { (it as XTypeElement).qualifiedName }
return emptySet()
}
}
assertAbout(
JavaSourcesSubjectFactory.javaSources()
).that(
listOf(main, other)
).processedWith(
object : JavacBasicAnnotationProcessor() {
override fun processingSteps() = listOf(mainStep, otherStep)
}
).compilesWithoutError()
assertThat(elementsByStep[mainStep])
.containsExactly("foo.bar.Main")
assertThat(elementsByStep[otherStep])
.containsExactly("foo.bar.Other")
}
@Test
fun javacDeferredStep() {
// create a scenario where we defer the first round of processing
val main = JavaFileObjects.forSourceString(
"foo.bar.Main",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@MainAnnotation(
typeList = {},
singleType = Object.class,
intMethod = 3,
singleOtherAnnotation = @OtherAnnotation("y")
)
class Main {}
""".trimIndent()
)
val stepsProcessed = mutableListOf<XProcessingStep>()
var invokedProcessOver = 0
val mainStep = object : XProcessingStep {
var round = 0
override fun annotations() = setOf(MainAnnotation::class.qualifiedName!!)
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XElement> {
stepsProcessed.add(this)
val deferredElements = if (round++ == 0) {
// Generate a random class to trigger another processing round
val className = ClassName.get("foo.bar", "Main_Impl")
val spec = TypeSpec.classBuilder(className).build()
JavaFile.builder(className.packageName(), spec)
.build()
.writeTo(env.filer)
// Defer all processing to the next round
elementsByAnnotation.values.flatten().toSet()
} else {
emptySet()
}
return deferredElements
}
override fun processOver(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
) {
invokedProcessOver++
}
}
var invokedProcessingSteps = 0
val invokedPostRound = mutableListOf<Boolean>()
assertAbout(
JavaSourcesSubjectFactory.javaSources()
).that(
listOf(main)
).processedWith(
object : JavacBasicAnnotationProcessor() {
override fun processingSteps(): List<XProcessingStep> {
invokedProcessingSteps++
return listOf(mainStep)
}
override fun postRound(env: XProcessingEnv, round: XRoundEnv) {
invokedPostRound.add(round.isProcessingOver)
}
}
).compilesWithoutError()
// Assert that mainStep was processed twice due to deferring
assertThat(stepsProcessed).containsExactly(mainStep, mainStep)
// Assert processingSteps() was only called once
assertThat(invokedProcessingSteps).isEqualTo(1)
// Assert processOver() was only called once
assertThat(invokedProcessOver).isEqualTo(1)
// Assert postRound() is invoked exactly 3 times, and the last round env reported
// that processing was over.
assertThat(invokedPostRound).containsExactly(false, false, true)
}
@Test
fun javacDeferredViaException() {
val main = JavaFileObjects.forSourceString(
"foo.bar.Main",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@MainAnnotation(
typeList = {},
singleType = AnotherSource.class,
intMethod = 3,
singleOtherAnnotation = @OtherAnnotation("y")
)
class Main {}
""".trimIndent()
)
val anotherSource = JavaFileObjects.forSourceString(
"foo.bar.AnotherSource",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@SingleTypeValueAnnotation(GeneratedType.class)
class AnotherSource { }
""".trimIndent()
)
var round = 0
val genClassName = ClassName.get("foo.bar", "GeneratedType")
val mainStep = object : XProcessingStep {
override fun annotations() = setOf(MainAnnotation::class.qualifiedName!!)
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XElement> {
if (round++ == 0) {
// Generate the interface, should not be resolvable in 1st round
val spec = TypeSpec.interfaceBuilder(genClassName).build()
JavaFile.builder(genClassName.packageName(), spec)
.build()
.writeTo(env.filer)
}
val mainElement =
elementsByAnnotation[MainAnnotation::class.qualifiedName!!]!!.single()
try {
val otherElement = env.requireTypeElement(
mainElement.requireAnnotation(MainAnnotation::class.className())
.getAsType("singleType")
.typeName
)
val generatedType =
otherElement.requireAnnotation(SingleTypeValueAnnotation::class.className())
.getAsType("value")
assertThat(generatedType.typeName).isEqualTo(genClassName)
return emptySet()
} catch (ex: TypeNotPresentException) {
return setOf(mainElement)
}
}
}
assertAbout(
JavaSourcesSubjectFactory.javaSources()
).that(
listOf(main, anotherSource)
).processedWith(
object : JavacBasicAnnotationProcessor() {
override fun processingSteps(): List<XProcessingStep> {
return listOf(mainStep)
}
}
).compilesWithoutError()
// Expect two rounds due to implicit deferring caused by missing type.
assertThat(round).isEqualTo(2)
}
@Test
fun javacStepOnlyCalledIfElementsToProcess() {
val main = JavaFileObjects.forSourceString(
"foo.bar.Main",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@MainAnnotation(
typeList = {},
singleType = Object.class,
intMethod = 3,
singleOtherAnnotation = @OtherAnnotation("y")
)
class Main {
}
""".trimIndent()
)
val stepsProcessed = mutableListOf<XProcessingStep>()
val mainStep = object : XProcessingStep {
override fun annotations() = setOf(MainAnnotation::class.qualifiedName!!)
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XElement> {
stepsProcessed.add(this)
return emptySet()
}
}
val otherStep = object : XProcessingStep {
override fun annotations() = setOf(OtherAnnotation::class.qualifiedName!!)
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XElement> {
stepsProcessed.add(this)
return emptySet()
}
}
assertAbout(
JavaSourcesSubjectFactory.javaSources()
).that(
listOf(main)
).processedWith(
object : JavacBasicAnnotationProcessor() {
override fun processingSteps() = listOf(mainStep, otherStep)
}
).compilesWithoutError()
assertThat(stepsProcessed).containsExactly(mainStep)
}
@Test
fun kspAnnotatedElementsByStep() {
val main = Source.kotlin(
"Classes.kt",
"""
package foo.bar
import androidx.room.compiler.processing.testcode.*
@MainAnnotation(
typeList = [],
singleType = Any::class,
intMethod = 3,
singleOtherAnnotation = OtherAnnotation("y")
)
class Main {
}
@OtherAnnotation("y")
class Other {
}
""".trimIndent()
)
val elementsByStep = mutableMapOf<XProcessingStep, Collection<String>>()
val mainStep = object : XProcessingStep {
override fun annotations() = setOf(MainAnnotation::class.qualifiedName!!)
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XElement> {
elementsByStep[this] = elementsByAnnotation.values.flatten()
.map { (it as XTypeElement).qualifiedName }
return emptySet()
}
}
val otherStep = object : XProcessingStep {
override fun annotations() = setOf(OtherAnnotation::class.qualifiedName!!)
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XElement> {
elementsByStep[this] = elementsByAnnotation.values.flatten()
.map { (it as XTypeElement).qualifiedName }
return emptySet()
}
}
val processorProvider = object : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return object : KspBasicAnnotationProcessor(environment) {
override fun processingSteps() = listOf(mainStep, otherStep)
}
}
}
compile(
workingDir = temporaryFolder.root,
arguments = TestCompilationArguments(
sources = listOf(main),
symbolProcessorProviders = listOf(processorProvider)
)
)
assertThat(elementsByStep[mainStep])
.containsExactly("foo.bar.Main")
assertThat(elementsByStep[otherStep])
.containsExactly("foo.bar.Other")
}
@Test
fun kspDeferredStep() {
// create a scenario where we defer the first round of processing
val main = Source.kotlin(
"Classes.kt",
"""
package foo.bar
import androidx.room.compiler.processing.testcode.*
@MainAnnotation(
typeList = [],
singleType = Any::class,
intMethod = 3,
singleOtherAnnotation = OtherAnnotation("y")
)
class Main {}
""".trimIndent()
)
val stepsProcessed = mutableListOf<XProcessingStep>()
var invokedProcessOver = 0
val mainStep = object : XProcessingStep {
var round = 0
override fun annotations() = setOf(MainAnnotation::class.qualifiedName!!)
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XElement> {
stepsProcessed.add(this)
val deferredElements = if (round++ == 0) {
// Generate a random class to trigger another processing round
val className = ClassName.get("foo.bar", "Main_Impl")
val spec = TypeSpec.classBuilder(className).build()
JavaFile.builder(className.packageName(), spec)
.build()
.writeTo(env.filer)
// Defer all processing to the next round
elementsByAnnotation.values.flatten().toSet()
} else {
emptySet()
}
return deferredElements
}
override fun processOver(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
) {
invokedProcessOver++
}
}
var invokedProcessingSteps = 0
val invokedPostRound = mutableListOf<Boolean>()
val processorProvider = object : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return object : KspBasicAnnotationProcessor(environment) {
override fun processingSteps(): List<XProcessingStep> {
invokedProcessingSteps++
return listOf(mainStep)
}
override fun postRound(env: XProcessingEnv, round: XRoundEnv) {
invokedPostRound.add(round.isProcessingOver)
}
}
}
}
compile(
workingDir = temporaryFolder.root,
arguments = TestCompilationArguments(
sources = listOf(main),
symbolProcessorProviders = listOf(processorProvider)
)
)
// Assert that mainStep was processed twice due to deferring
assertThat(stepsProcessed).containsExactly(mainStep, mainStep)
// Assert processingSteps() was only called once
assertThat(invokedProcessingSteps).isEqualTo(1)
// Assert processOver() was only called once
assertThat(invokedProcessOver).isEqualTo(1)
// Assert postRound() is invoked exactly 3 times, and the last round env reported
// that processing was over.
assertThat(invokedPostRound).containsExactly(false, false, true)
}
@Test
fun kspStepOnlyCalledIfElementsToProcess() {
val main = Source.kotlin(
"Classes.kt",
"""
package foo.bar
import androidx.room.compiler.processing.testcode.*
@MainAnnotation(
typeList = [],
singleType = Any::class,
intMethod = 3,
singleOtherAnnotation = OtherAnnotation("y")
)
class Main {
}
""".trimIndent()
)
val stepsProcessed = mutableListOf<XProcessingStep>()
val mainStep = object : XProcessingStep {
override fun annotations() = setOf(MainAnnotation::class.qualifiedName!!)
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XElement> {
stepsProcessed.add(this)
return emptySet()
}
}
val otherStep = object : XProcessingStep {
override fun annotations() = setOf(OtherAnnotation::class.qualifiedName!!)
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>
): Set<XElement> {
stepsProcessed.add(this)
return emptySet()
}
}
val processorProvider = object : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return object : KspBasicAnnotationProcessor(environment) {
override fun processingSteps() = listOf(mainStep, otherStep)
}
}
}
compile(
workingDir = temporaryFolder.root,
arguments = TestCompilationArguments(
sources = listOf(main),
symbolProcessorProviders = listOf(processorProvider)
)
)
assertThat(stepsProcessed).containsExactly(mainStep)
}
}