blob: 62604ac6df1e0bd79d9c9f30e9bd4dae142ac22f [file] [log] [blame]
/*
* Copyright (C) 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.databinding.compilationTest.bazel
import android.databinding.tool.processing.ScopedErrorReport
import android.databinding.tool.store.Location
import androidx.databinding.compilationTest.BaseCompilationTest.DEFAULT_APP_PACKAGE
import androidx.databinding.compilationTest.BaseCompilationTest.KEY_DEPENDENCIES
import androidx.databinding.compilationTest.BaseCompilationTest.KEY_MANIFEST_PACKAGE
import androidx.databinding.compilationTest.BaseCompilationTest.KEY_SETTINGS_INCLUDES
import androidx.databinding.compilationTest.CompilationResult
import androidx.databinding.compilationTest.pattern
import com.android.testutils.TestUtils
import com.android.tools.idea.gradle.project.build.invoker.GradleBuildInvoker
import com.android.tools.idea.testing.AndroidGradleTestCase
import com.android.tools.idea.testing.TestProjectPaths
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListenerAdapter
import com.intellij.openapi.util.io.FileUtil.toSystemDependentName
import com.intellij.util.io.createDirectories
import com.intellij.util.io.readText
import org.junit.Assert
import java.io.File
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.nio.file.Files
private const val TEST_DEPENDENCIES = "implementation 'androidx.fragment:fragment:+'"
private const val DEFAULT_SETTINGS_GRADLE = "include ':app'"
private const val TEST_DATA_PATH = "tools/data-binding/compilationTests/testData"
abstract class DataBindingCompilationTestCase : AndroidGradleTestCase() {
protected fun loadApp() {
loadApp(emptyMap())
}
protected fun loadApp(appReplacements: Map<String, String>) {
loadProject(TestProjectPaths.DATA_BINDING_COMPILATION)
projectRoot.toPath().resolve("app/src/main").createDirectories()
val replacements = appendTestReplacements(appReplacements)
copyTestDataWithReplacement(
"AndroidManifest.xml",
"app/src/main/AndroidManifest.xml",
replacements
)
copyTestDataWithReplacement(
"app_build.gradle",
"app/build.gradle",
replacements
)
copyTestDataWithReplacement(
"settings.gradle",
"settings.gradle",
replacements
)
}
protected fun loadModule(moduleName: String, moduleReplacements: Map<String, String>) {
val replacements = appendTestReplacements(moduleReplacements)
copyTestDataWithReplacement(
"AndroidManifest.xml",
"${moduleName}/src/main/AndroidManifest.xml",
replacements
)
copyTestDataWithReplacement(
"module_build.gradle",
"${moduleName}/build.gradle",
replacements
)
}
protected fun assembleDebug() = invokeTasks(listOf("assembleDebug"))
protected fun invokeTasks(tasks: List<String>, args: List<String> = emptyList()): CompilationResult {
val request =
GradleBuildInvoker.Request(
project,
File(toSystemDependentName(project.basePath!!)),
tasks
)
val outBuilder = StringBuilder()
val errBuilder = StringBuilder()
request.taskListener = object : ExternalSystemTaskNotificationListenerAdapter() {
override fun onTaskOutput(id: ExternalSystemTaskId, text: String, stdOut: Boolean) {
if (stdOut) {
outBuilder.append(text)
} else {
errBuilder.append(text)
}
}
}
request.setCommandLineArguments(listOf("--offline") + args)
val result = invokeGradle(project) { gradleInvoker ->
gradleInvoker.executeTasks(request)
}
return CompilationResult(
if (result.isBuildSuccessful) 0 else 1,
outBuilder.toString(),
errBuilder.toString()
)
}
protected val projectRoot: File
get() = File(toSystemDependentName(project.basePath!!))
/**
* Copies the file in the testData directory to the target directory.
*
* [source] and [target] are relative to testData and projectRoot respectively.
*/
protected fun copyTestData(source: String, target: String) {
val sourcePath = TestUtils.resolveWorkspacePath(TEST_DATA_PATH).resolve(source)
val targetPath = File(projectRoot, target).toPath()
targetPath.parent.createDirectories()
Files.copy(sourcePath, targetPath)
}
protected fun copyTestDataWithReplacement(
source: String,
target: String,
replacements: Map<String, String> = emptyMap()
) {
val sourcePath = TestUtils.resolveWorkspacePath(TEST_DATA_PATH).resolve(source)
val targetPath = File(projectRoot, target)
val contents = sourcePath.readText()
val out = StringBuilder(contents.length)
val matcher = pattern.matcher(contents)
var location = 0
while (matcher.find()) {
val start = matcher.start()
if (start > location) {
out.append(contents, location, start)
}
val key = matcher.group(1)
val replacement = replacements[key]
if (replacement != null) {
out.append(replacement)
}
location = matcher.end()
}
if (location < contents.length) {
out.append(contents, location, contents.length)
}
targetPath.parentFile.mkdirs()
targetPath.writeText(out.toString(), StandardCharsets.UTF_8)
}
/**
* Custom logic that replaces or appends to the replacement values
* depending on the key.
*
* If key is [KEY_DEPENDENCIES], then append the replacement.
* If key is [KEY_MANIFEST_PACKAGE] or [KEY_SETTINGS_INCLUDES],
* then put if value doesn't already exist.
*/
private fun appendTestReplacements(
map: Map<String, String>
): Map<String, String> {
val mutableMap = map.toMutableMap()
if (mutableMap.containsKey(KEY_DEPENDENCIES)) {
mutableMap[KEY_DEPENDENCIES] += "\n${TEST_DEPENDENCIES}"
} else {
mutableMap[KEY_DEPENDENCIES] = TEST_DEPENDENCIES
}
if (!mutableMap.containsKey(KEY_MANIFEST_PACKAGE)) {
mutableMap[KEY_MANIFEST_PACKAGE] = DEFAULT_APP_PACKAGE
}
if (!mutableMap.containsKey(KEY_SETTINGS_INCLUDES)) {
mutableMap[KEY_SETTINGS_INCLUDES] = DEFAULT_SETTINGS_GRADLE
}
return mutableMap
}
/**
* Finds the error file referenced in the given error report.
* Handles possibly relative paths.
*
* Throws an assertion exception if the error file reported cannot be found.
*/
protected fun requireErrorFile(report: ScopedErrorReport): File {
val path = report.filePath
Assert.assertNotNull(path)
var file = File(path)
if (file.exists()) {
return file
}
// might be relative, try in test project folder
file = File(projectRoot, path)
Assert.assertTrue("required error file is missing in " + file.absolutePath, file.exists())
return file
}
/**
* Extracts the text in the given location from the file at the given application path.
*
* @param relativePath the relative path of the file to be extracted from
* @param location The location to extract
* @return The string that is contained in the given location
* @throws IOException If file is invalid.
*/
protected fun extract(relativePath: String, location: Location): String {
val file = File(projectRoot, relativePath)
Assert.assertTrue(file.exists())
val result = StringBuilder()
val lines = file.readLines(StandardCharsets.UTF_8)
for (i in location.startLine..location.endLine) {
if (i > location.startLine) {
result.append("\n")
}
val line = lines[i]
var start = 0
if (i == location.startLine) {
start = location.startOffset
}
var end = line.length - 1 // inclusive
if (i == location.endLine) {
end = location.endOffset
}
result.append(line.substring(start, end + 1))
}
return result.toString()
}
protected fun writeFile(path: String, contents: String) {
val targetFile = File(projectRoot, path)
targetFile.parentFile.mkdirs()
targetFile.writeText(contents, StandardCharsets.UTF_8)
}
}