Update Room Gradle plugin APIs to not always require per-variant configurations.

Most Room users don't have per-variant schemas which means having per-variant schema directories can decrease the user experience, specially as users migrate from the current annotation processor option approach to the plugin as per-variant directories where also not needed then if not necessary. This change updates the plugin APIs to accept a global location for all variants without creating multiple directories, enabling smoother migrations but also flexible enough to configure flavors or build type schemas while still retaining the benefits of the plugin (reproducible and cacheable builds).

The API is updated to contain overloads for configuring a single schema location or per-variant locations:
```
room {
  // For all variants
  schemaLocation("$projectDir/schemas/")

  // For a specific variant
  schemaLocation("variantName", ("$projectDir/schemas/variantName")
}
```
and an example usage assuming two build flavors: ‘demo’ and ‘full’ and the two default build types ‘debug’ and ‘release’:
```
room {
  // Applies to demoDebug only
  schemaLocation("demoDebug", ("$projectDir/schemas/demoDebug")

  // Applies to demoDebug and demoRelease
  schemaLocation("demo", ("$projectDir/schemas/demo")

  // Applies to demoDebug and fullDebug
  schemaLocation("debug", ("$projectDir/schemas/debug")
}
```
Bug: 278266663
Test: RoomGradlePluginTest

Change-Id: I09d6f9a77d3737bef2ce73193344a8b31f5059a8
diff --git a/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomExtension.kt b/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomExtension.kt
index dc10b3c..bf9bd93 100644
--- a/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomExtension.kt
+++ b/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomExtension.kt
@@ -19,19 +19,22 @@
 import javax.inject.Inject
 import org.gradle.api.provider.Provider
 import org.gradle.api.provider.ProviderFactory
