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 {