Custom kotlin compiler testing infra
This CL replaces Kotlin Compile Testing library with a custom
implementation for easy kotlin / ksp updates going forward.
As part of it, now xprocessing provides ability to assert on sources and
lines for diagnostics (for line and source).
```
invocation.assertCompilationResult {
hasWarningContaining("on field")
.onLine(3)
.onLineContaining("Foo bar;")
.onSource(source)
}
```
Also briefly changed internals of runProcessorTest to clean temporary
folders before returning to leave less dirt on the OS.
With this change, we collect messages from compilers, hence, I removed
the RecordingXMessager. Since messages coming from the compiler include
part of the source, assertions are updated to do contains match for
diagnostic messages instead of exact match.
Bug: 196879136
Test: existing tests
Change-Id: Iec2f4fe6ff3776fc5d9488db258b5ed16834933d
diff --git a/room/room-compiler-processing-testing/build.gradle b/room/room-compiler-processing-testing/build.gradle
index c09c633..9657ee2 100644
--- a/room/room-compiler-processing-testing/build.gradle
+++ b/room/room-compiler-processing-testing/build.gradle
@@ -30,7 +30,6 @@
implementation(libs.kotlinStdlibJdk8) // KSP defines older version as dependency, force update.
implementation(libs.ksp)
implementation(libs.googleCompileTesting)
- implementation(libs.kotlinCompileTesting)
// specify these to match the kotlin compiler version in AndroidX rather than what KSP or KCT
// uses
implementation(libs.kotlinCompilerEmbeddable)
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticProcessor.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticProcessor.kt
index 0be69b3..159e83a 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticProcessor.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticProcessor.kt
@@ -15,7 +15,6 @@
*/
package androidx.room.compiler.processing
-import androidx.room.compiler.processing.util.RecordingXMessager
import androidx.room.compiler.processing.util.XTestInvocation
/**
@@ -32,11 +31,6 @@
val invocationInstances: List<XTestInvocation>
/**
- * The recorder for messages where we'll grab the diagnostics.
- */
- val messageWatcher: RecordingXMessager
-
- /**
* Should return any assertion error that happened during processing.
*
* When assertions fail, we don't fail the compilation to keep the stack trace, instead,
@@ -61,7 +55,6 @@
private var result: Result<Unit>? = null
override val invocationInstances = mutableListOf<XTestInvocation>()
private val nextRunHandlers = handlers.toMutableList()
- override val messageWatcher = RecordingXMessager()
internal fun processingSteps() = listOf<XProcessingStep>(
// A processing step that just ensures we're run every round.
@@ -118,7 +111,6 @@
}
val handler = nextRunHandlers.removeAt(0)
invocationInstances.add(invocation)
- invocation.processingEnv.messager.addMessageWatcher(messageWatcher)
result = kotlin.runCatching {
handler(invocation)
invocation.dispose()
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/CompilationResultSubject.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/CompilationResultSubject.kt
index 3a7b940..4a4e63f 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/CompilationResultSubject.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/CompilationResultSubject.kt
@@ -19,6 +19,7 @@
import androidx.room.compiler.processing.ExperimentalProcessingApi
import androidx.room.compiler.processing.SyntheticJavacProcessor
import androidx.room.compiler.processing.SyntheticProcessor
+import androidx.room.compiler.processing.util.compiler.TestCompilationResult
import androidx.room.compiler.processing.util.runner.CompilationTestRunner
import com.google.common.truth.Fact.fact
import com.google.common.truth.Fact.simpleFact
@@ -27,8 +28,6 @@
import com.google.common.truth.Subject.Factory
import com.google.common.truth.Truth
import com.google.testing.compile.Compilation
-import com.tschuchort.compiletesting.KotlinCompilation
-import java.io.File
import javax.tools.Diagnostic
/**
@@ -37,7 +36,7 @@
@ExperimentalProcessingApi
abstract class CompilationResult internal constructor(
/**
- * The test infra which run this test
+ * The test infra dwhich run this test
*/
internal val testRunnerName: String,
/**
@@ -48,10 +47,18 @@
* True if compilation result was success.
*/
internal val successfulCompilation: Boolean,
+
+ /**
+ * List of diagnostics that were reported during compilation
+ */
+ diagnostics: Map<Diagnostic.Kind, List<DiagnosticMessage>>
) {
+
internal abstract val generatedSources: List<Source>
- private val diagnostics = processor.messageWatcher.diagnostics()
+ val diagnostics = diagnostics.mapValues {
+ it.value.filterNot { it.isIgnored() }
+ }
fun diagnosticsOfKind(kind: Diagnostic.Kind) = diagnostics[kind].orEmpty()
@@ -82,6 +89,25 @@
appendLine(rawOutput())
}
}
+
+ internal companion object {
+ fun DiagnosticMessage.isIgnored() = FILTERED_MESSAGE_PREFIXES.any {
+ msg.startsWith(it)
+ }
+
+ /**
+ * These messages are mostly verbose and not helpful for testing.
+ */
+ private val FILTERED_MESSAGE_PREFIXES = listOf(
+ "No processor claimed any of these annotations:",
+ "The following options were not recognized by any processor:",
+ "Using Kotlin home directory",
+ "Scripting plugin will not be loaded: not",
+ "Using JVM IR backend",
+ "Configuring the compilation environment",
+ "Loading modules:"
+ )
+ }
}
/**
@@ -151,7 +177,7 @@
* @see hasError
* @see hasNote
*/
- fun hasWarning(expected: String) = apply {
+ fun hasWarning(expected: String) =
hasDiagnosticWithMessage(
kind = Diagnostic.Kind.WARNING,
expected = expected,
@@ -159,7 +185,6 @@
) {
"expected warning: $expected"
}
- }
/**
* Asserts that compilation has a warning that contains the given text.
@@ -167,7 +192,7 @@
* @see hasErrorContaining
* @see hasNoteContaining
*/
- fun hasWarningContaining(expected: String) = apply {
+ fun hasWarningContaining(expected: String) =
hasDiagnosticWithMessage(
kind = Diagnostic.Kind.WARNING,
expected = expected,
@@ -175,7 +200,6 @@
) {
"expected warning: $expected"
}
- }
/**
* Asserts that compilation has a note with the given text.
@@ -183,7 +207,7 @@
* @see hasError
* @see hasWarning
*/
- fun hasNote(expected: String) = apply {
+ fun hasNote(expected: String) =
hasDiagnosticWithMessage(
kind = Diagnostic.Kind.NOTE,
expected = expected,
@@ -191,7 +215,6 @@
) {
"expected note: $expected"
}
- }
/**
* Asserts that compilation has a note that contains the given text.
@@ -199,7 +222,7 @@
* @see hasErrorContaining
* @see hasWarningContaining
*/
- fun hasNoteContaining(expected: String) = apply {
+ fun hasNoteContaining(expected: String) =
hasDiagnosticWithMessage(
kind = Diagnostic.Kind.NOTE,
expected = expected,
@@ -207,7 +230,6 @@
) {
"expected note: $expected"
}
- }
/**
* Asserts that compilation has an error with the given text.
@@ -215,9 +237,9 @@
* @see hasWarning
* @see hasNote
*/
- fun hasError(expected: String) = apply {
+ fun hasError(expected: String): DiagnosticMessageSubject {
shouldSucceed = false
- hasDiagnosticWithMessage(
+ return hasDiagnosticWithMessage(
kind = Diagnostic.Kind.ERROR,
expected = expected,
acceptPartialMatch = false
@@ -232,9 +254,9 @@
* @see hasWarningContaining
* @see hasNoteContaining
*/
- fun hasErrorContaining(expected: String) = apply {
+ fun hasErrorContaining(expected: String): DiagnosticMessageSubject {
shouldSucceed = false
- hasDiagnosticWithMessage(
+ return hasDiagnosticWithMessage(
kind = Diagnostic.Kind.ERROR,
expected = expected,
acceptPartialMatch = true
@@ -346,15 +368,21 @@
expected: String,
acceptPartialMatch: Boolean,
buildErrorMessage: () -> String
- ) {
+ ): DiagnosticMessageSubject {
val diagnostics = compilationResult.diagnosticsOfKind(kind)
- if (diagnostics.any { it.msg == expected }) {
- return
+ var diagnostic = diagnostics.firstOrNull {
+ it.msg == expected
}
- if (acceptPartialMatch && diagnostics.any { it.msg.contains(expected) }) {
- return
+ if (diagnostic == null && acceptPartialMatch) {
+ diagnostic = diagnostics.firstOrNull {
+ it.msg.contains(expected)
+ }
+ }
+ diagnostic?.let {
+ return DiagnosticMessageSubject.assertThat(it)
}
failWithActual(simpleFact(buildErrorMessage()))
+ error("unreachable")
}
/**
@@ -391,65 +419,41 @@
testRunner: CompilationTestRunner,
@Suppress("unused")
private val delegate: Compilation,
- processor: SyntheticJavacProcessor
+ processor: SyntheticJavacProcessor,
+ diagnostics: Map<Diagnostic.Kind, List<DiagnosticMessage>>,
+ override val generatedSources: List<Source>
) : CompilationResult(
testRunnerName = testRunner.name,
processor = processor,
- successfulCompilation = delegate.status() == Compilation.Status.SUCCESS
+ successfulCompilation = delegate.status() == Compilation.Status.SUCCESS,
+ diagnostics = diagnostics
) {
- override val generatedSources: List<Source> by lazy {
- if (successfulCompilation) {
- delegate.generatedSourceFiles().map(Source::fromJavaFileObject)
- } else {
- // java compile testing does not provide access to generated files when compilation
- // fails
- emptyList()
- }
- }
-
override fun rawOutput(): String {
return delegate.diagnostics().joinToString {
it.toString()
}
}
}
+
@ExperimentalProcessingApi
-internal class KotlinCompileTestingCompilationResult(
+internal class KotlinCompilationResult constructor(
testRunner: CompilationTestRunner,
- @Suppress("unused")
- private val delegate: KotlinCompilation.Result,
processor: SyntheticProcessor,
- successfulCompilation: Boolean,
- outputSourceDirs: List<File>,
- private val rawOutput: String,
+ private val delegate: TestCompilationResult
) : CompilationResult(
testRunnerName = testRunner.name,
processor = processor,
- successfulCompilation = successfulCompilation
+ successfulCompilation = delegate.success,
+ diagnostics = delegate.diagnostics
) {
- override val generatedSources: List<Source> by lazy {
- outputSourceDirs.flatMap { srcRoot ->
- srcRoot.walkTopDown().mapNotNull { sourceFile ->
- when {
- sourceFile.name.endsWith(".java") -> {
- val qName = sourceFile.absolutePath.substringAfter(
- srcRoot.absolutePath
- ).dropWhile { it == '/' }
- .replace('/', '.')
- .dropLast(".java".length)
- Source.loadJavaSource(sourceFile, qName)
- }
- sourceFile.name.endsWith(".kt") -> {
- val relativePath = sourceFile.absolutePath.substringAfter(
- srcRoot.absolutePath
- ).dropWhile { it == '/' }
- Source.loadKotlinSource(sourceFile, relativePath)
- }
- else -> null
- }
- }
+ override val generatedSources: List<Source>
+ get() = delegate.generatedSources
+
+ override fun rawOutput(): String {
+ return delegate.diagnostics.flatMap {
+ it.value
+ }.joinToString {
+ it.toString()
}
}
-
- override fun rawOutput() = rawOutput
-}
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/DiagnosticMessage.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/DiagnosticMessage.kt
index da8e542..438396e 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/DiagnosticMessage.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/DiagnosticMessage.kt
@@ -16,16 +16,23 @@
package androidx.room.compiler.processing.util
-import androidx.room.compiler.processing.XAnnotation
-import androidx.room.compiler.processing.XAnnotationValue
-import androidx.room.compiler.processing.XElement
+import javax.tools.Diagnostic
/**
* Holder for diagnostics messages
*/
data class DiagnosticMessage(
+ val kind: Diagnostic.Kind,
val msg: String,
- val element: XElement?,
- val annotation: XAnnotation?,
- val annotationValue: XAnnotationValue?
+ val location: DiagnosticLocation? = null,
+)
+
+/**
+ * Location of a diagnostic message.
+ * Note that, when run with KAPT this location might be on the stubs or may not exactly match the
+ * kotlin source file (KAPT's stub to source mapping is not very fine grained)
+ */
+data class DiagnosticLocation(
+ val source: Source?,
+ val line: Int,
)
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/DiagnosticMessageSubject.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/DiagnosticMessageSubject.kt
new file mode 100644
index 0000000..ba817ef
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/DiagnosticMessageSubject.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2021 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.util
+
+import com.google.common.truth.Fact.simpleFact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth
+
+/**
+ * Truth subject for diagnostic messages
+ */
+class DiagnosticMessageSubject internal constructor(
+ failureMetadata: FailureMetadata,
+ private val diagnosticMessage: DiagnosticMessage,
+) : Subject<DiagnosticMessageSubject, DiagnosticMessage>(
+ failureMetadata, diagnosticMessage
+) {
+ private val lineContent by lazy {
+ val location = diagnosticMessage.location ?: return@lazy null
+ location.source?.contents?.lines()?.getOrNull(
+ location.line - 1
+ )
+ }
+
+ /**
+ * Checks the location of the diagnostic message against the given [lineNumber].
+ */
+ fun onLine(lineNumber: Int) = apply {
+ if (diagnosticMessage.location?.line != lineNumber) {
+ failWithActual(
+ simpleFact(
+ "expected line $lineNumber but it was ${diagnosticMessage.location}"
+ )
+ )
+ }
+ }
+
+ /**
+ * Checks the contents of the line from the original file against the given [content].
+ */
+ fun onLineContaining(content: String) = apply {
+ if (lineContent == null) {
+ failWithActual(
+ simpleFact("Cannot validate line content due to missing location information")
+ )
+ }
+ if (lineContent?.contains(content) != true) {
+ failWithActual(
+ simpleFact("expected line content with $content but was $lineContent")
+ )
+ }
+ }
+
+ /**
+ * Checks the contents of the source where the diagnostic message was reported on, against
+ * the given [source].
+ */
+ fun onSource(source: Source) = apply {
+ if (diagnosticMessage.location?.source != source) {
+ failWithActual(
+ simpleFact(
+ """
+ Expected diagnostic to be on $source but found it on
+ ${diagnosticMessage.location?.source}
+ """.trimIndent()
+ )
+ )
+ }
+ }
+
+ companion object {
+ private val FACTORY =
+ Factory<DiagnosticMessageSubject, DiagnosticMessage> { metadata, actual ->
+ DiagnosticMessageSubject(metadata, actual)
+ }
+
+ fun assertThat(
+ diagnosticMessage: DiagnosticMessage
+ ): DiagnosticMessageSubject {
+ return Truth.assertAbout(FACTORY).that(
+ diagnosticMessage
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt
deleted file mode 100644
index 6da8f93..0000000
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright 2021 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.util
-
-import com.tschuchort.compiletesting.KotlinCompilation
-import org.jetbrains.kotlin.config.JvmTarget
-import java.io.File
-import java.io.OutputStream
-import java.net.URLClassLoader
-
-/**
- * Helper class for Kotlin Compile Testing library to have common setup for room.
- */
-internal object KotlinCompilationUtil {
- fun prepareCompilation(
- sources: List<Source>,
- outputStream: OutputStream,
- classpaths: List<File> = emptyList()
- ): KotlinCompilation {
- val compilation = KotlinCompilation()
- val srcRoot = compilation.workingDir.resolve("ksp/srcInput")
- val javaSrcRoot = srcRoot.resolve("java")
- val kotlinSrcRoot = srcRoot.resolve("kotlin")
- compilation.sources = sources.map {
- when (it) {
- is Source.JavaSource -> it.toKotlinSourceFile(javaSrcRoot)
- is Source.KotlinSource -> it.toKotlinSourceFile(kotlinSrcRoot)
- }
- }
- // workaround for https://github.com/tschuchortdev/kotlin-compile-testing/issues/105
- compilation.kotlincArguments += "-Xjava-source-roots=${javaSrcRoot.absolutePath}"
- compilation.jvmDefault = "enable"
- compilation.jvmTarget = JvmTarget.JVM_1_8.description
- compilation.inheritClassPath = false
- compilation.verbose = false
- compilation.classpaths = Classpaths.inheritedClasspath + classpaths
- compilation.messageOutputStream = outputStream
- compilation.kotlinStdLibJar = Classpaths.kotlinStdLibJar
- compilation.kotlinStdLibCommonJar = Classpaths.kotlinStdLibCommonJar
- compilation.kotlinStdLibJdkJar = Classpaths.kotlinStdLibJdkJar
- compilation.kotlinReflectJar = Classpaths.kotlinReflectJar
- compilation.kotlinScriptRuntimeJar = Classpaths.kotlinScriptRuntimeJar
- return compilation
- }
-
- /**
- * Helper object to persist common classpaths resolved by KCT to make sure it does not
- * re-resolve host classpath repeatedly and also runs compilation with a smaller classpath.
- * see: https://github.com/tschuchortdev/kotlin-compile-testing/issues/113
- */
- private object Classpaths {
-
- val inheritedClasspath: List<File>
-
- /**
- * These jars are files that Kotlin Compile Testing discovers from classpath. It uses a
- * rather expensive way of discovering these so we cache them here for now.
- *
- * We can remove this cache once we update to a version that includes the fix in KCT:
- * https://github.com/tschuchortdev/kotlin-compile-testing/pull/114
- */
- val kotlinStdLibJar: File?
- val kotlinStdLibCommonJar: File?
- val kotlinStdLibJdkJar: File?
- val kotlinReflectJar: File?
- val kotlinScriptRuntimeJar: File?
-
- init {
- // create a KotlinCompilation to resolve common jars
- val compilation = KotlinCompilation()
- kotlinStdLibJar = compilation.kotlinStdLibJar
- kotlinStdLibCommonJar = compilation.kotlinStdLibCommonJar
- kotlinStdLibJdkJar = compilation.kotlinStdLibJdkJar
- kotlinReflectJar = compilation.kotlinReflectJar
- kotlinScriptRuntimeJar = compilation.kotlinScriptRuntimeJar
-
- inheritedClasspath = getClasspathFromClassloader(
- KotlinCompilationUtil::class.java.classLoader
- )
- }
- }
-
- // ported from https://github.com/google/compile-testing/blob/master/src/main/java/com
- // /google/testing/compile/Compiler.java#L231
- private fun getClasspathFromClassloader(referenceClassLoader: ClassLoader): List<File> {
- val platformClassLoader: ClassLoader = ClassLoader.getPlatformClassLoader()
- var currentClassloader = referenceClassLoader
- val systemClassLoader = ClassLoader.getSystemClassLoader()
-
- // Concatenate search paths from all classloaders in the hierarchy
- // 'till the system classloader.
- val classpaths: MutableSet<String> = LinkedHashSet()
- while (true) {
- if (currentClassloader === systemClassLoader) {
- classpaths.addAll(getSystemClasspaths())
- break
- }
- if (currentClassloader === platformClassLoader) {
- break
- }
- check(currentClassloader is URLClassLoader) {
- """Classpath for compilation could not be extracted
- since $currentClassloader is not an instance of URLClassloader
- """.trimIndent()
- }
- // We only know how to extract classpaths from URLClassloaders.
- currentClassloader.urLs.forEach { url ->
- check(url.protocol == "file") {
- """Given classloader consists of classpaths which are unsupported for
- compilation.
- """.trimIndent()
- }
- classpaths.add(url.path)
- }
- currentClassloader = currentClassloader.parent
- }
- return classpaths.map { File(it) }.filter { it.exists() }
- }
-}
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
index 6b53b5c..c02ec44 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
@@ -19,18 +19,17 @@
import androidx.room.compiler.processing.ExperimentalProcessingApi
import androidx.room.compiler.processing.XProcessingStep
import androidx.room.compiler.processing.XTypeElement
+import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
+import androidx.room.compiler.processing.util.compiler.compile
import androidx.room.compiler.processing.util.runner.CompilationTestRunner
import androidx.room.compiler.processing.util.runner.JavacCompilationTestRunner
import androidx.room.compiler.processing.util.runner.KaptCompilationTestRunner
import androidx.room.compiler.processing.util.runner.KspCompilationTestRunner
import androidx.room.compiler.processing.util.runner.TestCompilationParameters
+import com.google.common.io.Files
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import com.google.devtools.ksp.processing.SymbolProcessorProvider
-import com.tschuchort.compiletesting.KotlinCompilation
-import com.tschuchort.compiletesting.kspArgs
-import com.tschuchort.compiletesting.symbolProcessorProviders
-import java.io.ByteArrayOutputStream
import java.io.File
import javax.annotation.processing.Processor
@@ -41,21 +40,23 @@
) {
val runCount = runners.count { runner ->
if (runner.canRun(params)) {
- val compilationResult = runner.compile(params)
- val subject = CompilationResultSubject.assertThat(compilationResult)
- // if any assertion failed, throw first those.
- subject.assertNoProcessorAssertionErrors()
- compilationResult.processor.invocationInstances.forEach {
- it.runPostCompilationChecks(subject)
- }
- assertWithMessage(
- "compilation should've run the processor callback at least once"
- ).that(
- compilationResult.processor.invocationInstances
- ).isNotEmpty()
+ withTempDir { tmpDir ->
+ val compilationResult = runner.compile(tmpDir, params)
+ val subject = CompilationResultSubject.assertThat(compilationResult)
+ // if any assertion failed, throw first those.
+ subject.assertNoProcessorAssertionErrors()
+ compilationResult.processor.invocationInstances.forEach {
+ it.runPostCompilationChecks(subject)
+ }
+ assertWithMessage(
+ "compilation should've run the processor callback at least once"
+ ).that(
+ compilationResult.processor.invocationInstances
+ ).isNotEmpty()
- subject.assertCompilationResult()
- subject.assertAllExpectedRoundsAreCompleted()
+ subject.assertCompilationResult()
+ subject.assertAllExpectedRoundsAreCompleted()
+ }
true
} else {
false
@@ -88,7 +89,7 @@
sources = sources,
classpath = classpath,
options = options,
- handlers = listOf(handler)
+ handlers = listOf(handler),
),
JavacCompilationTestRunner,
KaptCompilationTestRunner
@@ -323,23 +324,34 @@
symbolProcessorProviders: List<SymbolProcessorProvider> = emptyList(),
javacArguments: List<String> = emptyList()
): List<File> {
- val outputStream = ByteArrayOutputStream()
- val compilation = KotlinCompilationUtil.prepareCompilation(
- sources = sources,
- outputStream = outputStream
+ val workingDir = Files.createTempDir()
+ val result = compile(
+ workingDir = workingDir,
+ arguments = TestCompilationArguments(
+ sources = sources,
+ kaptProcessors = annotationProcessors,
+ symbolProcessorProviders = symbolProcessorProviders,
+ processorOptions = options,
+ javacArguments = javacArguments
+ )
)
- if (annotationProcessors.isNotEmpty()) {
- compilation.kaptArgs.putAll(options)
- }
- if (symbolProcessorProviders.isNotEmpty()) {
- compilation.kspArgs.putAll(options)
- }
- compilation.javacArguments.addAll(javacArguments)
- compilation.annotationProcessors = annotationProcessors
- compilation.symbolProcessorProviders = symbolProcessorProviders
- val result = compilation.compile()
- check(result.exitCode == KotlinCompilation.ExitCode.OK) {
- "compilation failed: ${outputStream.toString(Charsets.UTF_8)}"
- }
- return listOf(compilation.classesDir) + compilation.classpaths
+ assertThat(result.success).isTrue()
+ return result.outputClasspath + getSystemClasspathFiles()
}
+
+/**
+ * Runs a block in a temporary directory and cleans it up afterwards.
+ *
+ * This method intentionally returns Unit to make it harder to return something that might
+ * reference the temporary directory.
+ */
+private inline fun withTempDir(
+ block: (tmpDir: File) -> Unit
+) {
+ val tmpDir = Files.createTempDir()
+ try {
+ return block(tmpDir)
+ } finally {
+ tmpDir.deleteRecursively()
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/RecordingXMessager.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/RecordingXMessager.kt
deleted file mode 100644
index ed481d6..0000000
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/RecordingXMessager.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.util
-
-import androidx.room.compiler.processing.XAnnotation
-import androidx.room.compiler.processing.XAnnotationValue
-import androidx.room.compiler.processing.XElement
-import androidx.room.compiler.processing.XMessager
-import javax.tools.Diagnostic
-
-/**
- * An XMessager implementation that holds onto dispatched diagnostics.
- */
-class RecordingXMessager : XMessager() {
- private val diagnostics = mutableMapOf<Diagnostic.Kind, MutableList<DiagnosticMessage>>()
-
- fun diagnostics(): Map<Diagnostic.Kind, List<DiagnosticMessage>> = diagnostics
-
- override fun onPrintMessage(
- kind: Diagnostic.Kind,
- msg: String,
- element: XElement?,
- annotation: XAnnotation?,
- annotationValue: XAnnotationValue?
- ) {
- diagnostics.getOrPut(
- kind
- ) {
- mutableListOf()
- }.add(
- DiagnosticMessage(
- msg = msg,
- element = element,
- annotation = annotation,
- annotationValue = annotationValue
- )
- )
- }
-}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/Source.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/Source.kt
index 1f28f13..b800570 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/Source.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/Source.kt
@@ -17,7 +17,6 @@
package androidx.room.compiler.processing.util
import com.google.testing.compile.JavaFileObjects
-import com.tschuchort.compiletesting.SourceFile
import org.intellij.lang.annotations.Language
import java.io.File
import javax.tools.JavaFileObject
@@ -29,12 +28,27 @@
abstract val relativePath: String
abstract val contents: String
abstract fun toJFO(): JavaFileObject
- abstract fun toKotlinSourceFile(srcRoot: File): SourceFile
override fun toString(): String {
return "SourceFile[$relativePath]"
}
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Source) return false
+
+ if (relativePath != other.relativePath) return false
+ if (contents != other.contents) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = relativePath.hashCode()
+ result = 31 * result + contents.hashCode()
+ return result
+ }
+
class JavaSource(
val qName: String,
override val contents: String
@@ -46,15 +60,6 @@
)
}
- override fun toKotlinSourceFile(srcRoot: File): SourceFile {
- val outFile = srcRoot.resolve(relativePath)
- .also {
- it.parentFile.mkdirs()
- it.writeText(contents)
- }
- return SourceFile.fromPath(outFile)
- }
-
override val relativePath
get() = qName.replace(".", "/") + ".java"
}
@@ -66,16 +71,6 @@
override fun toJFO(): JavaFileObject {
throw IllegalStateException("cannot include kotlin code in javac compilation")
}
-
- override fun toKotlinSourceFile(srcRoot: File): SourceFile {
- val outFile = srcRoot.resolve(relativePath).also {
- it.parentFile.mkdirs()
- it.writeText(contents)
- }
- return SourceFile.fromPath(
- outFile
- )
- }
}
companion object {
@@ -147,7 +142,7 @@
relativePath: String
): Source {
check(file.exists()) {
- "file does not exist: ${file.absolutePath}"
+ "file does not exist: ${file.canonicalPath}"
}
return when {
file.name.endsWith(".kt") -> loadKotlinSource(file, relativePath)
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/TestUilts.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/TestUilts.kt
index bf96270..e912ce9 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/TestUilts.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/TestUilts.kt
@@ -17,6 +17,9 @@
package androidx.room.compiler.processing.util
import java.io.File
+import java.util.Locale
+import javax.tools.Diagnostic
+import javax.tools.JavaFileObject
/**
* Returns the list of File's in the system classpath
@@ -35,4 +38,30 @@
fun getSystemClasspaths(): Set<String> {
val pathSeparator = System.getProperty("path.separator")!!
return System.getProperty("java.class.path")!!.split(pathSeparator).toSet()
+}
+
+/**
+ * Converts java compilation diagnostic messages into [DiagnosticMessage] objects.
+ */
+internal fun List<Diagnostic<out JavaFileObject>>.toDiagnosticMessages(
+ javaSources: Map<JavaFileObject, Source>
+): List<DiagnosticMessage> {
+ return this.map { diagnostic ->
+ val source = diagnostic.source?.let {
+ javaSources[it]
+ }
+ val location = if (source == null) {
+ null
+ } else {
+ DiagnosticLocation(
+ source = source,
+ line = diagnostic.lineNumber.toInt(),
+ )
+ }
+ DiagnosticMessage(
+ kind = diagnostic.kind,
+ msg = diagnostic.getMessage(Locale.US),
+ location = location,
+ )
+ }
}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DelegatingTestRegistrar.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DelegatingTestRegistrar.kt
new file mode 100644
index 0000000..ae10928
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DelegatingTestRegistrar.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2021 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.util.compiler
+
+import androidx.room.compiler.processing.util.compiler.DelegatingTestRegistrar.Companion.runCompilation
+import org.jetbrains.kotlin.cli.common.ExitCode
+import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
+import org.jetbrains.kotlin.cli.common.messages.MessageCollector
+import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
+import org.jetbrains.kotlin.cli.jvm.plugins.ServiceLoaderLite
+import org.jetbrains.kotlin.com.intellij.mock.MockProject
+import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
+import org.jetbrains.kotlin.config.CompilerConfiguration
+import org.jetbrains.kotlin.config.Services
+import java.net.URI
+import java.nio.file.Paths
+
+/**
+ * A component registrar for Kotlin Compiler that delegates to a list of thread local delegates.
+ *
+ * see [runCompilation] for usages.
+ */
+internal class DelegatingTestRegistrar : ComponentRegistrar {
+ override fun registerProjectComponents(
+ project: MockProject,
+ configuration: CompilerConfiguration
+ ) {
+ delegates.get()?.let {
+ it.forEach {
+ it.registerProjectComponents(project, configuration)
+ }
+ }
+ }
+
+ companion object {
+ private const val REGISTRAR_CLASSPATH =
+ "META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar"
+
+ private val resourcePathForSelfClassLoader by lazy {
+ this::class.java.classLoader.getResources(REGISTRAR_CLASSPATH)
+ .asSequence()
+ .mapNotNull { url ->
+ val uri = URI.create(url.toString().removeSuffix("/$REGISTRAR_CLASSPATH"))
+ when (uri.scheme) {
+ "jar" -> Paths.get(URI.create(uri.schemeSpecificPart.removeSuffix("!")))
+ "file" -> Paths.get(uri)
+ else -> return@mapNotNull null
+ }.toAbsolutePath()
+ }
+ .find { resourcesPath ->
+ ServiceLoaderLite.findImplementations(
+ ComponentRegistrar::class.java,
+ listOf(resourcesPath.toFile())
+ ).any { implementation ->
+ implementation == DelegatingTestRegistrar::class.java.name
+ }
+ }?.toString()
+ ?: throw AssertionError(
+ """
+ Could not find the ComponentRegistrar class loader that should load
+ ${DelegatingTestRegistrar::class.qualifiedName}
+ """.trimIndent()
+ )
+ }
+ private val delegates = ThreadLocal<List<ComponentRegistrar>>()
+ fun runCompilation(
+ compiler: K2JVMCompiler,
+ messageCollector: MessageCollector,
+ arguments: K2JVMCompilerArguments,
+ pluginRegistrars: List<ComponentRegistrar>
+ ): ExitCode {
+ try {
+ arguments.addDelegatingTestRegistrar()
+ delegates.set(pluginRegistrars)
+ return compiler.exec(
+ messageCollector = messageCollector,
+ services = Services.EMPTY,
+ arguments = arguments
+ )
+ } finally {
+ delegates.remove()
+ }
+ }
+
+ private fun K2JVMCompilerArguments.addDelegatingTestRegistrar() {
+ pluginClasspaths = (pluginClasspaths ?: arrayOf()) + resourcePathForSelfClassLoader
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DiagnosticsMessageCollector.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DiagnosticsMessageCollector.kt
new file mode 100644
index 0000000..e991f25
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DiagnosticsMessageCollector.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2021 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.util.compiler
+
+import androidx.room.compiler.processing.util.compiler.steps.RawDiagnosticMessage
+import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
+import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation
+import org.jetbrains.kotlin.cli.common.messages.MessageCollector
+import javax.tools.Diagnostic
+
+/**
+ * Custom message collector for Kotlin compilation that collects messages into
+ * [RawDiagnosticMessage] objects.
+ *
+ * Neither KAPT nor KSP report location in the `location` parameter of the callback, instead,
+ * they embed location into the messages. This collector parses these messages to recover the
+ * location.
+ */
+internal class DiagnosticsMessageCollector : MessageCollector {
+ private val diagnostics = mutableListOf<RawDiagnosticMessage>()
+
+ fun getDiagnostics(): List<RawDiagnosticMessage> = diagnostics
+
+ override fun clear() {
+ diagnostics.clear()
+ }
+
+ override fun hasErrors(): Boolean {
+ return diagnostics.any {
+ it.kind == Diagnostic.Kind.ERROR
+ }
+ }
+
+ override fun report(
+ severity: CompilerMessageSeverity,
+ message: String,
+ location: CompilerMessageSourceLocation?
+ ) {
+ // Both KSP and KAPT reports null location but instead put the location into the message.
+ // We parse it back here to recover the location.
+ val (strippedMessage, rawLocation) = if (location == null) {
+ message.parseLocation() ?: message.stripPrefixes() to null
+ } else {
+ message.stripPrefixes() to location.toRawLocation()
+ }
+ diagnostics.add(
+ RawDiagnosticMessage(
+ kind = severity.kind,
+ message = strippedMessage,
+ location = rawLocation
+ )
+ )
+ }
+
+ /**
+ * Parses the location out of a diagnostic message.
+ *
+ * Note that this is tailor made for KSP and KAPT where the location is reported in the first
+ * line of the message.
+ *
+ * If location is found, this method will return the location along with the message without
+ * location. Otherwise, it will return `null`.
+ */
+ private fun String.parseLocation(): Pair<String, RawDiagnosticMessage.Location>? {
+ val firstLine = lineSequence().firstOrNull() ?: return null
+ val match =
+ KSP_LOCATION_REGEX.find(firstLine) ?: KAPT_LOCATION_AND_KIND_REGEX.find(firstLine)
+ ?: return null
+ if (match.groups.size != 4) return null
+ return substring(match.range.last + 1) to RawDiagnosticMessage.Location(
+ path = match.groupValues[1],
+ line = match.groupValues[3].toInt(),
+ )
+ }
+
+ /**
+ * Removes prefixes added by kapt / ksp from the message
+ */
+ private fun String.stripPrefixes(): String {
+ return stripKind().stripKspPrefix()
+ }
+
+ /**
+ * KAPT prepends the message kind to the message, we'll remove it here.
+ */
+ private fun String.stripKind(): String {
+ val firstLine = lineSequence().firstOrNull() ?: return this
+ val match = KIND_REGEX.find(firstLine) ?: return this
+ return substring(match.range.last + 1)
+ }
+
+ /**
+ * KSP prepends ksp to each message, we'll strip it here.
+ */
+ private fun String.stripKspPrefix(): String {
+ val firstLine = lineSequence().firstOrNull() ?: return this
+ val match = KSP_PREFIX_REGEX.find(firstLine) ?: return this
+ return substring(match.range.last + 1)
+ }
+
+ private fun CompilerMessageSourceLocation.toRawLocation(): RawDiagnosticMessage.Location {
+ return RawDiagnosticMessage.Location(
+ line = this.line,
+ path = this.path
+ )
+ }
+
+ private val CompilerMessageSeverity.kind
+ get() = when (this) {
+ CompilerMessageSeverity.ERROR,
+ CompilerMessageSeverity.EXCEPTION -> Diagnostic.Kind.ERROR
+ CompilerMessageSeverity.INFO,
+ CompilerMessageSeverity.LOGGING -> Diagnostic.Kind.NOTE
+ CompilerMessageSeverity.WARNING,
+ CompilerMessageSeverity.STRONG_WARNING -> Diagnostic.Kind.WARNING
+ else -> Diagnostic.Kind.OTHER
+ }
+
+ companion object {
+ // example: foo/bar/Subject.kt:2: warning: the real message
+ private val KAPT_LOCATION_AND_KIND_REGEX = """^(.*\.(kt|java)):(\d+): \w+: """.toRegex()
+ // example: [ksp] /foo/bar/Subject.kt:3: the real message
+ private val KSP_LOCATION_REGEX = """^\[ksp] (.*\.(kt|java)):(\d+): """.toRegex()
+
+ // detect things like "Note: " to be stripped from the message.
+ // We could limit this to known diagnostic kinds (instead of matching \w:) but it is always
+ // added so not really necessary until we hit a parser bug :)
+ // example: "error: the real message"
+ private val KIND_REGEX = """^\w+: """.toRegex()
+ // example: "[ksp] the real message"
+ private val KSP_PREFIX_REGEX = """^\[ksp] """.toRegex()
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/KotlinCliRunner.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/KotlinCliRunner.kt
new file mode 100644
index 0000000..1343fc0
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/KotlinCliRunner.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2021 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.util.compiler
+
+import androidx.room.compiler.processing.util.compiler.steps.CompilationStepArguments
+import androidx.room.compiler.processing.util.compiler.steps.RawDiagnosticMessage
+import androidx.room.compiler.processing.util.getSystemClasspaths
+import org.jetbrains.kotlin.cli.common.ExitCode
+import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
+import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
+import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
+import org.jetbrains.kotlin.config.JvmDefaultMode
+import org.jetbrains.kotlin.config.JvmTarget
+import java.io.File
+import java.net.URLClassLoader
+
+/**
+ * Utility object to run kotlin compiler via its CLI API.
+ */
+internal object KotlinCliRunner {
+ private val compiler = K2JVMCompiler()
+ private fun List<SourceSet>.existingRootPaths() = this.asSequence()
+ .map { it.root }
+ .filter { it.exists() }
+ .map { it.canonicalPath }
+ .distinct()
+
+ private fun CompilationStepArguments.copyToCliArguments(cliArguments: K2JVMCompilerArguments) {
+ // stdlib is in the classpath so no need to specify it here.
+ cliArguments.noStdlib = true
+ cliArguments.noReflect = true
+ cliArguments.jvmTarget = JvmTarget.JVM_1_8.description
+ cliArguments.noOptimize = true
+ // useJavac & compileJava are experimental so lets not use it for now.
+ cliArguments.useJavac = false
+ cliArguments.compileJava = false
+ cliArguments.jvmDefault = JvmDefaultMode.ENABLE.description
+ cliArguments.allowNoSourceFiles = true
+ cliArguments.javacArguments = javacArguments.toTypedArray()
+ val inherited = if (inheritClasspaths) {
+ inheritedClasspath
+ } else {
+ emptyList()
+ }
+ cliArguments.classpath = (additionalClasspaths + inherited)
+ .filter { it.exists() }
+ .distinct()
+ .joinToString(
+ separator = File.pathSeparator
+ ) {
+ it.canonicalPath
+ }
+ cliArguments.javaSourceRoots = this.sourceSets.filter {
+ it.hasJavaSource
+ }.existingRootPaths()
+ .toList()
+ .toTypedArray()
+ cliArguments.freeArgs += this.sourceSets.filter {
+ it.hasKotlinSource
+ }.existingRootPaths()
+ }
+
+ /**
+ * Runs the kotlin cli API with the given arguments.
+ */
+ fun runKotlinCli(
+ /**
+ * Compilation arguments (sources, classpaths etc)
+ */
+ arguments: CompilationStepArguments,
+ /**
+ * Destination directory where generated class files will be written to
+ */
+ destinationDir: File,
+ /**
+ * List of component registrars for the compilation.
+ */
+ pluginRegistrars: List<ComponentRegistrar>
+ ): KotlinCliResult {
+ val cliArguments = compiler.createArguments()
+ destinationDir.mkdirs()
+ cliArguments.destination = destinationDir.absolutePath
+ arguments.copyToCliArguments(cliArguments)
+
+ val diagnosticsMessageCollector = DiagnosticsMessageCollector()
+ val exitCode = DelegatingTestRegistrar.runCompilation(
+ compiler = compiler,
+ messageCollector = diagnosticsMessageCollector,
+ arguments = cliArguments,
+ pluginRegistrars = pluginRegistrars
+ )
+ return KotlinCliResult(
+ exitCode = exitCode,
+ diagnostics = diagnosticsMessageCollector.getDiagnostics(),
+ compiledClasspath = destinationDir
+ )
+ }
+
+ /**
+ * Result of a kotlin compilation request
+ */
+ internal class KotlinCliResult(
+ /**
+ * The exit code reported by the compiler
+ */
+ val exitCode: ExitCode,
+ /**
+ * List of diagnostic messages reported by the compiler
+ */
+ val diagnostics: List<RawDiagnosticMessage>,
+ /**
+ * The output classpath for the compiled files.
+ */
+ val compiledClasspath: File
+ )
+
+ private val inheritedClasspath by lazy(LazyThreadSafetyMode.NONE) {
+ getClasspathFromClassloader(KotlinCliRunner::class.java.classLoader)
+ }
+
+ // ported from https://github.com/google/compile-testing/blob/master/src/main/java/com
+ // /google/testing/compile/Compiler.java#L231
+ private fun getClasspathFromClassloader(referenceClassLoader: ClassLoader): List<File> {
+ val platformClassLoader: ClassLoader = ClassLoader.getPlatformClassLoader()
+ var currentClassloader = referenceClassLoader
+ val systemClassLoader = ClassLoader.getSystemClassLoader()
+
+ // Concatenate search paths from all classloaders in the hierarchy
+ // 'till the system classloader.
+ val classpaths: MutableSet<String> = LinkedHashSet()
+ while (true) {
+ if (currentClassloader === systemClassLoader) {
+ classpaths.addAll(getSystemClasspaths())
+ break
+ }
+ if (currentClassloader === platformClassLoader) {
+ break
+ }
+ check(currentClassloader is URLClassLoader) {
+ """Classpath for compilation could not be extracted
+ since $currentClassloader is not an instance of URLClassloader
+ """.trimIndent()
+ }
+ // We only know how to extract classpaths from URLClassloaders.
+ currentClassloader.urLs.forEach { url ->
+ check(url.protocol == "file") {
+ """Given classloader consists of classpaths which are unsupported for
+ compilation.
+ """.trimIndent()
+ }
+ classpaths.add(url.path)
+ }
+ currentClassloader = currentClassloader.parent
+ }
+ return classpaths.map { File(it) }.filter { it.exists() }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/SourceSet.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/SourceSet.kt
new file mode 100644
index 0000000..f83b60c
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/SourceSet.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2021 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.util.compiler
+
+import androidx.room.compiler.processing.util.Source
+import java.io.File
+
+/**
+ * Represents sources that are positioned in the [root] folder.
+ * see: [fromExistingFiles]
+ */
+internal class SourceSet(
+ /**
+ * The root source folder for the given sources
+ */
+ root: File,
+ /**
+ * List of actual sources in the folder
+ */
+ val sources: List<Source>
+) {
+ // always use canonical files
+ val root = root.canonicalFile
+
+ init {
+ check(root.isDirectory) {
+ "$root must be a directory"
+ }
+ }
+
+ val hasJavaSource by lazy {
+ javaSources.isNotEmpty()
+ }
+
+ val hasKotlinSource by lazy {
+ kotlinSources.isNotEmpty()
+ }
+
+ val javaSources by lazy {
+ sources.filterIsInstance<Source.JavaSource>()
+ }
+
+ val kotlinSources by lazy {
+ sources.filterIsInstance<Source.KotlinSource>()
+ }
+
+ /**
+ * Finds the source file matching the given relative path (from root)
+ */
+ fun findSourceFile(
+ path: String
+ ): Source? {
+ val file = File(path).canonicalFile
+ if (!file.path.startsWith(root.path)) {
+ return null
+ }
+ val relativePath = path.substringAfter(root.canonicalPath + "/")
+ return sources.firstOrNull {
+ it.relativePath == relativePath
+ }
+ }
+
+ companion object {
+ /**
+ * Creates a new SourceSet from the given files.
+ */
+ fun fromExistingFiles(
+ root: File
+ ) = SourceSet(
+ root = root,
+ sources = root.collectSources().toList()
+ )
+ }
+}
+
+/**
+ * Collects all java/kotlin sources in a given directory.
+ * Note that the package name for java sources are inherited from the given relative path.
+ */
+private fun File.collectSources(): Sequence<Source> {
+ val root = this
+ return walkTopDown().mapNotNull { file ->
+ when (file.extension) {
+ "java" -> Source.loadJavaSource(
+ file = file,
+ qName = file.relativeTo(root).path
+ .replace('/', '.')
+ .substringBeforeLast('.') // drop .java
+ )
+ "kt" -> Source.loadKotlinSource(
+ file = file,
+ relativePath = file.relativeTo(root).path
+ )
+ else -> null
+ }
+ }
+}
+/**
+ * Converts the file to a [SourceSet] if and only if it is a directory.
+ */
+internal fun File.toSourceSet() = if (isDirectory) {
+ SourceSet.fromExistingFiles(this)
+} else {
+ null
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKapt3Registrar.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKapt3Registrar.kt
new file mode 100644
index 0000000..d794e56
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKapt3Registrar.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2010-2016 JetBrains s.r.o.
+ * Copyright 2021 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.util.compiler
+
+import org.jetbrains.kotlin.base.kapt3.KaptFlag
+import org.jetbrains.kotlin.base.kapt3.KaptOptions
+import org.jetbrains.kotlin.base.kapt3.logString
+import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
+import org.jetbrains.kotlin.cli.common.messages.MessageCollector
+import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot
+import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot
+import org.jetbrains.kotlin.com.intellij.mock.MockProject
+import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
+import org.jetbrains.kotlin.config.CompilerConfiguration
+import org.jetbrains.kotlin.config.JVMConfigurationKeys
+import org.jetbrains.kotlin.container.StorageComponentContainer
+import org.jetbrains.kotlin.container.useInstance
+import org.jetbrains.kotlin.descriptors.ModuleDescriptor
+import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor
+import org.jetbrains.kotlin.kapt3.AbstractKapt3Extension
+import org.jetbrains.kotlin.kapt3.KaptAnonymousTypeTransformer
+import org.jetbrains.kotlin.kapt3.base.LoadedProcessors
+import org.jetbrains.kotlin.kapt3.base.incremental.DeclaredProcType
+import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor
+import org.jetbrains.kotlin.kapt3.util.MessageCollectorBackedKaptLogger
+import org.jetbrains.kotlin.platform.TargetPlatform
+import org.jetbrains.kotlin.platform.jvm.isJvm
+import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension
+import org.jetbrains.kotlin.resolve.jvm.extensions.PartialAnalysisHandlerExtension
+import java.io.File
+import javax.annotation.processing.Processor
+
+/**
+ * Registers the KAPT component for the kotlin compilation.
+ *
+ * mostly taken from
+ * https://github.com/JetBrains/kotlin/blob/master/plugins/kapt3/kapt3-compiler/src/
+ * org/jetbrains/kotlin/kapt3/Kapt3Plugin.kt
+ */
+internal class TestKapt3Registrar(
+ val processors: List<Processor>,
+ val baseOptions: KaptOptions.Builder,
+ val messageCollector: MessageCollector
+) : ComponentRegistrar {
+ override fun registerProjectComponents(
+ project: MockProject,
+ configuration: CompilerConfiguration
+ ) {
+ val contentRoots = configuration[CLIConfigurationKeys.CONTENT_ROOTS] ?: emptyList()
+
+ val optionsBuilder = baseOptions.apply {
+ projectBaseDir = project.basePath?.let(::File)
+ compileClasspath.addAll(
+ contentRoots.filterIsInstance<JvmClasspathRoot>().map { it.file }
+ )
+ javaSourceRoots.addAll(contentRoots.filterIsInstance<JavaSourceRoot>().map { it.file })
+ classesOutputDir =
+ classesOutputDir ?: configuration.get(JVMConfigurationKeys.OUTPUT_DIRECTORY)
+ }
+
+ val logger = MessageCollectorBackedKaptLogger(
+ isVerbose = optionsBuilder.flags.contains(KaptFlag.VERBOSE),
+ isInfoAsWarnings = optionsBuilder.flags.contains(KaptFlag.INFO_AS_WARNINGS),
+ messageCollector = messageCollector
+ )
+
+ val options = optionsBuilder.build()
+
+ options.sourcesOutputDir.mkdirs()
+
+ if (options[KaptFlag.VERBOSE]) {
+ logger.info(options.logString())
+ }
+
+ val kapt3AnalysisCompletedHandlerExtension = object : AbstractKapt3Extension(
+ options = options,
+ logger = logger,
+ compilerConfiguration = configuration
+ ) {
+ override fun loadProcessors(): LoadedProcessors {
+ return LoadedProcessors(
+ processors = processors.map {
+ IncrementalProcessor(
+ processor = it,
+ kind = DeclaredProcType.NON_INCREMENTAL,
+ logger = logger
+ )
+ },
+ classLoader = TestKapt3Registrar::class.java.classLoader
+ )
+ }
+ }
+
+ AnalysisHandlerExtension.registerExtension(project, kapt3AnalysisCompletedHandlerExtension)
+ StorageComponentContainerContributor.registerExtension(
+ project,
+ KaptComponentContributor(kapt3AnalysisCompletedHandlerExtension)
+ )
+ }
+
+ class KaptComponentContributor(private val analysisExtension: PartialAnalysisHandlerExtension) :
+ StorageComponentContainerContributor {
+ override fun registerModuleComponents(
+ container: StorageComponentContainer,
+ platform: TargetPlatform,
+ moduleDescriptor: ModuleDescriptor
+ ) {
+ if (!platform.isJvm()) return
+ container.useInstance(KaptAnonymousTypeTransformer(analysisExtension))
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKotlinCompiler.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKotlinCompiler.kt
new file mode 100644
index 0000000..2c72b64
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKotlinCompiler.kt
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2021 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.util.compiler
+
+import androidx.room.compiler.processing.util.DiagnosticMessage
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compiler.steps.CompilationStepArguments
+import androidx.room.compiler.processing.util.compiler.steps.CompilationStepResult
+import androidx.room.compiler.processing.util.compiler.steps.JavaSourceCompilationStep
+import androidx.room.compiler.processing.util.compiler.steps.KaptCompilationStep
+import androidx.room.compiler.processing.util.compiler.steps.KotlinSourceCompilationStep
+import androidx.room.compiler.processing.util.compiler.steps.KspCompilationStep
+import com.google.devtools.ksp.processing.SymbolProcessorProvider
+import java.io.File
+import javax.annotation.processing.Processor
+import javax.tools.Diagnostic
+
+/**
+ * Compilation runner for kotlin using kotlin CLI tool
+ */
+data class TestCompilationArguments(
+ /**
+ * List of source files for the compilation
+ */
+ val sources: List<Source>,
+ /**
+ * Additional classpath for the compilation
+ */
+ val classpath: List<File> = emptyList(),
+ /**
+ * If `true` (default), the classpath of the current process will be included in the
+ * classpath list.
+ */
+ val inheritClasspath: Boolean = true,
+ /**
+ * Arguments for the java compiler. This will be used when both running KAPT and also java
+ * compiler.
+ */
+ val javacArguments: List<String> = emptyList(),
+ /**
+ * List of annotation processors to be run by KAPT.
+ */
+ val kaptProcessors: List<Processor> = emptyList(),
+ /**
+ * List of symbol processor providers to be run by KSP.
+ */
+ val symbolProcessorProviders: List<SymbolProcessorProvider> = emptyList(),
+ /**
+ * Map of annotation/symbol processor options. Used for both KAPT and KSP.
+ */
+ val processorOptions: Map<String, String> = emptyMap()
+)
+
+/**
+ * Result of a test compilation.
+ */
+data class TestCompilationResult(
+ /**
+ * true if the compilation succeeded, false otherwise.
+ */
+ val success: Boolean,
+ /**
+ * List of generated source files by the compilation.
+ */
+ val generatedSources: List<Source>,
+ /**
+ * Diagnostic messages that were reported during compilation.
+ */
+ val diagnostics: Map<Diagnostic.Kind, List<DiagnosticMessage>>,
+ /**
+ * List of classpath folders that contain the produced .class files.
+ */
+ val outputClasspath: List<File>
+)
+
+/**
+ * Ensures the list of sources has at least 1 kotlin file, if not, adds one.
+ */
+internal fun TestCompilationArguments.withAtLeastOneKotlinSource(): TestCompilationArguments {
+ val hasKotlinSource = sources.any {
+ it is Source.KotlinSource
+ }
+ if (hasKotlinSource) return this
+ return copy(
+ sources = sources + Source.kotlin(
+ "SyntheticSource",
+ code = """
+ package xprocessing.generated
+ class SyntheticKotlinSource
+ """.trimIndent()
+ )
+ )
+}
+
+/**
+ * Copies the [Source] file into the given root directories based on file type.
+ */
+private fun Source.copyTo(
+ kotlinRootDir: File,
+ javaRootDir: File
+): File {
+ val locationRoot = when (this) {
+ is Source.KotlinSource -> kotlinRootDir
+ is Source.JavaSource -> javaRootDir
+ }
+ val location = locationRoot.resolve(relativePath)
+ check(!location.exists()) {
+ "duplicate source file: $location ($this)"
+ }
+ location.parentFile.mkdirs()
+ location.writeText(contents, Charsets.UTF_8)
+ return location
+}
+
+/**
+ * Converts [TestCompilationArguments] into the internal [CompilationStepArguments] type.
+ *
+ * This involves copying sources into the working directory.
+ */
+private fun TestCompilationArguments.toInternal(
+ workingDir: File
+): CompilationStepArguments {
+ val (kotlinRoot, javaRoot) = workingDir.resolve("src").let {
+ it.resolve("kotlin") to it.resolve("java")
+ }
+ // copy sources based on type.
+ sources.map {
+ it.copyTo(kotlinRootDir = kotlinRoot, javaRootDir = javaRoot)
+ }
+ return CompilationStepArguments(
+ sourceSets = listOfNotNull(
+ javaRoot.toSourceSet(),
+ kotlinRoot.toSourceSet()
+ ),
+ additionalClasspaths = classpath,
+ inheritClasspaths = inheritClasspath,
+ javacArguments = javacArguments
+ )
+}
+
+/**
+ * Executes a build for the given [TestCompilationArguments].
+ */
+fun compile(
+ /**
+ * The temporary directory to use during compilation
+ */
+ workingDir: File,
+ /**
+ * The compilation arguments
+ */
+ arguments: TestCompilationArguments,
+): TestCompilationResult {
+ val steps = listOf(
+ KaptCompilationStep(arguments.kaptProcessors, arguments.processorOptions),
+ KspCompilationStep(arguments.symbolProcessorProviders, arguments.processorOptions),
+ KotlinSourceCompilationStep,
+ JavaSourceCompilationStep
+ )
+ workingDir.ensureEmptyDirectory()
+
+ val initialArgs = arguments.toInternal(workingDir.resolve("input"))
+ val initial = listOf(
+ CompilationStepResult(
+ success = true,
+ generatedSourceRoots = emptyList(),
+ diagnostics = emptyList(),
+ nextCompilerArguments = initialArgs,
+ outputClasspath = emptyList()
+ )
+ )
+ val resultFromEachStep = steps.fold(initial) { prevResults, step ->
+ val prev = prevResults.last()
+ if (prev.success) {
+ prevResults + step.execute(
+ workingDir = workingDir.resolve(step.name),
+ arguments = prev.nextCompilerArguments
+ )
+ } else {
+ prevResults
+ }
+ }
+ val combinedDiagnostics = mutableMapOf<Diagnostic.Kind, MutableList<DiagnosticMessage>>()
+ resultFromEachStep.forEach { result ->
+ result.diagnostics.forEach { diagnostic ->
+ combinedDiagnostics.getOrPut(
+ diagnostic.kind
+ ) {
+ mutableListOf()
+ }.add(diagnostic)
+ }
+ }
+ return TestCompilationResult(
+ success = resultFromEachStep.all { it.success },
+ generatedSources = resultFromEachStep.flatMap { it.generatedSources },
+ diagnostics = combinedDiagnostics,
+ outputClasspath = resultFromEachStep.flatMap { it.outputClasspath }
+ )
+}
+
+internal fun File.ensureEmptyDirectory() {
+ if (exists()) {
+ check(isDirectory) {
+ "$this cannot be a file"
+ }
+ val existingFiles = listFiles()
+ check(existingFiles == null || existingFiles.isEmpty()) {
+ "$this must be empty, found: ${existingFiles?.joinToString("\n")}"
+ }
+ } else {
+ check(this.mkdirs()) {
+ "failed to create working directory ($this)"
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt
new file mode 100644
index 0000000..53d12aa
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2021 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.util.compiler
+
+import com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension
+import com.google.devtools.ksp.KspCliOption
+import com.google.devtools.ksp.KspOptions
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.processing.SymbolProcessorProvider
+import com.google.devtools.ksp.processing.impl.MessageCollectorBasedKSPLogger
+import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
+import org.jetbrains.kotlin.cli.common.messages.MessageCollector
+import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot
+import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot
+import org.jetbrains.kotlin.com.intellij.core.CoreApplicationEnvironment
+import org.jetbrains.kotlin.com.intellij.mock.MockProject
+import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeAdapter
+import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeListener
+import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
+import org.jetbrains.kotlin.config.CompilerConfiguration
+import org.jetbrains.kotlin.resolve.extensions.AnalysisHandlerExtension
+import java.io.File
+
+/**
+ * Registers the KSP component for the kotlin compilation.
+ */
+internal class TestKspRegistrar(
+ val kspWorkingDir: File,
+ val baseOptions: KspOptions.Builder,
+
+ val processorProviders: List<SymbolProcessorProvider>,
+ val messageCollector: MessageCollector
+) : ComponentRegistrar {
+ override fun registerProjectComponents(
+ project: MockProject,
+ configuration: CompilerConfiguration
+ ) {
+ baseOptions.apply {
+ projectBaseDir = project.basePath?.let {
+ File(it)
+ } ?: kspWorkingDir
+ incremental = false
+ incrementalLog = false
+ // NOT supported yet, hence we set a default
+ classOutputDir = classOutputDir ?: kspWorkingDir.resolve(
+ KspCliOption
+ .CLASS_OUTPUT_DIR_OPTION.optionName
+ )
+ // NOT supported yet, hence we set a default
+ resourceOutputDir = resourceOutputDir ?: kspWorkingDir.resolve(
+ KspCliOption.RESOURCE_OUTPUT_DIR_OPTION.optionName
+ )
+ cachesDir = cachesDir ?: kspWorkingDir.resolve(
+ KspCliOption.CACHES_DIR_OPTION.optionName
+ )
+
+ kspOutputDir = kspOutputDir ?: kspWorkingDir.resolve(
+ KspCliOption.KSP_OUTPUT_DIR_OPTION.optionName
+ )
+ val contentRoots = configuration[CLIConfigurationKeys.CONTENT_ROOTS] ?: emptyList()
+
+ compileClasspath.addAll(
+ contentRoots.filterIsInstance<JvmClasspathRoot>().map { it.file }
+ )
+
+ javaSourceRoots.addAll(
+ contentRoots.filterIsInstance<JavaSourceRoot>().map { it.file }
+ )
+ }
+ val logger = MessageCollectorBasedKSPLogger(
+ messageCollector = messageCollector
+ )
+ val options = baseOptions.build()
+ AnalysisHandlerExtension.registerExtension(
+ project,
+ TestKspExtension(
+ options = options,
+ processorProviders = processorProviders,
+ logger = logger
+ )
+ )
+ // Placeholder extension point; Required by dropPsiCaches().
+ CoreApplicationEnvironment.registerExtensionPoint(
+ project.extensionArea,
+ PsiTreeChangeListener.EP.name,
+ PsiTreeChangeAdapter::class.java
+ )
+ }
+
+ private class TestKspExtension(
+ options: KspOptions,
+ processorProviders: List<SymbolProcessorProvider>,
+ logger: KSPLogger
+ ) : AbstractKotlinSymbolProcessingExtension(
+ options = options,
+ logger = logger,
+ testMode = false
+ ) {
+ private val loadedProviders = processorProviders
+
+ override fun loadProviders() = loadedProviders
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/JavaSourceCompilationStep.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/JavaSourceCompilationStep.kt
new file mode 100644
index 0000000..27b3e95
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/JavaSourceCompilationStep.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2021 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.util.compiler.steps
+
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.getSystemClasspathFiles
+import androidx.room.compiler.processing.util.toDiagnosticMessages
+import com.google.testing.compile.Compilation
+import com.google.testing.compile.Compiler
+import java.io.File
+import javax.tools.JavaFileObject
+
+/**
+ * Compiles java sources. Note that this does not run java annotation processors. They are run in
+ * the KAPT step for consistency. When a test is run with purely java sources, it uses the google
+ * compile testing library directly instead of the kotlin compilation pipeline.
+ */
+internal object JavaSourceCompilationStep : KotlinCompilationStep {
+ override val name = "javaSourceCompilation"
+
+ override fun execute(
+ workingDir: File,
+ arguments: CompilationStepArguments
+ ): CompilationStepResult {
+ val javaSources: Map<JavaFileObject, Source> = arguments.sourceSets
+ .asSequence()
+ .flatMap {
+ it.javaSources
+ }.associateBy {
+ it.toJFO()
+ }
+ if (javaSources.isEmpty()) {
+ return CompilationStepResult.skip(arguments)
+ }
+ val classpaths = if (arguments.inheritClasspaths) {
+ arguments.additionalClasspaths + getSystemClasspathFiles()
+ } else {
+ arguments.additionalClasspaths
+ }.filter { it.exists() }
+
+ val compiler = Compiler.javac()
+ .withOptions(arguments.javacArguments + "-Xlint")
+ .withClasspath(classpaths)
+
+ val result = compiler.compile(javaSources.keys)
+
+ val generatedClasses = if (result.status() == Compilation.Status.SUCCESS) {
+ val classpathOut = workingDir.resolve(GEN_CLASS_OUT)
+ result.generatedFiles().map {
+ val targetFile = classpathOut.resolve(
+ it.toUri().path.substringAfter("CLASS_OUTPUT/")
+ ).also { file ->
+ file.parentFile.mkdirs()
+ }
+ it.openInputStream().use { inputStream ->
+ targetFile.outputStream().use { outputStream ->
+ inputStream.transferTo(outputStream)
+ }
+ }
+ }
+ listOf(
+ classpathOut
+ )
+ } else {
+ emptyList()
+ }
+
+ return CompilationStepResult(
+ success = result.status() == Compilation.Status.SUCCESS,
+ generatedSourceRoots = emptyList(),
+ diagnostics = result.diagnostics().toDiagnosticMessages(javaSources),
+ nextCompilerArguments = arguments.copy(
+ // NOTE: ideally, we should remove java sources but we know that there are no next
+ // steps so we skip unnecessary work
+ sourceSets = arguments.sourceSets
+ ),
+ outputClasspath = generatedClasses
+ )
+ }
+
+ private const val GEN_CLASS_OUT = "classOut"
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KaptCompilationStep.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KaptCompilationStep.kt
new file mode 100644
index 0000000..e07117e
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KaptCompilationStep.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2021 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.util.compiler.steps
+
+import androidx.room.compiler.processing.util.compiler.DiagnosticsMessageCollector
+import androidx.room.compiler.processing.util.compiler.KotlinCliRunner
+import androidx.room.compiler.processing.util.compiler.TestKapt3Registrar
+import androidx.room.compiler.processing.util.compiler.toSourceSet
+import org.jetbrains.kotlin.base.kapt3.AptMode
+import org.jetbrains.kotlin.base.kapt3.KaptFlag
+import org.jetbrains.kotlin.base.kapt3.KaptOptions
+import org.jetbrains.kotlin.cli.common.ExitCode
+import java.io.File
+import javax.annotation.processing.Processor
+
+/**
+ * Runs KAPT to run annotation processors.
+ */
+internal class KaptCompilationStep(
+ private val annotationProcessors: List<Processor>,
+ private val processorOptions: Map<String, String>,
+) : KotlinCompilationStep {
+ override val name = "kapt"
+ private fun createKaptArgs(
+ workingDir: File,
+ ): KaptOptions.Builder {
+ return KaptOptions.Builder().also {
+ it.stubsOutputDir = workingDir.resolve("kapt-stubs") // IGNORED
+ it.sourcesOutputDir = workingDir.resolve(JAVA_SRC_OUT_FOLDER_NAME)
+ it.classesOutputDir = workingDir.resolve("kapt-classes-out") // IGNORED
+ it.projectBaseDir = workingDir
+ it.processingOptions["kapt.kotlin.generated"] =
+ workingDir.resolve(KOTLIN_SRC_OUT_FOLDER_NAME)
+ .also {
+ it.mkdirs()
+ }
+ .canonicalPath
+ it.processingOptions.putAll(processorOptions)
+ it.mode = AptMode.STUBS_AND_APT
+ it.processors.addAll(annotationProcessors.map { it::class.java.name })
+ // NOTE: this does not work very well until the following bug is fixed
+ // https://youtrack.jetbrains.com/issue/KT-47934
+ it.flags.add(KaptFlag.MAP_DIAGNOSTIC_LOCATIONS)
+ }
+ }
+
+ override fun execute(
+ workingDir: File,
+ arguments: CompilationStepArguments
+ ): CompilationStepResult {
+ if (annotationProcessors.isEmpty()) {
+ return CompilationStepResult.skip(arguments)
+ }
+ val kaptMessages = DiagnosticsMessageCollector()
+ val result = KotlinCliRunner.runKotlinCli(
+ arguments = arguments, // output is ignored,
+ destinationDir = workingDir.resolve(CLASS_OUT_FOLDER_NAME),
+ pluginRegistrars = listOf(
+ TestKapt3Registrar(
+ processors = annotationProcessors,
+ baseOptions = createKaptArgs(workingDir),
+ messageCollector = kaptMessages
+ )
+ )
+ )
+ val generatedSources = listOfNotNull(
+ workingDir.resolve(JAVA_SRC_OUT_FOLDER_NAME).toSourceSet(),
+ workingDir.resolve(KOTLIN_SRC_OUT_FOLDER_NAME).toSourceSet()
+ )
+
+ val diagnostics = resolveDiagnostics(
+ diagnostics = result.diagnostics + kaptMessages.getDiagnostics(),
+ sourceSets = arguments.sourceSets + generatedSources
+ )
+ return CompilationStepResult(
+ success = result.exitCode == ExitCode.OK,
+ generatedSourceRoots = generatedSources,
+ diagnostics = diagnostics,
+ nextCompilerArguments = arguments.copy(
+ sourceSets = arguments.sourceSets + generatedSources
+ ),
+ outputClasspath = listOf(result.compiledClasspath)
+ )
+ }
+
+ companion object {
+ private const val JAVA_SRC_OUT_FOLDER_NAME = "kapt-java-src-out"
+ private const val KOTLIN_SRC_OUT_FOLDER_NAME = "kapt-kotlin-src-out"
+ private const val CLASS_OUT_FOLDER_NAME = "class-out"
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KotlinCompilationStep.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KotlinCompilationStep.kt
new file mode 100644
index 0000000..1d4ba58
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KotlinCompilationStep.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2021 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.util.compiler.steps
+
+import androidx.room.compiler.processing.util.DiagnosticLocation
+import androidx.room.compiler.processing.util.DiagnosticMessage
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compiler.SourceSet
+import java.io.File
+import javax.tools.Diagnostic
+
+/**
+ * Kotlin compilation is run in multiple steps:
+ * process KSP
+ * process KAPT
+ * compile kotlin sources
+ * compile java sources
+ *
+ * Each step implements the [KotlinCompilationStep] interfaces and provides the arguments for
+ * the following step.
+ */
+internal interface KotlinCompilationStep {
+ /**
+ * A name to identify the step.
+ */
+ val name: String
+
+ fun execute(
+ /**
+ * Temporary folder that can be used by the step
+ */
+ workingDir: File,
+ /**
+ * Compilation parameters for the step.
+ */
+ arguments: CompilationStepArguments
+ ): CompilationStepResult
+}
+
+/**
+ * Diagnostic message that was captured from the compiler, before it is processed.
+ */
+internal data class RawDiagnosticMessage(
+ val kind: Diagnostic.Kind,
+ val message: String,
+ val location: Location?
+) {
+ data class Location(
+ val path: String,
+ val line: Int,
+ )
+}
+
+/**
+ * Parameters for each compilation step
+ */
+internal data class CompilationStepArguments(
+ /**
+ * List of source sets. Each source set has a root folder that can be used to pass to the
+ * compiler.
+ */
+ val sourceSets: List<SourceSet>,
+ /**
+ * Any additional classpath provided to the compilation
+ */
+ val additionalClasspaths: List<File>,
+ /**
+ * If `true`, the classpath of the test application should be provided to the compiler
+ */
+ val inheritClasspaths: Boolean,
+ /**
+ * Arguments to pass to the java compiler. This is also important for KAPT where part of the
+ * compilation is run by javac.
+ */
+ val javacArguments: List<String>,
+)
+
+/**
+ * Result of a compilation step.
+ */
+internal data class CompilationStepResult(
+ /**
+ * Whether it succeeded or not.
+ */
+ val success: Boolean,
+ /**
+ * List of source sets generated by this step
+ */
+ val generatedSourceRoots: List<SourceSet>,
+ /**
+ * List of diagnotic messages created by this step
+ */
+ val diagnostics: List<DiagnosticMessage>,
+ /**
+ * Arguments for the next compilation step. Current step might've modified its own parameters
+ * (e.g. add generated sources etc) for this one.
+ */
+ val nextCompilerArguments: CompilationStepArguments,
+ /**
+ * If the step compiled sources, this field includes the list of Files for each classpath.
+ */
+ val outputClasspath: List<File>
+) {
+ val generatedSources: List<Source> by lazy {
+ generatedSourceRoots.flatMap { it.sources }
+ }
+
+ companion object {
+ /**
+ * Creates a [CompilationStepResult] that does not create any outputs but instead simply
+ * passes the arguments to the next step.
+ */
+ fun skip(arguments: CompilationStepArguments) = CompilationStepResult(
+ success = true,
+ generatedSourceRoots = emptyList(),
+ diagnostics = emptyList(),
+ nextCompilerArguments = arguments,
+ outputClasspath = emptyList()
+ )
+ }
+}
+
+/**
+ * Associates [RawDiagnosticMessage]s with sources and creates [DiagnosticMessage]s.
+ */
+internal fun resolveDiagnostics(
+ diagnostics: List<RawDiagnosticMessage>,
+ sourceSets: List<SourceSet>,
+): List<DiagnosticMessage> {
+ return diagnostics.map { rawDiagnostic ->
+ // match it to source
+ val location = rawDiagnostic.location
+ if (location == null) {
+ DiagnosticMessage(
+ kind = rawDiagnostic.kind,
+ msg = rawDiagnostic.message,
+ location = null,
+ )
+ } else {
+ // find matching source file
+ val source = sourceSets.firstNotNullOfOrNull {
+ it.findSourceFile(location.path)
+ }
+
+ // source might be null for KAPT if it failed to match the diagnostic to a real
+ // source file (e.g. error is reported on the stub)
+ check(source != null || location.path.contains("kapt")) {
+ "Cannot find source file for the diagnostic :/ $rawDiagnostic"
+ }
+ DiagnosticMessage(
+ kind = rawDiagnostic.kind,
+ msg = rawDiagnostic.message,
+ location = DiagnosticLocation(
+ source = source,
+ line = location.line,
+ ),
+ )
+ }
+ }
+}
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KotlinSourceCompilationStep.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KotlinSourceCompilationStep.kt
new file mode 100644
index 0000000..dcfe22b
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KotlinSourceCompilationStep.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2021 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.util.compiler.steps
+
+import androidx.room.compiler.processing.util.compiler.KotlinCliRunner
+import org.jetbrains.kotlin.cli.common.ExitCode
+import java.io.File
+
+/**
+ * Compiles kotlin sources.
+ *
+ * Note that annotation/symbol processors are not run by this step.
+ */
+internal object KotlinSourceCompilationStep : KotlinCompilationStep {
+ override val name = "kotlinSourceCompilation"
+
+ override fun execute(
+ workingDir: File,
+ arguments: CompilationStepArguments
+ ): CompilationStepResult {
+ if (arguments.sourceSets.none { it.hasKotlinSource }) {
+ return CompilationStepResult.skip(arguments)
+ }
+ val result = KotlinCliRunner.runKotlinCli(
+ arguments = arguments,
+ destinationDir = workingDir.resolve(CLASS_OUT_FOLDER_NAME),
+ pluginRegistrars = emptyList()
+ )
+ val diagnostics = resolveDiagnostics(
+ diagnostics = result.diagnostics,
+ sourceSets = arguments.sourceSets
+ )
+ return CompilationStepResult(
+ success = result.exitCode == ExitCode.OK,
+ generatedSourceRoots = emptyList(),
+ diagnostics = diagnostics,
+ nextCompilerArguments = arguments.copy(
+ additionalClasspaths = listOf(workingDir.resolve(CLASS_OUT_FOLDER_NAME)) +
+ arguments.additionalClasspaths,
+ // NOTE: ideally, we should remove kotlin sources but we know that there are no more
+ // kotlin steps so we skip unnecessary work
+ sourceSets = arguments.sourceSets
+ ),
+ outputClasspath = listOf(result.compiledClasspath)
+ )
+ }
+
+ private const val CLASS_OUT_FOLDER_NAME = "class-out"
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KspCompilationStep.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KspCompilationStep.kt
new file mode 100644
index 0000000..2f8ed26
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KspCompilationStep.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021 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.util.compiler.steps
+
+import androidx.room.compiler.processing.util.compiler.DiagnosticsMessageCollector
+import androidx.room.compiler.processing.util.compiler.KotlinCliRunner
+import androidx.room.compiler.processing.util.compiler.TestKspRegistrar
+import androidx.room.compiler.processing.util.compiler.toSourceSet
+import com.google.devtools.ksp.KspOptions
+import com.google.devtools.ksp.processing.SymbolProcessorProvider
+import org.jetbrains.kotlin.cli.common.ExitCode
+import java.io.File
+
+/**
+ * Runs the Symbol Processors
+ */
+internal class KspCompilationStep(
+ private val symbolProcessorProviders: List<SymbolProcessorProvider>,
+ private val processorOptions: Map<String, String>
+) : KotlinCompilationStep {
+ override val name: String = "ksp"
+
+ private fun createKspOptions(
+ workingDir: File
+ ): KspOptions.Builder {
+ return KspOptions.Builder().apply {
+ this.javaOutputDir = workingDir.resolve(JAVA_OUT_DIR)
+ this.kotlinOutputDir = workingDir.resolve(KOTLIN_OUT_DIR)
+ this.processingOptions.putAll(processorOptions)
+ }
+ }
+
+ override fun execute(
+ workingDir: File,
+ arguments: CompilationStepArguments
+ ): CompilationStepResult {
+ if (symbolProcessorProviders.isEmpty()) {
+ return CompilationStepResult.skip(arguments)
+ }
+ val kspMessages = DiagnosticsMessageCollector()
+ val result = KotlinCliRunner.runKotlinCli(
+ arguments = arguments,
+ destinationDir = workingDir.resolve(CLASS_OUT_FOLDER_NAME),
+ pluginRegistrars = listOf(
+ TestKspRegistrar(
+ kspWorkingDir = workingDir.resolve("ksp-compiler"),
+ baseOptions = createKspOptions(workingDir),
+ processorProviders = symbolProcessorProviders,
+ messageCollector = kspMessages
+ )
+ )
+ )
+
+ val generatedSources = listOfNotNull(
+ workingDir.resolve(KOTLIN_OUT_DIR).toSourceSet(),
+ workingDir.resolve(JAVA_OUT_DIR).toSourceSet(),
+ )
+ val diagnostics = resolveDiagnostics(
+ diagnostics = result.diagnostics + kspMessages.getDiagnostics(),
+ sourceSets = arguments.sourceSets + generatedSources
+ )
+ return CompilationStepResult(
+ success = result.exitCode == ExitCode.OK,
+ generatedSourceRoots = generatedSources,
+ diagnostics = diagnostics,
+ nextCompilerArguments = arguments.copy(
+ sourceSets = arguments.sourceSets + generatedSources
+ ),
+ outputClasspath = listOf(result.compiledClasspath)
+ )
+ }
+
+ companion object {
+ private const val JAVA_OUT_DIR = "generatedJava"
+ private const val KOTLIN_OUT_DIR = "generatedKotlin"
+ private const val CLASS_OUT_FOLDER_NAME = "class-out"
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt
index 844bb20..b3f574f 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt
@@ -32,7 +32,7 @@
fun canRun(params: TestCompilationParameters): Boolean
- fun compile(params: TestCompilationParameters): CompilationResult
+ fun compile(workingDir: File, params: TestCompilationParameters): CompilationResult
}
@ExperimentalProcessingApi
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt
index d3bd3a8..9b797e3 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt
@@ -19,9 +19,13 @@
import androidx.room.compiler.processing.ExperimentalProcessingApi
import androidx.room.compiler.processing.SyntheticJavacProcessor
import androidx.room.compiler.processing.util.CompilationResult
+import androidx.room.compiler.processing.util.DiagnosticMessage
import androidx.room.compiler.processing.util.JavaCompileTestingCompilationResult
import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.toDiagnosticMessages
+import com.google.testing.compile.Compilation
import com.google.testing.compile.Compiler
+import java.io.File
@ExperimentalProcessingApi
internal object JavacCompilationTestRunner : CompilationTestRunner {
@@ -32,7 +36,7 @@
return params.sources.all { it is Source.JavaSource }
}
- override fun compile(params: TestCompilationParameters): CompilationResult {
+ override fun compile(workingDir: File, params: TestCompilationParameters): CompilationResult {
val syntheticJavacProcessor = SyntheticJavacProcessor(params.handlers)
val sources = if (params.sources.isEmpty()) {
// synthesize a source to trigger compilation
@@ -63,14 +67,30 @@
it
}
}
- val javaFileObjects = sources.map {
- it.toJFO()
+ val javaFileObjects = sources.associateBy { it.toJFO() }
+ val compilation = compiler.compile(javaFileObjects.keys)
+ val generatedSources = if (compilation.status() == Compilation.Status.SUCCESS) {
+ compilation.generatedSourceFiles().associate {
+ it to Source.fromJavaFileObject(it)
+ }
+ } else {
+ compilation.diagnostics().mapNotNull {
+ it.source
+ }.associate {
+ it to Source.fromJavaFileObject(it)
+ }
}
- val compilation = compiler.compile(javaFileObjects)
+
+ val diagnostics: List<DiagnosticMessage> = compilation.diagnostics().toDiagnosticMessages(
+ javaFileObjects + generatedSources
+ )
+
return JavaCompileTestingCompilationResult(
testRunner = this,
delegate = compilation,
- processor = syntheticJavacProcessor
+ processor = syntheticJavacProcessor,
+ diagnostics = diagnostics.groupBy { it.kind },
+ generatedSources = generatedSources.values.toList()
)
}
}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt
index 651d73b..2878067 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 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.
@@ -19,40 +19,36 @@
import androidx.room.compiler.processing.ExperimentalProcessingApi
import androidx.room.compiler.processing.SyntheticJavacProcessor
import androidx.room.compiler.processing.util.CompilationResult
-import androidx.room.compiler.processing.util.KotlinCompilationUtil
-import androidx.room.compiler.processing.util.KotlinCompileTestingCompilationResult
-import com.tschuchort.compiletesting.KotlinCompilation
-import java.io.ByteArrayOutputStream
+import androidx.room.compiler.processing.util.KotlinCompilationResult
+import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
+import androidx.room.compiler.processing.util.compiler.compile
+import androidx.room.compiler.processing.util.compiler.withAtLeastOneKotlinSource
+import java.io.File
@ExperimentalProcessingApi
internal object KaptCompilationTestRunner : CompilationTestRunner {
-
override val name: String = "kapt"
override fun canRun(params: TestCompilationParameters): Boolean {
return true
}
- override fun compile(params: TestCompilationParameters): CompilationResult {
+ override fun compile(workingDir: File, params: TestCompilationParameters): CompilationResult {
val syntheticJavacProcessor = SyntheticJavacProcessor(params.handlers)
- val outputStream = ByteArrayOutputStream()
- val compilation = KotlinCompilationUtil.prepareCompilation(
+ val args = TestCompilationArguments(
sources = params.sources,
- outputStream = outputStream,
- classpaths = params.classpath
+ classpath = params.classpath,
+ kaptProcessors = listOf(syntheticJavacProcessor),
+ processorOptions = params.options
+ ).withAtLeastOneKotlinSource()
+ val result = compile(
+ workingDir = workingDir,
+ arguments = args
)
- compilation.kaptArgs.putAll(params.options)
- compilation.annotationProcessors = listOf(syntheticJavacProcessor)
- val result = compilation.compile()
- return KotlinCompileTestingCompilationResult(
+ return KotlinCompilationResult(
testRunner = this,
- delegate = result,
processor = syntheticJavacProcessor,
- successfulCompilation = result.exitCode == KotlinCompilation.ExitCode.OK,
- outputSourceDirs = listOf(
- compilation.kaptSourceDir, compilation.kaptKotlinGeneratedDir
- ),
- rawOutput = outputStream.toString(Charsets.UTF_8),
+ delegate = result
)
}
}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
index 88c206d..9d7bbfe 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 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.
@@ -19,41 +19,24 @@
import androidx.room.compiler.processing.ExperimentalProcessingApi
import androidx.room.compiler.processing.SyntheticKspProcessor
import androidx.room.compiler.processing.util.CompilationResult
-import androidx.room.compiler.processing.util.KotlinCompilationUtil
-import androidx.room.compiler.processing.util.KotlinCompileTestingCompilationResult
-import androidx.room.compiler.processing.util.CompilationTestCapabilities
-import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.KotlinCompilationResult
+import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
+import androidx.room.compiler.processing.util.compiler.compile
+import androidx.room.compiler.processing.util.compiler.withAtLeastOneKotlinSource
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
-import com.tschuchort.compiletesting.KotlinCompilation
-import com.tschuchort.compiletesting.SourceFile
-import com.tschuchort.compiletesting.kspArgs
-import com.tschuchort.compiletesting.kspSourcesDir
-import com.tschuchort.compiletesting.symbolProcessorProviders
-import java.io.ByteArrayOutputStream
import java.io.File
-import javax.tools.Diagnostic
@ExperimentalProcessingApi
internal object KspCompilationTestRunner : CompilationTestRunner {
-
override val name: String = "ksp"
override fun canRun(params: TestCompilationParameters): Boolean {
- return CompilationTestCapabilities.canTestWithKsp
+ return true
}
- override fun compile(params: TestCompilationParameters): CompilationResult {
- @Suppress("NAME_SHADOWING")
- val sources = if (params.sources.none { it is Source.KotlinSource }) {
- // looks like this requires a kotlin source file
- // see: https://github.com/tschuchortdev/kotlin-compile-testing/issues/57
- params.sources + Source.kotlin("placeholder.kt", "")
- } else {
- params.sources
- }
-
+ override fun compile(workingDir: File, params: TestCompilationParameters): CompilationResult {
val processorProvider = object : SymbolProcessorProvider {
lateinit var processor: SyntheticKspProcessor
@@ -61,63 +44,20 @@
return SyntheticKspProcessor(environment, params.handlers).also { processor = it }
}
}
-
- val combinedOutputStream = ByteArrayOutputStream()
- val kspCompilation = KotlinCompilationUtil.prepareCompilation(
- sources = sources,
- outputStream = combinedOutputStream,
- classpaths = params.classpath
+ val args = TestCompilationArguments(
+ sources = params.sources,
+ classpath = params.classpath,
+ symbolProcessorProviders = listOf(processorProvider),
+ processorOptions = params.options
+ ).withAtLeastOneKotlinSource()
+ val result = compile(
+ workingDir = workingDir,
+ arguments = args
)
- kspCompilation.kspArgs.putAll(params.options)
- kspCompilation.symbolProcessorProviders = listOf(processorProvider)
- kspCompilation.compile()
- // ignore KSP result for now because KSP stops compilation, which might create false
- // negatives when java code accesses kotlin code.
- // TODO: fix once https://github.com/tschuchortdev/kotlin-compile-testing/issues/72 is
- // fixed
-
- // after ksp, compile without ksp with KSP's output as input
- val finalCompilation = KotlinCompilationUtil.prepareCompilation(
- sources = sources,
- outputStream = combinedOutputStream,
- classpaths = params.classpath,
- )
- // build source files from generated code
- finalCompilation.sources += kspCompilation.kspJavaSourceDir.collectSourceFiles() +
- kspCompilation.kspKotlinSourceDir.collectSourceFiles()
- val result = finalCompilation.compile()
- val syntheticKspProcessor = processorProvider.processor
- // workaround for: https://github.com/google/ksp/issues/122
- // KSP does not fail compilation for error diagnostics hence we do it here.
- val hasErrorDiagnostics = syntheticKspProcessor.messageWatcher
- .diagnostics()[Diagnostic.Kind.ERROR].orEmpty().isNotEmpty()
- return KotlinCompileTestingCompilationResult(
+ return KotlinCompilationResult(
testRunner = this,
- delegate = result,
- processor = syntheticKspProcessor,
- successfulCompilation = result.exitCode == KotlinCompilation.ExitCode.OK &&
- !hasErrorDiagnostics,
- outputSourceDirs = listOf(
- kspCompilation.kspJavaSourceDir,
- kspCompilation.kspKotlinSourceDir
- ),
- rawOutput = combinedOutputStream.toString(Charsets.UTF_8),
+ processor = processorProvider.processor,
+ delegate = result
)
}
-
- // TODO get rid of these once kotlin compile testing supports two step compilation for KSP.
- // https://github.com/tschuchortdev/kotlin-compile-testing/issues/72
- private val KotlinCompilation.kspJavaSourceDir: File
- get() = kspSourcesDir.resolve("java")
-
- private val KotlinCompilation.kspKotlinSourceDir: File
- get() = kspSourcesDir.resolve("kotlin")
-
- private fun File.collectSourceFiles(): List<SourceFile> {
- return walkTopDown().filter {
- it.isFile
- }.map { file ->
- SourceFile.fromPath(file)
- }.toList()
- }
}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/com/tschuchort/compiletesting/KspKotlinCompileTesting.kt b/room/room-compiler-processing-testing/src/main/java/com/tschuchort/compiletesting/KspKotlinCompileTesting.kt
deleted file mode 100644
index 8ac19ff..0000000
--- a/room/room-compiler-processing-testing/src/main/java/com/tschuchort/compiletesting/KspKotlinCompileTesting.kt
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright 2021 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.
- */
-
-/**
- * This file replicates the KSP support in KotlinCompileTesting as a workaround not to wait for
- * the library to take KSP beta05 update.
- *
- * Ideally, this file should either disappear and replaced with the KCT library OR moved to a
- * real AndroidX library as a replacement for KCT.
- */
-
-package com.tschuchort.compiletesting
-
-import com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension
-import com.google.devtools.ksp.KspOptions
-import com.google.devtools.ksp.processing.KSPLogger
-import com.google.devtools.ksp.processing.SymbolProcessorProvider
-import com.google.devtools.ksp.processing.impl.MessageCollectorBasedKSPLogger
-import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
-import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
-import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
-import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot
-import org.jetbrains.kotlin.com.intellij.core.CoreApplicationEnvironment
-import org.jetbrains.kotlin.com.intellij.mock.MockProject
-import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeAdapter
-import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeListener
-import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
-import org.jetbrains.kotlin.config.CompilerConfiguration
-import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension
-import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
-import java.io.File
-
-/**
- * The list of symbol processors for the kotlin compilation.
- * https://goo.gle/ksp
- */
-var KotlinCompilation.symbolProcessorProviders: List<SymbolProcessorProvider>
- get() = getKspRegistrar().providers
- set(value) {
- val registrar = getKspRegistrar()
- registrar.providers = value
- }
-
-/**
- * The directory where generated KSP sources are written
- */
-val KotlinCompilation.kspSourcesDir: File
- get() = kspWorkingDir.resolve("sources")
-
-/**
- * Arbitrary arguments to be passed to ksp
- */
-var KotlinCompilation.kspArgs: MutableMap<String, String>
- get() = getKspRegistrar().options
- set(value) {
- val registrar = getKspRegistrar()
- registrar.options = value
- }
-
-private val KotlinCompilation.kspJavaSourceDir: File
- get() = kspSourcesDir.resolve("java")
-
-private val KotlinCompilation.kspKotlinSourceDir: File
- get() = kspSourcesDir.resolve("kotlin")
-
-private val KotlinCompilation.kspResources: File
- get() = kspSourcesDir.resolve("resources")
-
-/**
- * The working directory for KSP
- */
-private val KotlinCompilation.kspWorkingDir: File
- get() = workingDir.resolve("ksp")
-
-/**
- * The directory where compiled KSP classes are written
- */
-// TODO this seems to be ignored by KSP and it is putting classes into regular classes directory
-// but we still need to provide it in the KSP options builder as it is required
-// once it works, we should make the property public.
-private val KotlinCompilation.kspClassesDir: File
- get() = kspWorkingDir.resolve("classes")
-
-/**
- * The directory where compiled KSP caches are written
- */
-private val KotlinCompilation.kspCachesDir: File
- get() = kspWorkingDir.resolve("caches")
-
-/**
- * Custom subclass of [AbstractKotlinSymbolProcessingExtension] where processors are pre-defined instead of being
- * loaded via ServiceLocator.
- */
-private class KspTestExtension(
- options: KspOptions,
- processorProviders: List<SymbolProcessorProvider>,
- logger: KSPLogger
-) : AbstractKotlinSymbolProcessingExtension(
- options = options,
- logger = logger,
- testMode = false
-) {
- private val loadedProviders = processorProviders
-
- override fun loadProviders() = loadedProviders
-}
-
-/**
- * Registers the [KspTestExtension] to load the given list of processors.
- */
-private class KspCompileTestingComponentRegistrar(
- private val compilation: KotlinCompilation
-) : ComponentRegistrar {
- var providers = emptyList<SymbolProcessorProvider>()
-
- var options: MutableMap<String, String> = mutableMapOf()
-
- var incremental: Boolean = false
- var incrementalLog: Boolean = false
-
- override fun registerProjectComponents(
- project: MockProject,
- configuration: CompilerConfiguration
- ) {
- if (providers.isEmpty()) {
- return
- }
- val options = KspOptions.Builder().apply {
- this.projectBaseDir = compilation.kspWorkingDir
-
- this.processingOptions.putAll(compilation.kspArgs)
-
- this.incremental = this@KspCompileTestingComponentRegistrar.incremental
- this.incrementalLog = this@KspCompileTestingComponentRegistrar.incrementalLog
-
- this.cachesDir = compilation.kspCachesDir.also {
- it.deleteRecursively()
- it.mkdirs()
- }
- this.kspOutputDir = compilation.kspSourcesDir.also {
- it.deleteRecursively()
- it.mkdirs()
- }
- this.classOutputDir = compilation.kspClassesDir.also {
- it.deleteRecursively()
- it.mkdirs()
- }
- this.javaOutputDir = compilation.kspJavaSourceDir.also {
- it.deleteRecursively()
- it.mkdirs()
- }
- this.kotlinOutputDir = compilation.kspKotlinSourceDir.also {
- it.deleteRecursively()
- it.mkdirs()
- }
- this.resourceOutputDir = compilation.kspResources.also {
- it.deleteRecursively()
- it.mkdirs()
- }
- configuration[CLIConfigurationKeys.CONTENT_ROOTS]
- ?.filterIsInstance<JavaSourceRoot>()
- ?.forEach {
- this.javaSourceRoots.add(it.file)
- }
- }.build()
-
- // Temporary until friend-paths is fully supported https://youtrack.jetbrains.com/issue/KT-34102
- @Suppress("invisible_member")
- val messageCollectorBasedKSPLogger = MessageCollectorBasedKSPLogger(
- PrintingMessageCollector(
- compilation.internalMessageStreamAccess,
- MessageRenderer.GRADLE_STYLE,
- compilation.verbose
- )
- )
- // Placeholder extension point; Required by dropPsiCaches().
- @Suppress("UnstableApiUsage")
- CoreApplicationEnvironment.registerExtensionPoint(
- project.extensionArea,
- PsiTreeChangeListener.EP.name,
- PsiTreeChangeAdapter::class.java
- )
- val registrar = KspTestExtension(options, providers, messageCollectorBasedKSPLogger)
- AnalysisHandlerExtension.registerExtension(project, registrar)
- }
-}
-
-/**
- * Gets the test registrar from the plugin list or adds if it does not exist.
- */
-private fun KotlinCompilation.getKspRegistrar(): KspCompileTestingComponentRegistrar {
- compilerPlugins.firstIsInstanceOrNull<KspCompileTestingComponentRegistrar>()?.let {
- return it
- }
- val kspRegistrar = KspCompileTestingComponentRegistrar(this)
- compilerPlugins = compilerPlugins + kspRegistrar
- return kspRegistrar
-}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar b/room/room-compiler-processing-testing/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
new file mode 100644
index 0000000..ffaca31
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
@@ -0,0 +1,17 @@
+#
+# Copyright 2021 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.
+#
+
+androidx.room.compiler.processing.util.compiler.DelegatingTestRegistrar
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/DiagnosticMessageCollectorTest.kt b/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/DiagnosticMessageCollectorTest.kt
new file mode 100644
index 0000000..e62ba38
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/DiagnosticMessageCollectorTest.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2021 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.util
+
+import androidx.room.compiler.processing.util.compiler.DiagnosticsMessageCollector
+import androidx.room.compiler.processing.util.compiler.steps.RawDiagnosticMessage
+import androidx.room.compiler.processing.util.compiler.steps.RawDiagnosticMessage.Location
+import com.google.common.truth.Truth.assertThat
+import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import javax.tools.Diagnostic
+
+@RunWith(Parameterized::class)
+internal class DiagnosticMessageCollectorTest(
+ private val params: TestParams
+) {
+ @Test
+ fun parseDiagnosticMessage() {
+ val collector = DiagnosticsMessageCollector()
+ collector.report(
+ severity = params.severity,
+ message = params.message
+ )
+ assertThat(
+ collector.getDiagnostics().firstOrNull()
+ ).isEqualTo(params.expected)
+ }
+
+ internal class TestParams(
+ val message: String,
+ val severity: CompilerMessageSeverity,
+ val expected: RawDiagnosticMessage
+ ) {
+ override fun toString(): String {
+ return message
+ }
+ }
+ companion object {
+ @get:JvmStatic
+ @get:Parameterized.Parameters(name = "{0}")
+ internal val testCases = listOf(
+ // ksp kotlin
+ TestParams(
+ message = "[ksp] /foo/bar/Subject.kt:3: the real message",
+ severity = CompilerMessageSeverity.ERROR,
+ expected = RawDiagnosticMessage(
+ kind = Diagnostic.Kind.ERROR,
+ message = "the real message",
+ location = Location(
+ path = "/foo/bar/Subject.kt",
+ line = 3
+ )
+ )
+ ),
+ // ksp java
+ TestParams(
+ message = "[ksp] /foo/bar/Subject.java:3: the real message",
+ severity = CompilerMessageSeverity.ERROR,
+ expected = RawDiagnosticMessage(
+ kind = Diagnostic.Kind.ERROR,
+ message = "the real message",
+ location = Location(
+ path = "/foo/bar/Subject.java",
+ line = 3
+ )
+ )
+ ),
+ // ksp not a kotlin file - bad extension
+ TestParams(
+ message = "[ksp] /foo/bar/Subject.ktn:3: the real message",
+ severity = CompilerMessageSeverity.ERROR,
+ expected = RawDiagnosticMessage(
+ kind = Diagnostic.Kind.ERROR,
+ message = "/foo/bar/Subject.ktn:3: the real message",
+ location = null
+ )
+ ),
+ // ksp not a kotlin file - no dot
+ TestParams(
+ message = "[ksp] /foo/bar/Subjectkt:3: the real message",
+ severity = CompilerMessageSeverity.ERROR,
+ expected = RawDiagnosticMessage(
+ kind = Diagnostic.Kind.ERROR,
+ message = "/foo/bar/Subjectkt:3: the real message",
+ location = null
+ )
+ ),
+ // ksp not a java file - bad extension
+ TestParams(
+ message = "[ksp] /foo/bar/Subject.javax:3: the real message",
+ severity = CompilerMessageSeverity.ERROR,
+ expected = RawDiagnosticMessage(
+ kind = Diagnostic.Kind.ERROR,
+ message = "/foo/bar/Subject.javax:3: the real message",
+ location = null
+ )
+ ),
+ // ksp not a java file - no dot
+ TestParams(
+ message = "[ksp] /foo/bar/Subjectjava:3: the real message",
+ severity = CompilerMessageSeverity.ERROR,
+ expected = RawDiagnosticMessage(
+ kind = Diagnostic.Kind.ERROR,
+ message = "/foo/bar/Subjectjava:3: the real message",
+ location = null
+ )
+ ),
+ // kapt kotlin
+ TestParams(
+ message = "/foo/bar/Subject.kt:2: warning: the real message",
+ severity = CompilerMessageSeverity.WARNING,
+ expected = RawDiagnosticMessage(
+ kind = Diagnostic.Kind.WARNING,
+ message = "the real message",
+ location = Location(
+ path = "/foo/bar/Subject.kt",
+ line = 2
+ )
+ )
+ ),
+ // kapt java
+ TestParams(
+ message = "/foo/bar/Subject.java:2: warning: the real message",
+ severity = CompilerMessageSeverity.WARNING,
+ expected = RawDiagnosticMessage(
+ kind = Diagnostic.Kind.WARNING,
+ message = "the real message",
+ location = Location(
+ path = "/foo/bar/Subject.java",
+ line = 2
+ )
+ )
+ ),
+ // kapt not a kotlin file - bad extension
+ TestParams(
+ message = "/foo/bar/Subject.ktn:2: warning: the real message",
+ severity = CompilerMessageSeverity.WARNING,
+ expected = RawDiagnosticMessage(
+ kind = Diagnostic.Kind.WARNING,
+ message = "/foo/bar/Subject.ktn:2: warning: the real message",
+ location = null
+ )
+ ),
+ // kapt not a kotlin file - no dot
+ TestParams(
+ message = "/foo/bar/Subjectkt:2: warning: the real message",
+ severity = CompilerMessageSeverity.WARNING,
+ expected = RawDiagnosticMessage(
+ kind = Diagnostic.Kind.WARNING,
+ message = "/foo/bar/Subjectkt:2: warning: the real message",
+ location = null
+ )
+ ),
+ // kapt not a java file - bad extension
+ TestParams(
+ message = "/foo/bar/Subject.javan:2: warning: the real message",
+ severity = CompilerMessageSeverity.WARNING,
+ expected = RawDiagnosticMessage(
+ kind = Diagnostic.Kind.WARNING,
+ message = "/foo/bar/Subject.javan:2: warning: the real message",
+ location = null
+ )
+ ),
+ // kapt not a java file - no dot
+ TestParams(
+ message = "/foo/bar/Subjectjava:2: warning: the real message",
+ severity = CompilerMessageSeverity.WARNING,
+ expected = RawDiagnosticMessage(
+ kind = Diagnostic.Kind.WARNING,
+ message = "/foo/bar/Subjectjava:2: warning: the real message",
+ location = null
+ )
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/DiagnosticsTest.kt b/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/DiagnosticsTest.kt
new file mode 100644
index 0000000..196b794
--- /dev/null
+++ b/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/DiagnosticsTest.kt
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2021 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.util
+
+import androidx.room.compiler.processing.ExperimentalProcessingApi
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import javax.tools.Diagnostic
+
+@RunWith(Parameterized::class)
+@OptIn(ExperimentalProcessingApi::class)
+class DiagnosticsTest internal constructor(
+ private val runTest: TestRunner
+) : MultiBackendTest() {
+
+ @Test
+ fun diagnosticsMessagesWithoutSource() {
+ runTest { invocation ->
+ invocation.processingEnv.messager.run {
+ printMessage(Diagnostic.Kind.NOTE, "note 1")
+ printMessage(Diagnostic.Kind.WARNING, "warn 1")
+ printMessage(Diagnostic.Kind.ERROR, "error 1")
+ }
+ invocation.assertCompilationResult {
+ hasNote("note 1")
+ hasWarning("warn 1")
+ hasError("error 1")
+ hasNoteContaining("ote")
+ hasWarningContaining("arn")
+ hasErrorContaining("rror")
+ // these should fail:
+ assertThat(
+ runCatching { hasNote("note") }.isFailure
+ ).isTrue()
+ assertThat(
+ runCatching { hasWarning("warn") }.isFailure
+ ).isTrue()
+ assertThat(
+ runCatching { hasError("error") }.isFailure
+ ).isTrue()
+ assertThat(
+ runCatching { hasNoteContaining("error") }.isFailure
+ ).isTrue()
+ assertThat(
+ runCatching { hasWarningContaining("note") }.isFailure
+ ).isTrue()
+ assertThat(
+ runCatching { hasErrorContaining("warning") }.isFailure
+ ).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun diagnoticMessageOnKotlinSource() {
+ runTest.assumeCanCompileKotlin()
+ val source = Source.kotlin(
+ "Subject.kt",
+ """
+ package foo.bar
+ class Subject {
+ val field: String = "foo"
+ }
+ """.trimIndent()
+ )
+ runTest(listOf(source)) { invocation ->
+ val field = invocation.processingEnv.requireTypeElement("foo.bar.Subject")
+ .getDeclaredFields().first()
+ invocation.processingEnv.messager.printMessage(
+ kind = Diagnostic.Kind.WARNING,
+ msg = "warning on field",
+ element = field
+ )
+ val expectedLine = if (invocation.isKsp) {
+ 3
+ } else {
+ // KAPT fails to report lines properly in certain cases
+ // (e.g. when searching for field, it uses its parent rather than itself)
+ // Hopefully, once it is fixed, this test will break and we can remove this if/else
+ // https://youtrack.jetbrains.com/issue/KT-47934
+ 2
+ }
+ invocation.assertCompilationResult {
+ hasWarningContaining("on field")
+ .onLine(expectedLine)
+ .onSource(source)
+ }
+ }
+ }
+
+ @Test
+ fun diagnoticMessageOnJavaSource() {
+ val source = Source.java(
+ "foo.bar.Subject",
+ """
+ package foo.bar;
+ public class Subject {
+ String field = "";
+ }
+ """.trimIndent()
+ )
+ runTest(listOf(source)) { invocation ->
+ val field = invocation.processingEnv.requireTypeElement("foo.bar.Subject")
+ .getDeclaredFields().first()
+ invocation.processingEnv.messager.printMessage(
+ kind = Diagnostic.Kind.WARNING,
+ msg = "warning on field",
+ element = field
+ )
+ invocation.assertCompilationResult {
+ hasWarningContaining("on field")
+ .onLine(3)
+ .onSource(source)
+ }
+ }
+ }
+
+ @Test
+ fun cleanJavaCompilationHasNoWarnings() {
+ val javaSource = Source.java(
+ "foo.bar.Subject",
+ """
+ package foo.bar;
+ public class Subject {
+ }
+ """.trimIndent()
+ )
+ cleanCompilationHasNoWarnings(javaSource)
+ cleanCompilationHasNoWarnings(
+ options = mapOf("foo" to "bar"),
+ javaSource
+ )
+ }
+
+ @Test
+ fun cleanKotlinCompilationHasNoWarnings() {
+ val kotlinSource = Source.kotlin(
+ "Subject",
+ """
+ package foo.bar
+ class Subject {
+ }
+ """.trimIndent()
+ )
+ cleanCompilationHasNoWarnings(kotlinSource)
+ cleanCompilationHasNoWarnings(
+ options = mapOf("foo" to "bar"),
+ kotlinSource
+ )
+ }
+
+ @Test
+ fun cleanJavaCompilationWithSomeAnnotationsHasNoWarnings() {
+ val annotation = Source.java(
+ "foo.bar.MyAnnotation",
+ """
+ package foo.bar;
+ public @interface MyAnnotation {}
+ """.trimIndent()
+ )
+ val source = Source.java(
+ "foo.bar.Subject",
+ """
+ package foo.bar;
+ @MyAnnotation
+ public class Subject {}
+ """.trimIndent()
+ )
+ cleanCompilationHasNoWarnings(annotation, source)
+ }
+
+ @Test
+ fun cleanKotlinCompilationWithSomeAnnotationsHasNoWarnings() {
+ val source = Source.kotlin(
+ "Foo.kt",
+ """
+ annotation class MyAnnotation
+
+ @MyAnnotation
+ class Subject {}
+ """.trimIndent()
+ )
+ cleanCompilationHasNoWarnings(source)
+ }
+
+ private fun cleanCompilationHasNoWarnings(
+ vararg source: Source
+ ) = cleanCompilationHasNoWarnings(options = emptyMap(), source = source)
+
+ private fun cleanCompilationHasNoWarnings(
+ options: Map<String, String>,
+ vararg source: Source
+ ) {
+ if (source.any { it is Source.KotlinSource }) {
+ runTest.assumeCanCompileKotlin()
+ }
+ runTest(options = options, sources = source.toList()) {
+ // no report
+ it.assertCompilationResult {
+ hasNoWarnings()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/GeneratedCodeMatchTest.kt b/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/GeneratedCodeMatchTest.kt
index 87a28e2..08b0e61 100644
--- a/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/GeneratedCodeMatchTest.kt
+++ b/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/GeneratedCodeMatchTest.kt
@@ -16,6 +16,7 @@
package androidx.room.compiler.processing.util
+import androidx.room.compiler.processing.ExperimentalProcessingApi
import com.google.common.truth.Truth.assertThat
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.TypeName
@@ -29,6 +30,7 @@
import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
+@OptIn(ExperimentalProcessingApi::class)
class GeneratedCodeMatchTest internal constructor(
private val runTest: TestRunner
) : MultiBackendTest() {
@@ -119,9 +121,7 @@
@Test
fun successfulGeneratedKotlinCodeMatch() {
// java environment will not generate kotlin files
- if (runTest.toString() == "java") {
- throw AssumptionViolatedException("javaAP won't generate kotlin code.")
- }
+ runTest.assumeCanCompileKotlin()
val file = FileSpec.builder("foo.bar", "Baz")
.addType(KTypeSpec.classBuilder("Baz").build())
@@ -141,9 +141,7 @@
@Test
fun missingGeneratedKotlinCode_mismatch() {
// java environment will not generate kotlin files
- if (runTest.toString() == "java") {
- throw AssumptionViolatedException("javaAP won't generate kotlin code.")
- }
+ runTest.assumeCanCompileKotlin()
val generated = FileSpec.builder("foo.bar", "Baz")
.addType(
diff --git a/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/MultiBackendTest.kt b/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/MultiBackendTest.kt
index 80e2465..80d423f 100644
--- a/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/MultiBackendTest.kt
+++ b/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/MultiBackendTest.kt
@@ -16,15 +16,37 @@
package androidx.room.compiler.processing.util
+import androidx.room.compiler.processing.ExperimentalProcessingApi
+import org.junit.AssumptionViolatedException
import org.junit.runners.Parameterized
+@OptIn(ExperimentalProcessingApi::class)
class TestRunner(
private val name: String,
- private val runner: (List<(XTestInvocation) -> Unit>) -> Unit
+ private val runner: (
+ sources: List<Source>,
+ options: Map<String, String>,
+ handlers: List<(XTestInvocation) -> Unit>
+ ) -> Unit
) {
- operator fun invoke(handlers: List<(XTestInvocation) -> Unit>) = runner(handlers)
- operator fun invoke(handler: (XTestInvocation) -> Unit) = runner(listOf(handler))
+ operator fun invoke(handlers: List<(XTestInvocation) -> Unit>) =
+ runner(emptyList(), emptyMap(), handlers)
+
+ operator fun invoke(handler: (XTestInvocation) -> Unit) =
+ runner(emptyList(), emptyMap(), listOf(handler))
+
+ operator fun invoke(
+ sources: List<Source>,
+ options: Map<String, String> = emptyMap(),
+ handler: (XTestInvocation) -> Unit
+ ) = runner(sources, options, listOf(handler))
+
override fun toString() = name
+ fun assumeCanCompileKotlin() {
+ if (name == "java") {
+ throw AssumptionViolatedException("cannot compile kotlin sources")
+ }
+ }
}
/**
@@ -32,18 +54,19 @@
*/
abstract class MultiBackendTest {
companion object {
+ @OptIn(ExperimentalProcessingApi::class)
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun runners(): List<TestRunner> = listOfNotNull(
- TestRunner("java") {
- runJavaProcessorTest(sources = emptyList(), handlers = it)
+ TestRunner("java") { sources, options, handlers ->
+ runJavaProcessorTest(sources = sources, options = options, handlers = handlers)
},
- TestRunner("kapt") {
- runKaptTest(sources = emptyList(), handlers = it)
+ TestRunner("kapt") { sources, options, handlers ->
+ runKaptTest(sources = sources, options = options, handlers = handlers)
},
if (CompilationTestCapabilities.canTestWithKsp) {
- TestRunner("ksp") {
- runKspTest(sources = emptyList(), handlers = it)
+ TestRunner("ksp") { sources, options, handlers ->
+ runKspTest(sources = sources, options = options, handlers = handlers)
}
} else {
null
diff --git a/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt b/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt
index 6ba44be..4bbab5b 100644
--- a/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt
+++ b/room/room-compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt
@@ -17,16 +17,110 @@
package androidx.room.compiler.processing.util
import androidx.room.compiler.processing.ExperimentalProcessingApi
+import androidx.room.compiler.processing.SyntheticJavacProcessor
+import androidx.room.compiler.processing.SyntheticKspProcessor
+import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
+import androidx.room.compiler.processing.util.compiler.compile
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.squareup.javapoet.CodeBlock
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.TypeSpec
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.KModifier
import org.junit.Test
+import java.net.URLClassLoader
+import java.nio.file.Files
+import javax.lang.model.element.Modifier
import javax.tools.Diagnostic
@OptIn(ExperimentalProcessingApi::class)
class TestRunnerTest {
@Test
+ fun compileFilesForClasspath() {
+ val kotlinSource = Source.kotlin(
+ "Foo.kt",
+ """
+ class KotlinClass1
+ class KotlinClass2
+ """.trimIndent()
+ )
+ val javaSource = Source.java(
+ "foo.bar.JavaClass1",
+ """
+ package foo.bar;
+ public class JavaClass1 {}
+ """.trimIndent()
+ )
+
+ val kspProcessorProvider = object : SymbolProcessorProvider {
+ override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
+ return SyntheticKspProcessor(
+ environment,
+ listOf { invocation ->
+ if (
+ invocation.processingEnv.findTypeElement("gen.GeneratedKotlin")
+ == null
+ ) {
+ invocation.processingEnv.filer.write(
+ FileSpec.builder("gen", "KotlinGen")
+ .addType(
+ com.squareup.kotlinpoet.TypeSpec.classBuilder
+ ("GeneratedKotlin").build()
+ )
+ .build()
+ )
+ }
+ }
+ )
+ }
+ }
+
+ val javaProcessor = SyntheticJavacProcessor(
+ listOf { invocation ->
+ if (
+ invocation.processingEnv.findTypeElement("gen.GeneratedJava")
+ == null
+ ) {
+ invocation.processingEnv.filer.write(
+ JavaFile.builder(
+ "gen",
+ TypeSpec.classBuilder
+ ("GeneratedJava").build()
+ ).build()
+ )
+ }
+ }
+ )
+ val classpaths = compile(
+ workingDir = Files.createTempDirectory("test-runner").toFile(),
+ arguments = TestCompilationArguments(
+ sources = listOf(kotlinSource, javaSource),
+ symbolProcessorProviders = listOf(
+ kspProcessorProvider
+ ),
+ kaptProcessors = listOf(
+ javaProcessor
+ )
+ )
+ ).outputClasspath
+ val classLoader = URLClassLoader.newInstance(
+ classpaths.map {
+ it.toURI().toURL()
+ }.toTypedArray()
+ )
+
+ // try loading generated classes. If any of them fails, it will throw and fail the test
+ classLoader.loadClass("KotlinClass1")
+ classLoader.loadClass("KotlinClass2")
+ classLoader.loadClass("foo.bar.JavaClass1")
+ classLoader.loadClass("gen.GeneratedKotlin")
+ classLoader.loadClass("gen.GeneratedJava")
+ }
+
+ @Test
fun generatedBadCode_expected() = generatedBadCode(assertFailure = true)
@Test(expected = AssertionError::class)
@@ -38,22 +132,35 @@
"a" to "b",
"c" to "d"
)
- runProcessorTest(
- options = testOptions
- ) {
+ val handler: (XTestInvocation) -> Unit = {
assertThat(it.processingEnv.options).containsAtLeastEntriesIn(testOptions)
}
+ runJavaProcessorTest(
+ sources = emptyList(),
+ options = testOptions,
+ handler = handler
+ )
+ runKaptTest(
+ sources = emptyList(),
+ options = testOptions,
+ handler = handler
+ )
+ runKspTest(
+ sources = emptyList(),
+ options = testOptions,
+ handler = handler
+ )
}
private fun generatedBadCode(assertFailure: Boolean) {
+ val badCode = TypeSpec.classBuilder("Foo").apply {
+ addStaticBlock(
+ CodeBlock.of("bad code")
+ )
+ }.build()
+ val badGeneratedFile = JavaFile.builder("foo", badCode).build()
runProcessorTest {
if (it.processingEnv.findTypeElement("foo.Foo") == null) {
- val badCode = TypeSpec.classBuilder("Foo").apply {
- addStaticBlock(
- CodeBlock.of("bad code")
- )
- }.build()
- val badGeneratedFile = JavaFile.builder("foo", badCode).build()
it.processingEnv.filer.write(
badGeneratedFile
)
@@ -61,6 +168,8 @@
if (assertFailure) {
it.assertCompilationResult {
compilationDidFail()
+ hasErrorContaining("';' expected")
+ .onSource(Source.java("foo.Foo", badGeneratedFile.toString()))
}
}
}
@@ -72,44 +181,6 @@
@Test(expected = AssertionError::class)
fun reportedError_unexpected() = reportedError(assertFailure = false)
- @Test
- fun diagnosticsMessages() {
- runProcessorTest { invocation ->
- invocation.processingEnv.messager.run {
- printMessage(Diagnostic.Kind.NOTE, "note 1")
- printMessage(Diagnostic.Kind.WARNING, "warn 1")
- printMessage(Diagnostic.Kind.ERROR, "error 1")
- }
- invocation.assertCompilationResult {
- hasNote("note 1")
- hasWarning("warn 1")
- hasError("error 1")
- hasNoteContaining("ote")
- hasWarningContaining("arn")
- hasErrorContaining("rror")
- // these should fail:
- assertThat(
- runCatching { hasNote("note") }.isFailure
- ).isTrue()
- assertThat(
- runCatching { hasWarning("warn") }.isFailure
- ).isTrue()
- assertThat(
- runCatching { hasError("error") }.isFailure
- ).isTrue()
- assertThat(
- runCatching { hasNoteContaining("error") }.isFailure
- ).isTrue()
- assertThat(
- runCatching { hasWarningContaining("note") }.isFailure
- ).isTrue()
- assertThat(
- runCatching { hasErrorContaining("warning") }.isFailure
- ).isTrue()
- }
- }
- }
-
private fun reportedError(assertFailure: Boolean) {
runProcessorTest {
it.processingEnv.messager.printMessage(
@@ -125,6 +196,56 @@
}
@Test
+ fun accessGeneratedCode() {
+ val kotlinSource = Source.kotlin(
+ "KotlinSubject.kt",
+ """
+ val x: ToBeGeneratedKotlin? = null
+ val y: ToBeGeneratedJava? = null
+ """.trimIndent()
+ )
+ val javaSource = Source.java(
+ "JavaSubject",
+ """
+ public class JavaSubject {
+ public static ToBeGeneratedKotlin x;
+ public static ToBeGeneratedJava y;
+ }
+ """.trimIndent()
+ )
+ runProcessorTest(
+ sources = listOf(kotlinSource, javaSource)
+ ) { invocation ->
+ invocation.processingEnv.findTypeElement("ToBeGeneratedJava").let {
+ if (it == null) {
+ invocation.processingEnv.filer.write(
+ JavaFile.builder(
+ "",
+ TypeSpec.classBuilder("ToBeGeneratedJava").apply {
+ addModifiers(Modifier.PUBLIC)
+ }.build()
+ ).build()
+ )
+ }
+ }
+ invocation.processingEnv.findTypeElement("ToBeGeneratedKotlin").let {
+ if (it == null) {
+ invocation.processingEnv.filer.write(
+ FileSpec.builder("", "Foo")
+ .addType(
+ com.squareup.kotlinpoet.TypeSpec.classBuilder(
+ "ToBeGeneratedKotlin"
+ ).apply {
+ addModifiers(KModifier.PUBLIC)
+ }.build()
+ ).build()
+ )
+ }
+ }
+ }
+ }
+
+ @Test
fun syntacticErrorsAreVisibleInTheErrorMessage_java() {
val src = Source.java(
"test.Foo",
@@ -134,7 +255,7 @@
public static class Foo {}
""".trimIndent()
)
- val errorMessage = "error: modifier static not allowed here"
+ val errorMessage = "modifier static not allowed here"
val javapResult = runCatching {
runJavaProcessorTest(
sources = listOf(src),
diff --git a/room/room-compiler-processing/build.gradle b/room/room-compiler-processing/build.gradle
index 4715bac..d63f0af 100644
--- a/room/room-compiler-processing/build.gradle
+++ b/room/room-compiler-processing/build.gradle
@@ -40,7 +40,6 @@
testImplementation(libs.googleCompileTesting)
testImplementation(libs.junit)
testImplementation(libs.jsr250)
- testImplementation(libs.kotlinCompileTesting)
testImplementation(libs.ksp)
testImplementation(project(":room:room-compiler-processing-testing"))
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMessager.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMessager.kt
index 2acd207..c03ee6a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMessager.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMessager.kt
@@ -42,15 +42,16 @@
// In Javac, the Messager requires all preceding parameters to report an error.
// In KSP, the KspLogger only needs the last so ignore the preceding parameters.
- val ksNode = if (annotationValue != null) {
- (annotationValue as KspAnnotationValue).valueArgument
- } else if (annotation != null) {
- (annotation as KspAnnotation).ksAnnotated
- } else {
- (element as KspElement).declaration
- }
-
- if (ksNode.location == NonExistLocation) {
+ val nodes = sequence {
+ yield((annotationValue as? KspAnnotationValue)?.valueArgument)
+ yield((annotation as? KspAnnotation)?.ksAnnotated)
+ yield((element as? KspElement)?.declaration)
+ }.filterNotNull()
+ val ksNode = nodes.firstOrNull {
+ // pick first node with a location, if possible
+ it.location != NonExistLocation
+ } ?: nodes.firstOrNull() // fallback to the first non-null argument
+ if (ksNode == null || ksNode.location == NonExistLocation) {
internalPrintMessage(kind, "$msg - ${element.fallbackLocationText}", ksNode)
} else {
internalPrintMessage(kind, msg, ksNode)
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XMessagerTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XMessagerTest.kt
index 7777a76..4091a00 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XMessagerTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XMessagerTest.kt
@@ -121,7 +121,8 @@
compilationDidFail()
hasErrorCount(1)
hasWarningCount(0)
- hasError("intentional failure")
+ hasErrorContaining("intentional failure")
+ .onLineContaining("class Foo")
}
}
}
@@ -163,7 +164,8 @@
compilationDidFail()
hasErrorCount(1)
hasWarningCount(0)
- hasError("intentional failure")
+ hasErrorContaining("intentional failure")
+ .onLineContaining("@FooAnnotation")
}
}
}
@@ -212,7 +214,8 @@
compilationDidFail()
hasErrorCount(1)
hasWarningCount(0)
- hasError("intentional failure")
+ hasErrorContaining("intentional failure")
+ .onLineContaining("@FooAnnotation")
}
}
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt
index 725f89f..993a4d0 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt
@@ -22,6 +22,9 @@
import androidx.room.compiler.processing.testcode.MainAnnotation
import androidx.room.compiler.processing.testcode.OtherAnnotation
import androidx.room.compiler.processing.util.CompilationTestCapabilities
+import androidx.room.compiler.processing.util.Source
+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
@@ -36,9 +39,6 @@
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.TypeName
import com.squareup.javapoet.TypeSpec
-import com.tschuchort.compiletesting.KotlinCompilation
-import com.tschuchort.compiletesting.SourceFile
-import com.tschuchort.compiletesting.symbolProcessorProviders
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
@@ -348,8 +348,8 @@
@Test
fun kspProcessingEnvCaching() {
- val main = SourceFile.java(
- "Main.java",
+ val main = Source.java(
+ "foo.bar.Main",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.*;
@@ -411,14 +411,14 @@
}
}
}
- KotlinCompilation().apply {
- workingDir = temporaryFolder.root
- inheritClassPath = true
- symbolProcessorProviders = listOf(processorProvider)
- sources = listOf(main)
- verbose = false
- }.compile()
+ 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()
@@ -517,7 +517,7 @@
}
}
}
- val main = SourceFile.kotlin(
+ val main = Source.kotlin(
"Other.kt",
"""
package foo.bar
@@ -528,13 +528,13 @@
""".trimIndent()
)
- KotlinCompilation().apply {
- workingDir = temporaryFolder.root
- inheritClassPath = true
- symbolProcessorProviders = listOf(processorProvider)
- sources = listOf(main)
- verbose = false
- }.compile()
+ compile(
+ workingDir = temporaryFolder.root,
+ arguments = TestCompilationArguments(
+ sources = listOf(main),
+ symbolProcessorProviders = listOf(processorProvider)
+ )
+ )
assertThat(returned).apply {
isNotNull()
@@ -726,7 +726,7 @@
@Test
fun kspAnnotatedElementsByStep() {
- val main = SourceFile.kotlin(
+ val main = Source.kotlin(
"Classes.kt",
"""
package foo.bar
@@ -774,13 +774,13 @@
}
}
}
- KotlinCompilation().apply {
- workingDir = temporaryFolder.root
- inheritClassPath = true
- symbolProcessorProviders = listOf(processorProvider)
- sources = listOf(main)
- verbose = false
- }.compile()
+ compile(
+ workingDir = temporaryFolder.root,
+ arguments = TestCompilationArguments(
+ sources = listOf(main),
+ symbolProcessorProviders = listOf(processorProvider)
+ )
+ )
assertThat(elementsByStep[mainStep])
.containsExactly("foo.bar.Main")
assertThat(elementsByStep[otherStep])
@@ -790,7 +790,7 @@
@Test
fun kspDeferredStep() {
// create a scenario where we defer the first round of processing
- val main = SourceFile.kotlin(
+ val main = Source.kotlin(
"Classes.kt",
"""
package foo.bar
@@ -840,14 +840,13 @@
}
}
}
- KotlinCompilation().apply {
- workingDir = temporaryFolder.root
- inheritClassPath = true
- symbolProcessorProviders = listOf(processorProvider)
- sources = listOf(main)
- verbose = false
- }.compile()
-
+ 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)
@@ -857,7 +856,7 @@
@Test
fun kspStepOnlyCalledIfElementsToProcess() {
- val main = SourceFile.kotlin(
+ val main = Source.kotlin(
"Classes.kt",
"""
package foo.bar
@@ -900,13 +899,13 @@
}
}
}
- KotlinCompilation().apply {
- workingDir = temporaryFolder.root
- inheritClassPath = true
- symbolProcessorProviders = listOf(processorProvider)
- sources = listOf(main)
- verbose = false
- }.compile()
+ compile(
+ workingDir = temporaryFolder.root,
+ arguments = TestCompilationArguments(
+ sources = listOf(main),
+ symbolProcessorProviders = listOf(processorProvider)
+ )
+ )
assertThat(stepsProcessed).containsExactly(mainStep)
}
}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
index 812a81e..9d0d7e4 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
@@ -364,7 +364,7 @@
@Test
fun isVoidObject() {
val javaBase = Source.java(
- "JavaInterface.java",
+ "JavaInterface",
"""
import java.lang.Void;
interface JavaInterface {
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/AutoMigrationProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/AutoMigrationProcessorTest.kt
index 49148bb..3080136 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/AutoMigrationProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/AutoMigrationProcessorTest.kt
@@ -55,7 +55,7 @@
toSchemaBundle = toSchemaBundle.database
).process()
invocation.assertCompilationResult {
- hasError(AUTOMIGRATION_SPEC_MISSING_NOARG_CONSTRUCTOR)
+ hasErrorContaining(AUTOMIGRATION_SPEC_MISSING_NOARG_CONSTRUCTOR)
}
}
}
@@ -80,7 +80,7 @@
toSchemaBundle = toSchemaBundle.database
).process()
invocation.assertCompilationResult {
- hasError(ProcessorErrors.AUTOMIGRATION_SPEC_MUST_BE_CLASS)
+ hasErrorContaining(ProcessorErrors.AUTOMIGRATION_SPEC_MUST_BE_CLASS)
}
}
}
@@ -111,7 +111,7 @@
toSchemaBundle = toSchemaBundle.database
).process()
invocation.assertCompilationResult {
- hasError(INNER_CLASS_AUTOMIGRATION_SPEC_MUST_BE_STATIC)
+ hasErrorContaining(INNER_CLASS_AUTOMIGRATION_SPEC_MUST_BE_STATIC)
}
}
}
@@ -138,7 +138,7 @@
toSchemaBundle = toSchemaBundle.database
).process()
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.autoMigrationElementMustImplementSpec("MyAutoMigration")
)
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
index 9600800..ddbf0b1 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
@@ -136,7 +136,8 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.INVALID_ANNOTATION_COUNT_IN_DAO_METHOD)
+ hasErrorContaining(ProcessorErrors.INVALID_ANNOTATION_COUNT_IN_DAO_METHOD)
+ .onLine(8)
}
}
}
@@ -306,7 +307,7 @@
`is`(false)
)
invocation.assertCompilationResult {
- hasWarning(ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
+ hasWarningContaining(ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
}
}
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
index 3d6caf9..f0e05bf 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -1367,7 +1367,7 @@
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorCount(2)
- hasError(DO_NOT_USE_GENERIC_IMMUTABLE_MULTIMAP)
+ hasErrorContaining(DO_NOT_USE_GENERIC_IMMUTABLE_MULTIMAP)
hasErrorContaining("Not sure how to convert a Cursor to this method's return type")
}
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
index 66b9271..65180f4 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
@@ -87,7 +87,7 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD)
+ hasErrorContaining(ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD)
}
}
}
@@ -117,8 +117,7 @@
classpathFiles = libraryClasspath
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD)
- hasRawOutputContaining(
+ hasError(
ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD +
" - id in test.library.MissingGetterEntity"
)
@@ -137,7 +136,8 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD)
+ hasErrorContaining(ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD)
+ .onLineContaining("int id")
}
}
}
@@ -153,7 +153,7 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.CANNOT_FIND_SETTER_FOR_FIELD)
+ hasErrorContaining(ProcessorErrors.CANNOT_FIND_SETTER_FOR_FIELD)
}
}
}
@@ -246,7 +246,7 @@
annotation
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.INVALID_INDEX_ORDERS_SIZE)
+ hasErrorContaining(ProcessorErrors.INVALID_INDEX_ORDERS_SIZE)
}
}
}
@@ -283,7 +283,7 @@
`is`(invocation.processingEnv.requireType(TypeName.INT).typeName)
)
invocation.assertCompilationResult {
- hasWarning(
+ hasWarningContaining(
ProcessorErrors.mismatchedSetter(
fieldName = "id",
ownerType = ClassName.bestGuess("foo.bar.MyEntity"),
@@ -325,7 +325,7 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.CANNOT_FIND_SETTER_FOR_FIELD)
+ hasErrorContaining(ProcessorErrors.CANNOT_FIND_SETTER_FOR_FIELD)
}
}
}
@@ -492,7 +492,7 @@
hashMapOf(Pair("tableName", "\" \""))
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.ENTITY_TABLE_NAME_CANNOT_BE_EMPTY)
+ hasErrorContaining(ProcessorErrors.ENTITY_TABLE_NAME_CANNOT_BE_EMPTY)
}
}
}
@@ -504,7 +504,7 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.MISSING_PRIMARY_KEY)
+ hasErrorContaining(ProcessorErrors.MISSING_PRIMARY_KEY)
}
}
}
@@ -518,7 +518,8 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.CANNOT_FIND_COLUMN_TYPE_ADAPTER)
+ hasErrorContaining(ProcessorErrors.CANNOT_FIND_COLUMN_TYPE_ADAPTER)
+ .onLineContaining("myDate")
}
}
}
@@ -540,7 +541,7 @@
) { entity, invocation ->
assertThat(entity.primaryKey.fields.map { it.name }, `is`(listOf("id")))
invocation.assertCompilationResult {
- hasWarning(
+ hasWarningContaining(
ProcessorErrors.embeddedPrimaryKeyIsDropped(
"foo.bar.MyEntity", "x"
)
@@ -814,7 +815,7 @@
annotation
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.INDEX_COLUMNS_CANNOT_BE_EMPTY)
+ hasErrorContaining(ProcessorErrors.INDEX_COLUMNS_CANNOT_BE_EMPTY)
}
}
}
@@ -833,7 +834,7 @@
annotation
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.indexColumnDoesNotExist("bar", listOf("id, foo"))
)
}
@@ -855,7 +856,7 @@
annotation
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.duplicateIndexInEntity("index_MyEntity_foo")
)
}
@@ -887,7 +888,7 @@
) { entity, invocation ->
assertThat(entity.indices.isEmpty(), `is`(true))
invocation.assertCompilationResult {
- hasWarning(
+ hasWarningContaining(
ProcessorErrors.droppedSuperClassFieldIndex(
fieldName = "name",
childEntity = "foo.bar.MyEntity",
@@ -1102,7 +1103,7 @@
) { entity, invocation ->
assertThat(entity.indices.isEmpty(), `is`(true))
invocation.assertCompilationResult {
- hasWarning(
+ hasWarningContaining(
ProcessorErrors.droppedSuperClassIndex(
childEntity = "foo.bar.MyEntity",
superEntity = "foo.bar.Base"
@@ -1136,7 +1137,7 @@
) { entity, invocation ->
assertThat(entity.indices.isEmpty(), `is`(true))
invocation.assertCompilationResult {
- hasWarning(
+ hasWarningContaining(
ProcessorErrors.droppedSuperClassIndex(
childEntity = "foo.bar.MyEntity",
superEntity = "foo.bar.Base"
@@ -1166,7 +1167,7 @@
) { entity, invocation ->
assertThat(entity.indices.isEmpty(), `is`(true))
invocation.assertCompilationResult {
- hasWarning(
+ hasWarningContaining(
ProcessorErrors.droppedEmbeddedIndex(
entityName = "foo.bar.MyEntity.Foo",
fieldPath = "foo",
@@ -1194,7 +1195,7 @@
) { entity, invocation ->
assertThat(entity.indices.isEmpty(), `is`(true))
invocation.assertCompilationResult {
- hasError(ProcessorErrors.CANNOT_USE_MORE_THAN_ONE_POJO_FIELD_ANNOTATION)
+ hasErrorContaining(ProcessorErrors.CANNOT_USE_MORE_THAN_ONE_POJO_FIELD_ANNOTATION)
}
}
}
@@ -1215,7 +1216,7 @@
) { entity, invocation ->
assertThat(entity.indices.isEmpty(), `is`(true))
invocation.assertCompilationResult {
- hasWarning(
+ hasWarningContaining(
ProcessorErrors.droppedEmbeddedFieldIndex("foo > a", "foo.bar.MyEntity")
)
}
@@ -1262,7 +1263,7 @@
attributes = mapOf("primaryKeys" to "\"id\"")
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.multiplePrimaryKeyAnnotations(
listOf("PrimaryKey[id]", "PrimaryKey[foo]")
)
@@ -1280,7 +1281,7 @@
attributes = mapOf("primaryKeys" to "\"foo\"")
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.primaryKeyColumnDoesNotExist("foo", listOf("id"))
)
}
@@ -1299,7 +1300,7 @@
) { entity, invocation ->
assertThat(entity.primaryKey.fields.isEmpty(), `is`(true))
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.multiplePrimaryKeyAnnotations(
listOf("PrimaryKey[x]", "PrimaryKey[y]")
)
@@ -1390,7 +1391,7 @@
assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
assertThat(entity.primaryKey.autoGenerateId, `is`(false))
invocation.assertCompilationResult {
- hasNote("PrimaryKey[baseId] is overridden by PrimaryKey[id]")
+ hasNoteContaining("PrimaryKey[baseId] is overridden by PrimaryKey[id]")
}
}
}
@@ -1420,7 +1421,7 @@
assertThat(entity.primaryKey.fields.size, `is`(1))
assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
invocation.assertCompilationResult {
- hasNote("PrimaryKey[baseId] is overridden by PrimaryKey[id]")
+ hasNoteContaining("PrimaryKey[baseId] is overridden by PrimaryKey[id]")
}
}
}
@@ -1451,7 +1452,7 @@
assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
assertThat(entity.primaryKey.autoGenerateId, `is`(false))
invocation.assertCompilationResult {
- hasNote("PrimaryKey[baseId] is overridden by PrimaryKey[id]")
+ hasNoteContaining("PrimaryKey[baseId] is overridden by PrimaryKey[id]")
}
}
}
@@ -1501,7 +1502,7 @@
assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
assertThat(entity.primaryKey.autoGenerateId, `is`(true))
invocation.assertCompilationResult {
- hasError(ProcessorErrors.AUTO_INCREMENTED_PRIMARY_KEY_IS_NOT_INT)
+ hasErrorContaining(ProcessorErrors.AUTO_INCREMENTED_PRIMARY_KEY_IS_NOT_INT)
}
}
}
@@ -1602,7 +1603,9 @@
) { entity, invocation ->
assertThat(entity.primaryKey.columnNames, `is`(listOf("bar_a", "bar_b")))
invocation.assertCompilationResult {
- hasNote("PrimaryKey[baseId] is overridden by PrimaryKey[foo > a, foo > b]")
+ hasNoteContaining(
+ "PrimaryKey[baseId] is overridden by PrimaryKey[foo > a, foo > b]"
+ )
}
}
}
@@ -1641,7 +1644,7 @@
) { entity, invocation ->
assertThat(entity.primaryKey.columnNames, `is`(listOf("id")))
invocation.assertCompilationResult {
- hasNote("PrimaryKey[foo > a, foo > b] is overridden by PrimaryKey[id]")
+ hasNoteContaining("PrimaryKey[foo > a, foo > b] is overridden by PrimaryKey[id]")
}
}
}
@@ -1671,7 +1674,7 @@
assertThat(entity.primaryKey.fields.size, `is`(1))
assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
invocation.assertCompilationResult {
- hasError(ProcessorErrors.primaryKeyNull("id"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("id"))
}
}
}
@@ -1687,8 +1690,8 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.primaryKeyNull("id"))
- hasError(ProcessorErrors.primaryKeyNull("anotherId"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("id"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("anotherId"))
}
}
}
@@ -1705,7 +1708,7 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.primaryKeyNull("anotherId")
)
}
@@ -1722,7 +1725,7 @@
attributes = mapOf("primaryKeys" to "{\"id\", \"foo\"}")
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.primaryKeyNull("foo"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
}
}
}
@@ -1758,7 +1761,7 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.primaryKeyNull("foo"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
}
}
}
@@ -1780,9 +1783,12 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.primaryKeyNull("foo > a"))
- hasError(ProcessorErrors.primaryKeyNull("foo > b"))
- hasError(ProcessorErrors.primaryKeyNull("foo"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
+ .onLineContaining("String a")
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
+ .onLineContaining("String b")
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ .onLineContaining("Foo foo")
hasErrorCount(3)
}
}
@@ -1810,10 +1816,10 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.primaryKeyNull("foo > a"))
- hasError(ProcessorErrors.primaryKeyNull("foo > b"))
- hasError(ProcessorErrors.primaryKeyNull("foo"))
- hasError(ProcessorErrors.primaryKeyNull("foo > a > bb"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo > a > bb"))
hasErrorCount(4)
}
}
@@ -1850,9 +1856,9 @@
sources = listOf(parent)
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.primaryKeyNull("foo"))
- hasError(ProcessorErrors.primaryKeyNull("foo > a"))
- hasError(ProcessorErrors.primaryKeyNull("foo > b"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
hasErrorCount(3)
}
}
@@ -1889,10 +1895,12 @@
sources = listOf(parent)
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.primaryKeyNull("foo"))
- hasError(ProcessorErrors.primaryKeyNull("foo > a"))
- hasError(ProcessorErrors.primaryKeyNull("foo > b"))
- hasNote("PrimaryKey[baseId] is overridden by PrimaryKey[foo > a, foo > b]")
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
+ hasNoteContaining(
+ "PrimaryKey[baseId] is overridden by PrimaryKey[foo > a, foo > b]"
+ )
hasErrorCount(3)
}
}
@@ -1930,10 +1938,10 @@
sources = listOf(parent)
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.primaryKeyNull("foo"))
- hasError(ProcessorErrors.primaryKeyNull("foo > a"))
- hasError(ProcessorErrors.primaryKeyNull("foo > b"))
- hasNote("PrimaryKey[foo > a, foo > b] is overridden by PrimaryKey[id]")
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
+ hasNoteContaining("PrimaryKey[foo > a, foo > b] is overridden by PrimaryKey[id]")
hasErrorCount(3)
}
}
@@ -1970,7 +1978,7 @@
sources = listOf(parent)
) { _, invocation ->
invocation.assertCompilationResult {
- hasNote("PrimaryKey[foo > a] is overridden by PrimaryKey[id]")
+ hasNoteContaining("PrimaryKey[foo > a] is overridden by PrimaryKey[id]")
}
}
}
@@ -2006,9 +2014,9 @@
sources = listOf(parent)
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.primaryKeyNull("foo"))
- hasError(ProcessorErrors.primaryKeyNull("foo > a"))
- hasNote("PrimaryKey[foo > a] is overridden by PrimaryKey[id]")
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ hasErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
+ hasNoteContaining("PrimaryKey[foo > a] is overridden by PrimaryKey[id]")
hasErrorCount(2)
}
}
@@ -2026,7 +2034,7 @@
sources = listOf(COMMON.USER)
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(RELATION_IN_ENTITY)
+ hasErrorContaining(RELATION_IN_ENTITY)
}
}
}
@@ -2051,7 +2059,7 @@
attributes = annotation, sources = listOf(COMMON.USER)
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.INVALID_FOREIGN_KEY_ACTION)
+ hasErrorContaining(ProcessorErrors.INVALID_FOREIGN_KEY_ACTION)
}
}
}
@@ -2075,8 +2083,17 @@
attributes = annotation, sources = listOf(COMMON.USER)
) { _, invocation ->
invocation.assertCompilationResult {
- compilationDidFail()
- hasRawOutputContaining("cannot find symbol")
+ // KSP runs processors even when java code is not valid hence we'll get the
+ // error from room. For JavaP and Kapt, they don't run the processor when
+ // the java code has an error
+ if (invocation.isKsp) {
+ hasErrorContaining(
+ ProcessorErrors.foreignKeyNotAnEntity("<Error>")
+ ).onLine(11)
+ } else {
+ hasErrorContaining("cannot find symbol")
+ .onLine(7)
+ }
}
}
}
@@ -2100,7 +2117,7 @@
attributes = annotation, sources = listOf(COMMON.NOT_AN_ENTITY)
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.foreignKeyNotAnEntity(
COMMON.NOT_AN_ENTITY_TYPE_NAME.toString()
)
@@ -2128,7 +2145,7 @@
attributes = annotation, sources = listOf(COMMON.USER)
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.foreignKeyChildColumnDoesNotExist(
"namex", listOf("id", "name")
)
@@ -2156,7 +2173,7 @@
attributes = annotation, sources = listOf(COMMON.USER)
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.foreignKeyColumnNumberMismatch(
listOf("name", "id"), listOf("lastName")
)
@@ -2184,7 +2201,7 @@
attributes = annotation, sources = listOf(COMMON.USER)
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.FOREIGN_KEY_EMPTY_CHILD_COLUMN_LIST)
+ hasErrorContaining(ProcessorErrors.FOREIGN_KEY_EMPTY_CHILD_COLUMN_LIST)
}
}
}
@@ -2208,7 +2225,7 @@
attributes = annotation, sources = listOf(COMMON.USER)
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.FOREIGN_KEY_EMPTY_PARENT_COLUMN_LIST)
+ hasErrorContaining(ProcessorErrors.FOREIGN_KEY_EMPTY_PARENT_COLUMN_LIST)
}
}
}
@@ -2356,7 +2373,7 @@
) { entity, invocation ->
assertThat(entity.indices, `is`(emptyList()))
invocation.assertCompilationResult {
- hasWarning(ProcessorErrors.foreignKeyMissingIndexInChildColumn("name"))
+ hasWarningContaining(ProcessorErrors.foreignKeyMissingIndexInChildColumn("name"))
}
}
}
@@ -2382,7 +2399,7 @@
) { entity, invocation ->
assertThat(entity.indices, `is`(emptyList()))
invocation.assertCompilationResult {
- hasWarning(
+ hasWarningContaining(
ProcessorErrors.foreignKeyMissingIndexInChildColumns(
listOf(
"lName",
@@ -2431,7 +2448,7 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
"foo.bar.MyEntity -> foo.bar.MyEntity"
)
@@ -2456,7 +2473,7 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
"foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.MyEntity"
)
@@ -2479,7 +2496,7 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
"foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.MyEntity"
)
@@ -2503,7 +2520,7 @@
"""
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(
+ hasErrorContaining(
ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
"foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.MyEntity"
)
@@ -2539,7 +2556,7 @@
attributes = annotation, sources = listOf(COMMON.USER)
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.INVALID_TABLE_NAME)
+ hasErrorContaining(ProcessorErrors.INVALID_TABLE_NAME)
}
}
}
@@ -2556,7 +2573,7 @@
sources = listOf(COMMON.USER)
) { _, invocation ->
invocation.assertCompilationResult {
- hasError(ProcessorErrors.INVALID_COLUMN_NAME)
+ hasErrorContaining(ProcessorErrors.INVALID_COLUMN_NAME)
}
}
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index 3793c0c..9b0a364 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -131,7 +131,7 @@
val context = Context(invocation.processingEnv)
CustomConverterProcessor.Companion.findConverters(context, typeElement)
invocation.assertCompilationResult {
- hasError(ProcessorErrors.INNER_CLASS_TYPE_CONVERTER_MUST_BE_STATIC)
+ hasErrorContaining(ProcessorErrors.INNER_CLASS_TYPE_CONVERTER_MUST_BE_STATIC)
}
}
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt b/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
index 8115cea..be1bdc8 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
@@ -87,7 +87,7 @@
}
val MULTI_PKEY_ENTITY by lazy {
- loadJavaCode("common/input/MultiPKeyEntity.java", "MultiPKeyEntity")
+ loadJavaCode("common/input/MultiPKeyEntity.java", "foo.bar.MultiPKeyEntity")
}
val FLOW by lazy {