+import org.gradle.api.tasks.TaskProvider
 
 open class RoomExtension @Inject constructor(private val providers: ProviderFactory) {
-    internal var schemaDirectory: Provider<String>? = null
-
-    // TODO(b/279748243): Consider adding overload that takes `org.gradle.api.file.Director`.
+    // TODO(b/279748243): Consider adding overloads that takes `org.gradle.api.file.Directory`.
+    // User provided variant match pattern to schema location
+    internal val schemaDirectories = mutableMapOf<VariantMatchName, Provider<String>>()
+    // Used variant match pattern to its copy task. Multiple variant compile tasks can be finalized
+    // by the same copy task.
+    internal val copyTasks =
+        mutableMapOf<VariantMatchName, TaskProvider<RoomGradlePlugin.RoomSchemaCopyTask>>()
 
     /**
      * Sets the schema location where Room will output exported schema files.
      *
-     * The location specified will be used as the base directory for schema files that will be
-     * generated per build variant. i.e. for a 'debug' build of the product flavor 'free' then a
-     * schema will be generated in
-     * `<schemaDirectory>/freeDebug/<database-package>/<database-version>.json`.
+     * The location specified will be used for all variants if per-variant schema locations are
+     * needed use the overloaded version of this function that takes in a `variantMatchName`.
      *
      * See [Export Schemas Documentation](https://developer.android.com/training/data-storage/room/migrating-db-versions#export-schemas)
      */
@@ -42,14 +45,89 @@
     /**
      * Sets the schema location where Room will output exported schema files.
      *
-     * The location specified will be used as the base directory for schema files that will be
-     * generated per build variant. i.e. for a 'debug' build of the product flavor 'free' then a
-     * schema will be generated in
-     * `<schemaDirectory>/freeDebug/<database-package>/<database-version>.json`.
+     * The location specified will be used for all variants if per-variant schema locations are
+     * needed use the overloaded version of this function that takes in a `variantMatchName`.
      *
      * See [Export Schemas Documentation](https://developer.android.com/training/data-storage/room/migrating-db-versions#export-schemas)
      */
     open fun schemaDirectory(path: Provider<String>) {
-        schemaDirectory = path
+        schemaDirectories[ALL_VARIANTS] = path
+    }
+
+    /**
+     * Sets the schema location for a variant, flavor or build type where Room will output exported
+     * schema files.
+     *
+     * The location specified will be used for a matching variants based on the provided
+     * [variantMatchName] where it can either be a full variant name, a product flavor name or a
+     * build type name.
+     *
+     * For example, assuming two build flavors: ‘demo’ and ‘full’ and the two default build types
+     * ‘debug’ and ‘release’, then the following are valid configurations:
+     * ```
+     * room {
+     *   // Applies 'demoDebug' only
+     *   schemaLocation("demoDebug", ("$projectDir/schemas/demoDebug")
+     *
+     *   // Applies to 'demoDebug' and 'demoRelease'
+     *   schemaLocation("demo", ("$projectDir/schemas/demo")
+     *
+     *   // Applies to 'demoDebug' and 'fullDebug'
+     *   schemaLocation("debug", ("$projectDir/schemas/debug")
+     * }
+     * ```
+     *
+     * If per-variant schema locations are not necessary due to all variants containing the same
+     * schema, then use the overloaded version of this function that does not take in a
+     * `variantMatchName`.
+     *
+     * See [Export Schemas Documentation](https://developer.android.com/training/data-storage/room/migrating-db-versions#export-schemas)
+     */
+    open fun schemaDirectory(variantMatchName: String, path: String) {
+        schemaDirectory(variantMatchName, providers.provider { path })
+    }
+
+    /**
+     * Sets the schema location for a variant, flavor or build type where Room will output exported
+     * schema files.
+     *
+     * The location specified will be used for a matching variants based on the provided
+     * [variantMatchName] where it can either be a full variant name, a product flavor name or a
+     * build type name.
+     *
+     * For example, assuming two build flavors: ‘demo’ and ‘full’ and the two default build types
+     * ‘debug’ and ‘release’, then the following are valid configurations:
+     * ```
+     * room {
+     *   // Applies 'demoDebug' only
+     *   schemaLocation("demoDebug", ("$projectDir/schemas/demoDebug")
+     *
+     *   // Applies to 'demoDebug' and 'demoRelease'
+     *   schemaLocation("demo", ("$projectDir/schemas/demo")
+     *
+     *   // Applies to 'demoDebug' and 'fullDebug'
+     *   schemaLocation("debug", ("$projectDir/schemas/debug")
+     * }
+     * ```
+     *
+     * If per-variant schema locations are not necessary due to all variants containing the same
+     * schema, then use the overloaded version of this function that does not take in a
+     * `variantMatchName`.
+     *
+     * See [Export Schemas Documentation](https://developer.android.com/training/data-storage/room/migrating-db-versions#export-schemas)
+     */
+    open fun schemaDirectory(variantMatchName: String, path: Provider<String>) {
+        check(variantMatchName.isNotEmpty()) { "variantMatchName must not be empty." }
+        schemaDirectories[VariantMatchName(variantMatchName)] = path
+    }
+
+    /**
+     * Represent a full variant name (demoDebug), flavor name (demo) or build type name (debug).
+     */
+    @JvmInline
+    internal value class VariantMatchName(val actual: String)
+
+    companion object {
+        internal val ALL_VARIANTS = VariantMatchName("")
     }
 }
\ No newline at end of file
diff --git a/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomGradlePlugin.kt b/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomGradlePlugin.kt
index 9dacb6d..98794bc 100644
--- a/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomGradlePlugin.kt
+++ b/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomGradlePlugin.kt
@@ -16,17 +16,22 @@
 
 package androidx.room.gradle
 
+import androidx.room.gradle.RoomExtension.VariantMatchName
 import com.android.build.api.AndroidPluginVersion
 import com.android.build.api.variant.AndroidComponentsExtension
 import com.android.build.api.variant.ComponentIdentity
