[go: nahoru, domu]

blob: 9eb884898fdf916947c49b6b563412f4eb042e09 [file] [log] [blame]
/*
* Copyright 2019 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.gradle
import com.google.common.truth.Expect
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.File
import java.nio.file.Files
import java.util.Properties
@RunWith(Parameterized::class)
class RoomIncrementalAnnotationProcessingTest(private val withIncrementalRoom: Boolean) {
companion object {
@Parameterized.Parameters(name = "incrementalRoom={0}")
@JvmStatic
fun parameters() = listOf(true, false)
private const val SRC_DIR = "src/main/java"
private const val GEN_SRC_DIR = "build/generated/ap_generated_sources/debug/out/"
private const val GEN_RES_DIR = "build/generated/resources"
private const val CLASS_DIR = "build/intermediates/javac/debug/classes"
private const val CLEAN_TASK = ":clean"
private const val COMPILE_TASK = ":compileDebugJavaWithJavac"
}
@get:Rule
val testProjectDir = TemporaryFolder()
@get:Rule
val expect: Expect = Expect.create()
// Properties to set up test project
private lateinit var prebuiltsRepo: String
private lateinit var agpVersion: String
private lateinit var localSupportRepo: String
private lateinit var compileSdkVersion: String
private lateinit var buildToolsVersion: String
private lateinit var minSdkVersion: String
private lateinit var debugKeystore: String
// Original source files
private lateinit var srcDatabase1: File
private lateinit var srcDao1: File
private lateinit var srcEntity1: File
// Generated source files
private lateinit var genDatabase1: File
private lateinit var genDao1: File
private lateinit var genDatabase2: File
private lateinit var genDao2: File
// Generated resource files
private lateinit var genSchema1: File
private lateinit var genSchema2: File
// Compiled classes
private lateinit var classSrcDatabase1: File
private lateinit var classSrcDao1: File
private lateinit var classSrcEntity1: File
private lateinit var classSrcDatabase2: File
private lateinit var classSrcDao2: File
private lateinit var classSrcEntity2: File
private lateinit var classGenDatabase1: File
private lateinit var classGenDao1: File
private lateinit var classGenDatabase2: File
private lateinit var classGenDao2: File
// Timestamps of files
private lateinit var fileToTimestampMap: Map<File, Long>
// Sets of files that have changed/not changed/deleted
private lateinit var changedFiles: Set<File>
private lateinit var unchangedFiles: Set<File>
private lateinit var deletedFiles: Set<File>
@Before
fun setup() {
val projectRoot = testProjectDir.root
// copy local.properties
File("../../../local.properties")
.copyTo(File(projectRoot, "local.properties"), overwrite = true)
// copy sdk.prop (created by module's build.gradle)
RoomIncrementalAnnotationProcessingTest::class.java.classLoader
.getResourceAsStream("sdk.prop")
.use { input ->
val properties = Properties().apply { load(input) }
prebuiltsRepo = properties.getProperty("prebuiltsRepo")
localSupportRepo = properties.getProperty("localSupportRepo")
agpVersion = properties.getProperty("agpVersion")
compileSdkVersion = properties.getProperty("compileSdkVersion")
buildToolsVersion = properties.getProperty("buildToolsVersion")
minSdkVersion = properties.getProperty("minSdkVersion")
debugKeystore = properties.getProperty("debugKeystore")
}
// copy test project
File("src/test/data/simple-project").copyRecursively(projectRoot)
// setup gradle.properties
File(projectRoot, "gradle.properties").writeText(
"android.useAndroidX=true"
)
// set up build file
File(projectRoot, "build.gradle").writeText(
"""
buildscript {
repositories {
maven { url "$prebuiltsRepo/androidx/external" }
maven { url "$prebuiltsRepo/androidx/internal" }
}
dependencies {
classpath 'com.android.tools.build:gradle:$agpVersion'
}
}
apply plugin: 'com.android.application'
repositories {
maven { url "$prebuiltsRepo/androidx/external" }
maven { url "$localSupportRepo" }
maven {
url "$prebuiltsRepo/androidx/internal"
content {
excludeModule("androidx.room", "room-compiler")
}
}
}
android {
compileSdkVersion $compileSdkVersion
buildToolsVersion "$buildToolsVersion"
defaultConfig {
minSdkVersion $minSdkVersion
}
signingConfigs {
debug {
storeFile file("$debugKeystore")
}
}
}
dependencies {
// Uses latest Room built from tip of tree
implementation "androidx.room:room-runtime:+"
annotationProcessor "androidx.room:room-compiler:+"
}
class SchemaLocationArgumentProvider implements CommandLineArgumentProvider {
@OutputDirectory
File schemaDir
SchemaLocationArgumentProvider(File schemaDir) {
this.schemaDir = schemaDir
}
@Override
Iterable<String> asArguments() {
["-Aroom.schemaLocation=" + schemaDir.path ]
}
}
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
argument 'room.incremental', '$withIncrementalRoom'
compilerArgumentProvider new SchemaLocationArgumentProvider(file('$GEN_RES_DIR'))
}
}
}
}
""".trimIndent()
)
// Compute file paths
srcDatabase1 = File(projectRoot, "$SRC_DIR/room/testapp/Database1.java")
srcDao1 = File(projectRoot, "$SRC_DIR/room/testapp/Dao1.java")
srcEntity1 = File(projectRoot, "$SRC_DIR/room/testapp/Entity1.java")
genDatabase1 = File(projectRoot, "$GEN_SRC_DIR/room/testapp/Database1_Impl.java")
genDao1 = File(projectRoot, "$GEN_SRC_DIR/room/testapp/Dao1_Impl.java")
genDatabase2 = File(projectRoot, "$GEN_SRC_DIR/room/testapp/Database2_Impl.java")
genDao2 = File(projectRoot, "$GEN_SRC_DIR/room/testapp/Dao2_Impl.java")
genSchema1 = File(projectRoot, "$GEN_RES_DIR/room.testapp.Database1/1.json")
genSchema2 = File(projectRoot, "$GEN_RES_DIR/room.testapp.Database2/1.json")
classSrcDatabase1 = File(projectRoot, "$CLASS_DIR/room/testapp/Database1.class")
classSrcDao1 = File(projectRoot, "$CLASS_DIR/room/testapp/Dao1.class")
classSrcEntity1 = File(projectRoot, "$CLASS_DIR/room/testapp/Entity1.class")
classSrcDatabase2 = File(projectRoot, "$CLASS_DIR/room/testapp/Database2.class")
classSrcDao2 = File(projectRoot, "$CLASS_DIR/room/testapp/Dao2.class")
classSrcEntity2 = File(projectRoot, "$CLASS_DIR/room/testapp/Entity2.class")
classGenDatabase1 = File(projectRoot, "$CLASS_DIR/room/testapp/Database1_Impl.class")
classGenDao1 = File(projectRoot, "$CLASS_DIR/room/testapp/Dao1_Impl.class")
classGenDatabase2 = File(projectRoot, "$CLASS_DIR/room/testapp/Database2_Impl.class")
classGenDao2 = File(projectRoot, "$CLASS_DIR/room/testapp/Dao2_Impl.class")
}
private fun runGradleTasks(vararg args: String): BuildResult {
return GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments(*args)
.build()
}
private fun runFullBuild(): BuildResult {
val result = runGradleTasks(CLEAN_TASK, COMPILE_TASK)
recordTimestamps()
return result
}
private fun runIncrementalBuild(): BuildResult {
val result = runGradleTasks(COMPILE_TASK)
recordFileChanges()
return result
}
private fun recordTimestamps() {
val files = listOf(
genDatabase1,
genDao1,
genDatabase2,
genDao2,
genSchema1,
genSchema2,
classSrcDatabase1,
classSrcDao1,
classSrcEntity1,
classSrcDatabase2,
classSrcDao2,
classSrcEntity2,
classGenDatabase1,
classGenDao1,
classGenDatabase2,
classGenDao2
)
val map = mutableMapOf<File, Long>()
for (file in files) {
map[file] = file.lastModified()
}
fileToTimestampMap = map.toMap()
}
private fun recordFileChanges() {
changedFiles = fileToTimestampMap.filter { (file, previousTimestamp) ->
file.exists() && file.lastModified() != previousTimestamp
}.keys
unchangedFiles = fileToTimestampMap.filter { (file, previousTimestamp) ->
file.exists() && file.lastModified() == previousTimestamp
}.keys
deletedFiles = fileToTimestampMap.filter { (file, _) -> !file.exists() }.keys
}
private fun assertFilesExist(vararg files: File) {
expect.withMessage("Existing files").that(files.filter { it.exists() })
.containsExactlyElementsIn(files)
}
private fun assertChangedFiles(vararg files: File) {
expect.withMessage("Changed files").that(changedFiles).containsAtLeastElementsIn(files)
}
private fun assertUnchangedFiles(vararg files: File) {
expect.withMessage("Unchanged files").that(unchangedFiles).containsAtLeastElementsIn(files)
}
private fun assertDeletedFiles(vararg files: File) {
expect.withMessage("Deleted files").that(deletedFiles).containsAtLeastElementsIn(files)
}
private fun searchAndReplace(file: File, search: String, replace: String) {
file.writeText(file.readText().replace(search, replace))
}
@Test
fun `verify first full build`() {
// This test verifies the results of the first full (non-incremental) build. The other tests
// verify the results of the second incremental build based on different change scenarios.
val result = runFullBuild()
expect.that(result.task(COMPILE_TASK)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
// Check annotation processing outputs
assertFilesExist(
genDatabase1,
genDao1,
genDatabase2,
genDao2,
genSchema1,
genSchema2
)
// Check compilation outputs
assertFilesExist(
classSrcDatabase1,
classSrcDao1,
classSrcEntity1,
classSrcDatabase2,
classSrcDao2,
classSrcEntity2,
classGenDatabase1,
classGenDao1,
classGenDatabase2,
classGenDao2
)
}
@Test
fun `change source file`() {
runFullBuild()
// Change a source file
searchAndReplace(
srcEntity1,
"// Insert a change here",
"""
@androidx.room.ColumnInfo(name = "name")
private String mName = "";
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
""".trimIndent()
)
val result = runIncrementalBuild()
expect.that(result.task(COMPILE_TASK)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
// Check annotation processing outputs:
// - Relevant files should be re-generated
// - Irrelevant files should not be re-generated (if Room is incremental)
if (withIncrementalRoom) {
assertChangedFiles(
genDatabase1,
genDao1,
genSchema1
)
assertUnchangedFiles(
genDatabase2,
genDao2,
genSchema2
)
} else {
assertChangedFiles(
genDatabase1,
genDao1,
genSchema1,
genDatabase2,
genDao2
)
// Room is able to avoid re-generating schema file 2 as its contents have not changed
assertUnchangedFiles(genSchema2)
}
// Check compilation outputs:
// - Relevant files should be recompiled
// - Irrelevant files should not be recompiled (if Room is incremental)
if (withIncrementalRoom) {
assertChangedFiles(
classSrcDatabase1,
classSrcEntity1,
classGenDatabase1,
classGenDao1
)
assertUnchangedFiles(
classSrcDao1, // Gradle detects that this file is not relevant to the change
classSrcDatabase2,
classSrcDao2,
classSrcEntity2,
classGenDatabase2,
classGenDao2
)
} else {
assertChangedFiles(
classSrcDatabase1,
classSrcDao1,
classSrcEntity1,
classGenDatabase1,
classGenDao1,
classSrcDatabase2,
classSrcDao2,
classSrcEntity2,
classGenDatabase2,
classGenDao2
)
}
}
@Test
fun `delete group of source files`() {
runFullBuild()
// Delete the first group of source files
Files.delete(srcDatabase1.toPath())
Files.delete(srcDao1.toPath())
Files.delete(srcEntity1.toPath())
val result = runIncrementalBuild()
if (withIncrementalRoom) {
// Gradle detects that nothing needs to be recompiled so it reports as UP-TO-DATE
expect.that(result.task(COMPILE_TASK)!!.outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
} else {
expect.that(result.task(COMPILE_TASK)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
}
// Check annotation processing outputs:
// - Relevant files should be re-generated (or deleted)
// - Irrelevant files should not be re-generated (if Room is incremental)
if (withIncrementalRoom) {
assertDeletedFiles(
genDatabase1,
genDao1
)
assertUnchangedFiles(
// EXPECTATION-NOT-MET: Schema file 1 should be deleted but is not
// (https://issuetracker.google.com/134472065).
genSchema1,
genDatabase2,
genDao2,
genSchema2
)
} else {
assertDeletedFiles(
genDatabase1,
genDao1
)
// EXPECTATION-NOT-MET: Schema file 1 should be deleted but is not
// (https://github.com/gradle/gradle/issues/9401).
assertUnchangedFiles(genSchema1)
assertChangedFiles(
genDatabase2,
genDao2
)
// Room is able to avoid re-generating schema file 2 as its contents have not changed
assertUnchangedFiles(genSchema2)
}
// Check compilation outputs:
// - Relevant files should be recompiled (or deleted)
// - Irrelevant files should not be recompiled (if Room is incremental)
if (withIncrementalRoom) {
assertDeletedFiles(
classSrcDatabase1,
classSrcDao1,
classSrcEntity1,
classGenDatabase1,
classGenDao1
)
assertUnchangedFiles(
classSrcDatabase2,
classSrcDao2,
classSrcEntity2,
classGenDatabase2,
classGenDao2
)
} else {
assertDeletedFiles(
classSrcDatabase1,
classSrcDao1,
classSrcEntity1,
classGenDatabase1,
classGenDao1
)
assertChangedFiles(
classSrcDatabase2,
classSrcDao2,
classSrcEntity2,
classGenDatabase2,
classGenDao2
)
}
}
}