-import com.android.build.api.variant.Variant
+import com.android.build.api.variant.HasAndroidTest
 import com.android.build.gradle.api.AndroidBasePlugin
 import com.google.devtools.ksp.gradle.KspTaskJvm
+import java.io.File
+import java.security.MessageDigest
 import java.util.Locale
 import javax.inject.Inject
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.contract
 import kotlin.io.path.Path
+import kotlin.io.path.copyTo
+import kotlin.io.path.createDirectories
 import kotlin.io.path.notExists
 import org.gradle.api.DefaultTask
 import org.gradle.api.GradleException
@@ -48,9 +53,7 @@
 import org.gradle.api.tasks.PathSensitivity
 import org.gradle.api.tasks.SkipWhenEmpty
 import org.gradle.api.tasks.TaskAction
-import org.gradle.api.tasks.TaskProvider
 import org.gradle.api.tasks.compile.JavaCompile
-import org.gradle.configurationcache.extensions.capitalized
 import org.gradle.process.CommandLineArgumentProvider
 import org.gradle.work.DisableCachingByDefault
 import org.jetbrains.kotlin.gradle.internal.KaptTask
@@ -78,7 +81,7 @@
             project.extensions.create("room", RoomExtension::class.java)
         val componentsExtension =
             project.extensions.findByType(AndroidComponentsExtension::class.java)
-        project.check(componentsExtension != null) {
+        project.check(componentsExtension != null, isFatal = true) {
             "Could not find the Android Gradle Plugin (AGP) extension, the Room Gradle plugin " +
                 "should be only applied to an Android projects."
         }
@@ -87,43 +90,61 @@
                 "version 7.3.0 or higher (found ${componentsExtension.pluginVersion})."
         }
         componentsExtension.onVariants { variant ->
-            val locationProvider = roomExtension.schemaDirectory
-            project.check(locationProvider != null) {
-                "The Room Gradle plugin was applied but not schema location was specified. " +
+            project.check(roomExtension.schemaDirectories.isNotEmpty(), isFatal = true) {
+                "The Room Gradle plugin was applied but no schema location was specified. " +
                     "Use the `room { schemaDirectory(...) }` DSL to specify one."
             }
-            val schemaDirectory = locationProvider.get()
-            project.check(schemaDirectory.isNotEmpty()) {
-                "The schemaDirectory path must not be empty."
+            configureVariant(project, roomExtension, variant)
+            variant.unitTest?.let { configureVariant(project, roomExtension, it) }
+            if (variant is HasAndroidTest) {
+                variant.androidTest?.let { configureVariant(project, roomExtension, it) }
             }
-            configureVariant(project, schemaDirectory, variant)
         }
     }
 
     private fun configureVariant(
         project: Project,
-        schemaDirectory: String,
-        variant: Variant
+        roomExtension: RoomExtension,
+        variant: ComponentIdentity
     ) {
-        val androidVariantTaskNames = AndroidVariantsTaskNames(variant.name, variant)
         val configureTask: (Task, ComponentIdentity) -> RoomSchemaDirectoryArgumentProvider = {
                 task, variantIdentity ->
-            val schemaDirectoryPath = Path(schemaDirectory, variantIdentity.name)
+            // Find schema location for variant from user declared location with priority:
+            // * Full variant name specified, e.g. `schemaLocation("demoDebug", "...")`
+            // * Flavor name, e.g. `schemaLocation("demo", "...")`
+            // * Build type name, e.g. `schemaLocation("debug", "...")`
+            // * All variants location, e.g. `schemaLocation("...")`
+            val schemaDirectories = roomExtension.schemaDirectories
+            fun <V> Map<VariantMatchName, V>.findPair(key: String) =
+                VariantMatchName(key).let { if (containsKey(it)) it to getValue(it) else null }
+            val matchedPair = schemaDirectories.findPair(variantIdentity.name)
+                    ?: variantIdentity.flavorName?.let { schemaDirectories.findPair(it) }
+                    ?: variantIdentity.buildType?.let { schemaDirectories.findPair(it) }
+                    ?: schemaDirectories.findPair(RoomExtension.ALL_VARIANTS.actual)
+            project.check(matchedPair != null, isFatal = true) {
+               "No matching schema directory for variant '${variantIdentity.name}'."
+            }
+            val (matchedName, schemaDirectoryProvider) = matchedPair
+            val schemaDirectory = schemaDirectoryProvider.get()
+            project.check(schemaDirectory.isNotEmpty()) {
+                "The schema directory path for variant '${variantIdentity.name}' must not be empty."
+            }
+            val schemaDirectoryPath = Path(schemaDirectory)
             if (schemaDirectoryPath.notExists()) {
                 project.check(schemaDirectoryPath.toFile().mkdirs()) {
                     "Unable to create directory: $schemaDirectoryPath"
                 }
             }
+
             val schemaInputDir = objectFactory.directoryProperty().apply {
                 set(project.file(schemaDirectoryPath))
             }
-
             val schemaOutputDir =
                 projectLayout.buildDirectory.dir("intermediates/room/schemas/${task.name}")
 
-            val copyTask = androidVariantTaskNames.copyTasks.getOrPut(variant.name) {
+            val copyTask = roomExtension.copyTasks.getOrPut(matchedName) {
                 project.tasks.register(
-                    "copyRoomSchemas${variantIdentity.name.capitalize()}",
+                    "copyRoomSchemas${matchedName.actual.capitalize()}",
                     RoomSchemaCopyTask::class.java
                 ) {
                     it.schemaDirectory.set(schemaInputDir)
@@ -139,6 +160,7 @@
             )
         }
 
+        val androidVariantTaskNames = AndroidVariantsTaskNames(variant.name, variant)
         configureJavaTasks(project, androidVariantTaskNames, configureTask)
         configureKaptTasks(project, androidVariantTaskNames, configureTask)
         configureKspTasks(project, androidVariantTaskNames, configureTask)
@@ -198,19 +220,16 @@
         private val variantName: String,
         private val variantIdentity: ComponentIdentity
     ) {
-        // Variant name to copy task
-        val copyTasks = mutableMapOf<String, TaskProvider<RoomSchemaCopyTask>>()
-
         private val javaCompileName by lazy {
-            "compile${variantName.capitalized()}JavaWithJavac"
+            "compile${variantName.capitalize()}JavaWithJavac"
         }
 
         private val kaptTaskName by lazy {
-            "kapt${variantName.capitalized()}Kotlin"
+            "kapt${variantName.capitalize()}Kotlin"
         }
 
         private val kspTaskJvm by lazy {
-            "ksp${variantName.capitalized()}Kotlin"
+            "ksp${variantName.capitalize()}Kotlin"
         }
 
         fun withJavaCompile(taskName: String) =
@@ -236,14 +255,48 @@
 
         @TaskAction
         fun copySchemas() {
+            // Map of relative path to its source file hash.
+            val copiedHashes = mutableMapOf<String, MutableMap<String, String>>()
             variantSchemaOutputDirectories.files
                 .filter { it.exists() }
-                .forEach {
-                    // TODO(b/278266663): Error when two same relative path schemas are found in out
-                    //  dirs and their content is different an indicator of an inconsistency between
-                    //  the compile tasks of the same variant.
-                    it.copyRecursively(schemaDirectory.get().asFile, overwrite = true)
+                .forEach { outputDir ->
+                    outputDir.walkTopDown().filter { it.isFile }.forEach { schemaFile ->
+                        val schemaPath = schemaFile.toPath()
+                        val basePath = outputDir.toPath().relativize(schemaPath)
+                        schemaPath.copyTo(
+                            target = schemaDirectory.get().asFile.toPath().resolve(basePath)
+                                .apply { parent?.createDirectories() },
+                            overwrite = true
+                        )
+                        copiedHashes.getOrPut(basePath.toString()) { mutableMapOf() }
+                            .put(schemaFile.sha256(), schemaPath.toString())
+                    }
                 }
+            // Validate that if multiple schema files for the same database and version are copied
+            // to the same schema directory that they are the same in content (via checksum), as
+            // otherwise it would indicate pre-variant schemas and thus requiring pre-variant
+            // schema directories.
+            copiedHashes.filterValues { it.size > 1 }.forEach { (schemaDir, hashes) ->
+                val errorMsg = buildString {
+                    appendLine(
+                        "Inconsistency detected exporting schema files (checksum - source):"
+                    )
+                    hashes.entries.forEach {
+                        appendLine("  ${it.key} - ${it.value}")
+                    }
+                    appendLine(
+                        "The listed files differ in content but were copied into the same " +
+                            "schema directory '$schemaDir'. A possible indicator that " +
+                            "per-variant schema locations must be provided."
+                    )
+                }
+                throw GradleException(errorMsg)
+            }
+        }
+
+        private fun File.sha256(): String {
+            return MessageDigest.getInstance("SHA-256").digest(this.readBytes())
+                .joinToString("") { "%02x".format(it) }
         }
     }
 
@@ -276,11 +329,15 @@
         }
 
         @OptIn(ExperimentalContracts::class)
-        internal fun Project.check(value: Boolean, lazyMessage: () -> String) {
+        internal fun Project.check(
+            value: Boolean,
+            isFatal: Boolean = false,
+            lazyMessage: () -> String
+        ) {
             contract {
                 returns() implies value
             }
-            if (isGradleSyncRunning()) return
+            if (isGradleSyncRunning() && !isFatal) return
             if (!value) {
                 throw GradleException(lazyMessage())
             }
diff --git a/room/room-gradle-plugin/src/test/java/androidx/room/gradle/RoomGradlePluginTest.kt b/room/room-gradle-plugin/src/test/java/androidx/room/gradle/RoomGradlePluginTest.kt
index 42c16d8..90ced10 100644
--- a/room/room-gradle-plugin/src/test/java/androidx/room/gradle/RoomGradlePluginTest.kt
+++ b/room/room-gradle-plugin/src/test/java/androidx/room/gradle/RoomGradlePluginTest.kt
@@ -28,10 +28,9 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@Suppress("JUnitMalformedDeclaration") // Using TestParameterInjector in test functions.
 @RunWith(TestParameterInjector::class)
-class RoomGradlePluginTest(
-    @TestParameter val backend: ProcessingBackend
-) {
+class RoomGradlePluginTest {
     @get:Rule
     val projectSetup = ProjectSetupRule()
 
@@ -39,7 +38,12 @@
         projectSetup.getLibraryLatestVersionInLocalRepo("androidx/room/room-compiler")
     }
 
-    private fun setup(projectName: String, projectRoot: File = projectSetup.rootDir) {
+    private fun setup(
+        projectName: String,
+        backend: ProcessingBackend = ProcessingBackend.JAVAC,
+        projectRoot: File = projectSetup.rootDir,
+        schemaDslLines: List<String> = listOf("schemaDirectory(\"\$projectDir/schemas\")")
+    ) {
         // copy test project
         File("src/test/test-data/$projectName").copyRecursively(projectRoot)
 
@@ -60,14 +64,14 @@
                 ""
             ProcessingBackend.KAPT ->
                 """
-                    id('kotlin-android')
-                    id('kotlin-kapt')
-                """
+                |    id('kotlin-android')
+                |    id('kotlin-kapt')
+                """.trimMargin()
             ProcessingBackend.KSP ->
                 """
-                    id('kotlin-android')
-                    id('com.google.devtools.ksp')
-                """
+                |    id('kotlin-android')
+                |    id('com.google.devtools.ksp')
+                """.trimMargin()
         }
 
         val repositoriesBlock = buildString {
@@ -101,48 +105,44 @@
         // set up build file
         File(projectRoot, "build.gradle").writeText(
             """
-            plugins {
-                id('com.android.application')
-                id('androidx.room')
-                $additionalPluginsBlock
-            }
-
-            $repositoriesBlock
-
-            %s
-
-            dependencies {
-                // Uses latest Room built from tip of tree
-                implementation "androidx.room:room-runtime:$roomVersion"
-                $processorConfig "androidx.room:room-compiler:$roomVersion"
-            }
-
-            android {
-                namespace "room.testapp"
-                compileOptions {
-                  sourceCompatibility = JavaVersion.VERSION_1_8
-                  targetCompatibility = JavaVersion.VERSION_1_8
-                }
-            }
-
-            $kotlinJvmTargetBlock
-
-            room {
-                schemaDirectory("${'$'}projectDir/schemas")
-            }
-
-            """
-                .trimMargin()
-                // doing format instead of "$projectSetup.androidProject" on purpose,
-                // because otherwise trimIndent will mess with formatting
-                .format(projectSetup.androidProject)
+            |plugins {
+            |    id('com.android.application')
+            |    id('androidx.room')
+            |    $additionalPluginsBlock
+            |}
+            |
+            |$repositoriesBlock
+            |
+            |${projectSetup.androidProject}
+            |
+            |dependencies {
+            |    // Uses latest Room built from tip of tree
+            |    implementation "androidx.room:room-runtime:$roomVersion"
+            |    $processorConfig "androidx.room:room-compiler:$roomVersion"
+            |}
+            |
+            |android {
+            |    namespace "room.testapp"
+            |    compileOptions {
+            |      sourceCompatibility = JavaVersion.VERSION_1_8
+            |      targetCompatibility = JavaVersion.VERSION_1_8
+            |    }
+            |}
+            |
+            |$kotlinJvmTargetBlock
+            |
+            |room {
+            |${schemaDslLines.joinToString(separator = "\n")}
+            |}
+            |
+            """.trimMargin()
 
         )
     }
 
     @Test
-    fun testWorkflow() {
-        setup("simple-project")
+    fun testWorkflow(@TestParameter backend: ProcessingBackend) {
+        setup("simple-project", backend = backend)
 
         // First clean build, all tasks need to run
         runGradleTasks(CLEAN_TASK, COMPILE_TASK).let { result ->
@@ -152,7 +152,7 @@
 
         // Schema file at version 1 is created
         var schemaOneTimestamp: Long
-        projectSetup.rootDir.resolve("schemas/debug/room.testapp.MyDatabase/1.json").let {
+        projectSetup.rootDir.resolve("schemas/room.testapp.MyDatabase/1.json").let {
             assertThat(it.exists()).isTrue()
             schemaOneTimestamp = it.lastModified()
         }
@@ -184,7 +184,7 @@
         }
 
         // Check schema file at version 1 is updated
-        projectSetup.rootDir.resolve("schemas/debug/room.testapp.MyDatabase/1.json").let {
+        projectSetup.rootDir.resolve("schemas/room.testapp.MyDatabase/1.json").let {
             assertThat(it.exists()).isTrue()
             assertThat(schemaOneTimestamp).isNotEqualTo(it.lastModified())
             schemaOneTimestamp = it.lastModified()
@@ -239,20 +239,27 @@
         }
 
         // Check schema file at version 1 is still present and unchanged.
-        projectSetup.rootDir.resolve("schemas/debug/room.testapp.MyDatabase/1.json").let {
+        projectSetup.rootDir.resolve("schemas/room.testapp.MyDatabase/1.json").let {
             assertThat(it.exists()).isTrue()
             assertThat(schemaOneTimestamp).isEqualTo(it.lastModified())
         }
 
         // Check schema file at version 2 is created and copied.
-        projectSetup.rootDir.resolve("schemas/debug/room.testapp.MyDatabase/2.json").let {
+        projectSetup.rootDir.resolve("schemas/room.testapp.MyDatabase/2.json").let {
             assertThat(it.exists()).isTrue()
         }
     }
 
     @Test
-    fun testFlavoredProject() {
-        setup("flavored-project")
+    fun testFlavoredProject(@TestParameter backend: ProcessingBackend) {
+        setup(
+            projectName = "flavored-project",
+            backend = backend,
+            schemaDslLines = listOf(
+                "schemaDirectory(\"flavorOne\", \"\$projectDir/schemas/flavorOne\")",
+                "schemaDirectory(\"flavorTwo\", \"\$projectDir/schemas/flavorTwo\")"
+            )
+        )
 
         File(projectSetup.rootDir, "build.gradle").appendText(
             """
@@ -277,15 +284,15 @@
         ).let { result ->
             result.assertTaskOutcome(":compileFlavorOneDebugJavaWithJavac", TaskOutcome.SUCCESS)
             result.assertTaskOutcome(":compileFlavorTwoDebugJavaWithJavac", TaskOutcome.SUCCESS)
-            result.assertTaskOutcome(":copyRoomSchemasFlavorOneDebug", TaskOutcome.SUCCESS)
-            result.assertTaskOutcome(":copyRoomSchemasFlavorTwoDebug", TaskOutcome.SUCCESS)
+            result.assertTaskOutcome(":copyRoomSchemasFlavorOne", TaskOutcome.SUCCESS)
+            result.assertTaskOutcome(":copyRoomSchemasFlavorTwo", TaskOutcome.SUCCESS)
         }
         // Check schema files are generated for both flavor, each in its own folder.
         val flavorOneSchema = projectSetup.rootDir.resolve(
-            "schemas/flavorOneDebug/room.testapp.MyDatabase/1.json"
+            "schemas/flavorOne/room.testapp.MyDatabase/1.json"
         )
         val flavorTwoSchema = projectSetup.rootDir.resolve(
-            "schemas/flavorTwoDebug/room.testapp.MyDatabase/1.json"
+            "schemas/flavorTwo/room.testapp.MyDatabase/1.json"
         )
         assertThat(flavorOneSchema.exists()).isTrue()
         assertThat(flavorTwoSchema.exists()).isTrue()
@@ -294,8 +301,15 @@
     }
 
     @Test
-    fun testMoreBuildTypesProject() {
-        setup("simple-project")
+    fun testMoreBuildTypesProject(@TestParameter backend: ProcessingBackend) {
+        setup(
+            projectName = "simple-project",
+            backend = backend,
+            schemaDslLines = listOf(
+                "schemaDirectory(\"\$projectDir/schemas\")",
+                "schemaDirectory(\"staging\", \"\$projectDir/schemas/staging\")"
+            )
+        )
 
         File(projectSetup.rootDir, "build.gradle").appendText(
             """
@@ -310,7 +324,7 @@
             """.trimIndent()
         )
 
-        runGradleTasks(CLEAN_TASK, "compileStagingJavaWithJavac",).let { result ->
+        runGradleTasks(CLEAN_TASK, "compileStagingJavaWithJavac").let { result ->
             result.assertTaskOutcome(":compileStagingJavaWithJavac", TaskOutcome.SUCCESS)
             result.assertTaskOutcome(":copyRoomSchemasStaging", TaskOutcome.SUCCESS)
         }
@@ -320,16 +334,129 @@
         assertThat(schemeFile.exists()).isTrue()
     }
 
+    @Test
+    fun testMissingConfigProject() {
+        setup(
+            projectName = "simple-project",
+            schemaDslLines = listOf()
+        )
+
+        runGradleTasks(CLEAN_TASK, COMPILE_TASK, expectFailure = true).let { result ->
+            assertThat(result.output).contains(
+                "The Room Gradle plugin was applied but no schema location was specified."
+            )
+        }
+    }
+
+    @Test
+    fun testEmptyDirConfigProject() {
+        setup(
+            projectName = "simple-project",
+            schemaDslLines = listOf("schemaDirectory(\"\")")
+        )
+
+        runGradleTasks(CLEAN_TASK, COMPILE_TASK, expectFailure = true).let { result ->
+            assertThat(result.output).contains(
+                "The schema directory path for variant 'debug' must not be empty."
+            )
+        }
+    }
+
+    @Test
+    fun testMissingConfigFlavoredProject() {
+        setup(
+            projectName = "flavored-project",
+            schemaDslLines = listOf(
+                "schemaDirectory(\"flavorOne\", \"\$projectDir/schemas/flavorOne\")",
+            )
+        )
+
+        File(projectSetup.rootDir, "build.gradle").appendText(
+            """
+            android {
+                flavorDimensions "mode"
+                productFlavors {
+                    flavorOne {
+                        dimension "mode"
+                    }
+                    flavorTwo {
+                        dimension "mode"
+                    }
+                }
+            }
+            """.trimIndent()
+        )
+
+        runGradleTasks(
+            CLEAN_TASK,
+            "compileFlavorOneDebugJavaWithJavac",
+            "compileFlavorTwoDebugJavaWithJavac",
+            expectFailure = true
+        ).let { result ->
+            assertThat(result.output).contains(
+                "No matching schema directory for variant 'flavorTwoDebug'."
+            )
+        }
+    }
+
+    @Test
+    fun testCopyInconsistencyFlavoredProject(@TestParameter backend: ProcessingBackend) {
+        setup(
+            projectName = "flavored-project",
+            backend = backend,
+            schemaDslLines = listOf(
+                "schemaDirectory(\"\$projectDir/schemas\")",
+            )
+        )
+
+        File(projectSetup.rootDir, "build.gradle").appendText(
+            """
+            android {
+                flavorDimensions "mode"
+                productFlavors {
+                    flavorOne {
+                        dimension "mode"
+                    }
+                    flavorTwo {
+                        dimension "mode"
+                    }
+                }
+            }
+            """.trimIndent()
+        )
+
+        runGradleTasks(
+            CLEAN_TASK,
+            "compileFlavorOneDebugJavaWithJavac",
+            "compileFlavorTwoDebugJavaWithJavac",
+            expectFailure = true
+        ).let { result ->
+            result.assertTaskOutcome(":compileFlavorOneDebugJavaWithJavac", TaskOutcome.SUCCESS)
+            result.assertTaskOutcome(":compileFlavorTwoDebugJavaWithJavac", TaskOutcome.SUCCESS)
+            result.assertTaskOutcome(":copyRoomSchemas", TaskOutcome.FAILED)
+
+            assertThat(result.output).contains(
+                "Inconsistency detected exporting schema files"
+            )
+        }
+    }
+
     private fun runGradleTasks(
         vararg args: String,
-        projectDir: File = projectSetup.rootDir
+        projectDir: File = projectSetup.rootDir,
+        expectFailure: Boolean = false
     ): BuildResult {
-        return GradleRunner.create()
+        val runner = GradleRunner.create()
             .withProjectDir(projectDir)
             .withPluginClasspath()
+            .withDebug(true)
             // workaround for b/231154556
             .withArguments("-Dorg.gradle.jvmargs=-Xmx1g -XX:MaxMetaspaceSize=512m", *args)
-            .build()
+        return if (expectFailure) {
+            runner.buildAndFail()
+        } else {
+            runner.build()
+        }
     }
 
     private fun BuildResult.assertTaskOutcome(taskPath: String, outcome: TaskOutcome) {
@@ -351,6 +478,6 @@
     companion object {
         private const val CLEAN_TASK = ":clean"
         private const val COMPILE_TASK = ":compileDebugJavaWithJavac"
-        private const val COPY_TASK = ":copyRoomSchemasDebug"
+        private const val COPY_TASK = ":copyRoomSchemas"
     }
 }
\ No newline at end of file