| /* |
| * Copyright (C) 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 com.android.build.api.apiTest |
| |
| import com.android.SdkConstants |
| import com.android.testutils.AbstractBuildGivenBuildCheckTest |
| import com.android.testutils.TestUtils |
| import com.google.common.truth.Truth |
| import org.gradle.testkit.runner.BuildResult |
| import org.gradle.testkit.runner.GradleRunner |
| import org.junit.Rule |
| import org.junit.rules.TemporaryFolder |
| import org.junit.rules.TestName |
| import java.io.File |
| import java.io.FileReader |
| import java.util.Properties |
| |
| /** |
| * Base test class for Variant API related tasks. These tasks setup a Gradle project and execute |
| * some tasks on those and finally verifies the build behavior or output. |
| * |
| * @param testType the test type. |
| * @param scriptingLanguage the language used to express the build logic. |
| */ |
| open class VariantApiBaseTest( |
| private val testType: TestType, |
| protected val scriptingLanguage: ScriptingLanguage = ScriptingLanguage.Kotlin |
| ) : |
| AbstractBuildGivenBuildCheckTest<VariantApiBaseTest.GivenBuilder, BuildResult>() { |
| |
| companion object { |
| /** |
| * AGP version which can be overridden when invoking the test execution. |
| */ |
| val agpVersion = System.getenv("API_TESTS_VERSION") |
| ?: com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION |
| |
| /** |
| * If running within a build system, make sure to use the expected version when generating |
| * build.gradle.kts files. |
| */ |
| val kotlinVersion: String by lazy { |
| System.getenv("KOTLIN_PLUGIN")?.split(':')?.last() |
| // fall back, use the version I am running against. |
| ?: KotlinVersion.CURRENT.toString() |
| } |
| |
| val generalRepos= listOf( |
| "google()", |
| "jcenter()") |
| |
| /** |
| * List of custom repositories where all projects dependencies can be satisfied. |
| */ |
| val mavenRepos: List<String> by lazy { |
| System.getenv("CUSTOM_REPO").split(File.pathSeparatorChar) |
| } |
| |
| } |
| |
| /** |
| * Type of test. |
| */ |
| enum class TestType { |
| /** |
| * In [Script] tests, all build logic is expressed in build file scripts. |
| */ |
| Script { |
| override fun getDirName(test: VariantApiBaseTest): String { |
| return test.scriptingLanguage.name |
| } |
| }, |
| |
| /** |
| * In [BuildSrc] tests, build customization is done through a buildSrc plugin. |
| */ |
| BuildSrc, |
| |
| /** |
| * In [Plugin] tests, build customization is done through a binary plugin dependency. |
| */ |
| Plugin; |
| |
| open fun getDirName(test: VariantApiBaseTest): String = name |
| } |
| |
| /** |
| * Supported scripting languages for expressing build logic, right now, Kotlin and Groovy. |
| * |
| * @param buildFileName name of the module build file for the language. |
| * @param settingsFileName name of the settings file name for the language. |
| */ |
| enum class ScriptingLanguage(val buildFileName: String, val settingsFileName: String) { |
| |
| /** |
| * Kotlin scripting languages, |
| */ |
| Kotlin("build.gradle.kts", "settings.gradle.kts") { |
| override fun configureBuildFile(repositories: List<String>) = |
| """ |
| buildscript { |
| ${addGlobalRepositories(repositories).prependIndent(" ")} |
| dependencies { |
| classpath("com.android.tools.build:gradle:${agpVersion}") |
| classpath(kotlin("gradle-plugin", version = "$kotlinVersion")) |
| } |
| } |
| allprojects { |
| ${addGlobalRepositories(generalRepos).prependIndent(" ")} |
| } |
| """ |
| |
| // we should not have to add the gobalRepos below but because of |
| // https://github.com/gradle/gradle/issues/1055 |
| override fun configureBuildSrcBuildFile(globalRepos: List<String>, localRepos: List<String>) = |
| """ |
| plugins { |
| kotlin("jvm") version "$kotlinVersion" |
| } |
| ${addRepositories(localRepos)} |
| ${addGlobalRepositories(globalRepos)} |
| |
| dependencies { |
| implementation("com.android.tools.build:gradle-api:${agpVersion}") |
| implementation(kotlin("stdlib")) |
| gradleApi() |
| } |
| """ |
| |
| private fun addRepositories(repositories: List<String>) = |
| """ |
| repositories { |
| ${repositories.joinToString( |
| separator = "\")\n maven(\"", |
| prefix = " maven(\"", |
| postfix = "\")" |
| )} |
| } |
| """ |
| |
| override fun configureInitScript(repositories: List<String>): String = |
| """ |
| allprojects { |
| buildscript { |
| ${addRepositories(repositories).prependIndent(" ")} |
| } |
| ${addRepositories(repositories).prependIndent(" ")} |
| } |
| """ |
| |
| override fun makeScriptFileName(root: String): String { |
| return "$root.kts" |
| } |
| }, |
| Groovy("build.gradle", "settings.gradle") { |
| override fun configureBuildFile(repositories: List<String>) = |
| """ |
| buildscript { |
| ${addGlobalRepositories(repositories).prependIndent(" ")} |
| dependencies { |
| classpath("com.android.tools.build:gradle:${agpVersion}") |
| } |
| } |
| allprojects { |
| ${addGlobalRepositories(generalRepos).prependIndent(" ")} |
| } |
| """ |
| |
| override fun configureBuildSrcBuildFile(globalRepos: List<String>, localRepos: List<String>) = |
| """ |
| """ |
| |
| override fun configureInitScript(repositories: List<String>): String = |
| """ |
| allprojects { |
| buildscript { |
| ${addRepositories(repositories).prependIndent(" ")} |
| } |
| ${addRepositories(repositories).prependIndent(" ")} |
| } |
| """ |
| |
| override fun makeScriptFileName(root: String): String { |
| return root |
| } |
| |
| private fun addRepositories(repositories: List<String>) = |
| """ |
| repositories { |
| ${repositories.joinToString( |
| separator = "\'}\n maven { url \'", |
| prefix = " maven { url \'", |
| postfix = "\'}" |
| )} |
| } |
| """ |
| }; |
| |
| protected fun addGlobalRepositories(repositories: List<String>) = |
| repositories.joinToString( |
| separator = "\n ", |
| prefix = "repositories {\n ", |
| postfix = "\n}" |
| ) |
| |
| /** |
| * Configure a module build file for the [scriptingLanguage]. |
| * |
| * By default the AGP plugin will be automatically added, depending on the scripting |
| * language, other plugins like the kotlin gradle plugin when using kotlin for instance. |
| * |
| * @param repositories the list of repositories where project dependencies will be obtained |
| * from |
| */ |
| abstract fun configureBuildFile(repositories: List<String>): String |
| |
| /** |
| * Configure the buildSrc/ build file for the [scriptingLanguage] |
| * |
| * @param globalRepos the list of global repositories where project dependencies will be |
| * obtained from. |
| * @param localRepos the list of repositories where project dependencies will be obtained |
| * from |
| */ |
| abstract fun configureBuildSrcBuildFile(globalRepos: List<String>, localRepos: List<String>): String |
| |
| /** |
| * Configure the init script file for the [scriptingLanguage] |
| * |
| * @param repositories the list of local repositories where project dependencies will be |
| * obtained from |
| */ |
| abstract fun configureInitScript(repositories: List<String>): String |
| |
| abstract fun makeScriptFileName(root: String): String |
| } |
| |
| @Rule |
| @JvmField val testName= TestName() |
| |
| @Rule |
| @JvmField val testProjectDir = if (System.getenv("API_TESTS_OUTDIR") != null) { |
| ApiTestFolder(File(System.getenv("API_TESTS_OUTDIR")), testType.getDirName(this)) |
| } else { |
| TemporaryFolder() |
| } |
| |
| @Rule |
| @JvmField val privateTestProjectDir = TemporaryFolder() |
| |
| @Rule |
| @JvmField val testBuildDir = TemporaryFolder() |
| |
| open fun sdkLocation(): String = TestUtils.getSdk().absolutePath |
| |
| open class ModuleGivenBuilder(val scriptingLanguage: ScriptingLanguage) { |
| |
| /** |
| * Manifest file for the module, optional. |
| */ |
| var manifest: String? = null |
| |
| /** |
| * Build file for the module, must be provided by the time |
| * [AbstractBuildGivenBuildCheckTest.when] is called. |
| */ |
| var buildFile: String? = null |
| |
| private val sourceFiles = mutableListOf<Pair<String, String>>() |
| |
| /** |
| * Add a source file to the current module |
| * |
| * @param path the relative path from the module root folder to store the source file in. |
| * @param content the source file content. |
| */ |
| fun addSource(path: String, content: String) { |
| sourceFiles.add(Pair(path, content)) |
| } |
| |
| internal open fun writeModule(folder: File) { |
| Truth.assertThat(buildFile != null) |
| addBuildFile(folder) |
| |
| if (manifest != null) { |
| File(folder, "src/main").apply { |
| mkdirs() |
| File( |
| this, |
| SdkConstants.ANDROID_MANIFEST_XML |
| ).writeText(manifest!!.trimIndent()) |
| } |
| } |
| |
| sourceFiles.forEach { |
| File(folder, it.first).apply { |
| parentFile.mkdirs() |
| writeText(it.second.trimIndent()) |
| } |
| } |
| |
| } |
| internal open fun addBuildFile(folder: File) { |
| File(folder, scriptingLanguage.buildFileName).apply { |
| writeText(scriptingLanguage.configureBuildFile(generalRepos) + (buildFile ?: "")) |
| } |
| } |
| } |
| |
| open class GivenBuilder(scriptingLanguage: ScriptingLanguage) |
| : ModuleGivenBuilder(scriptingLanguage) { |
| |
| private val modules = mutableListOf<Pair<String, GivenBuilder>>() |
| |
| /** |
| * the list of tasks' names to invoke on the project, by default, we only invoke |
| * the assembleDebug task. |
| */ |
| internal val tasksToInvoke = mutableListOf<String>() |
| |
| private fun modulesPath(): List<String> = modules.filter { it.first != "buildSrc" }.map { it.first } |
| |
| /** |
| * Add a new module to the current module and open a new block to configure it. |
| * |
| * @param path the Gradle relative path for the module, must start with ':' |
| * @param action the configuration block for the module. |
| */ |
| fun addModule(path: String, action: GivenBuilder.() -> Unit) { |
| if (path == "buildSrc") throw RuntimeException("Use addBuildSrc() for buildSrc module") |
| object: GivenBuilder(scriptingLanguage) { |
| override fun addBuildFile(folder: File) { |
| if (buildFile != null) { |
| File(folder, scriptingLanguage.buildFileName).apply { |
| writeText(buildFile!!) |
| } |
| } |
| } |
| }.also { |
| modules.add(Pair(path, it)) |
| action.invoke(it) |
| } |
| } |
| |
| /** |
| * Add a buildSrc module to the current module. |
| * |
| * @param action the configuration block for the buildSrc module. |
| */ |
| fun addBuildSrc(action: GivenBuilder.() -> Unit) { |
| object: GivenBuilder(scriptingLanguage) { |
| override fun addBuildFile(folder: File) { |
| File(folder, scriptingLanguage.buildFileName).apply { |
| writeText(scriptingLanguage.configureBuildSrcBuildFile(generalRepos, mavenRepos).trimIndent() + "\n" + (buildFile ?: "")) |
| } |
| } |
| }.also { |
| modules.add(Pair("buildSrc", it)) |
| action.invoke(it) |
| } |
| } |
| |
| fun writeModule(folder: File, sdkLocation: String) { |
| val settingsFile = File(folder, scriptingLanguage.settingsFileName) |
| |
| val includeList = modulesPath().joinToString( |
| separator = "\")\ninclude(\"", |
| prefix = "include(\"", |
| postfix = "\")\n" |
| ) |
| |
| settingsFile.writeText( |
| """ |
| $includeList |
| rootProject.name = "${javaClass.simpleName}" |
| """.trimIndent()) |
| |
| super.writeModule(folder) |
| |
| modules.forEach { |
| File(folder, it.first.replace(':', File.separatorChar)).apply { |
| mkdirs() |
| it.second.writeModule(this) |
| } |
| } |
| } |
| |
| override fun addBuildFile(folder: File) { |
| File(folder, scriptingLanguage.buildFileName).apply { |
| writeText(scriptingLanguage.configureBuildFile(generalRepos) + (buildFile ?: "")) |
| } |
| } |
| } |
| |
| open class DocumentationBuilder { |
| var index: String? = null |
| } |
| |
| private var docs= DocumentationBuilder() |
| |
| fun withDocs(docs: DocumentationBuilder.()-> Unit) { |
| docs.invoke(this.docs) |
| } |
| |
| override fun defaultWhen(given: GivenBuilder): BuildResult? { |
| |
| val projectDir = File(testProjectDir.root, testName.methodName) |
| projectDir.deleteRecursively() |
| projectDir.mkdirs() |
| given.writeModule(projectDir, sdkLocation()) |
| docs.index?.apply { |
| File(projectDir, "readme.md").writeText(this) |
| } |
| |
| if (given.tasksToInvoke.isEmpty()) { |
| given.tasksToInvoke.add("assembleDebug") |
| } |
| |
| // create the init script. |
| val initScript = privateTestProjectDir.newFile(scriptingLanguage.makeScriptFileName("init.gradle")).also { |
| it.writeText(scriptingLanguage.configureInitScript(mavenRepos)) |
| } |
| |
| val gradleRunner = GradleRunner.create() |
| .withProjectDir(projectDir) |
| .withArguments( |
| "-Dorg.gradle.jvmargs=-Xmx2G", |
| "-Dandroid.enableJvmResourceCompiler=true", |
| "--init-script", initScript.absolutePath, |
| *given.tasksToInvoke.toTypedArray(), |
| "--stacktrace") |
| .withEnvironment(mapOf("ANDROID_SDK_ROOT" to sdkLocation())) |
| .forwardOutput() |
| |
| // if running within Intellij, we must set the Gradle Distribution when invoking the |
| // GradleRunner, so fetch it from our workspace and use the same URL we use to build |
| // tools/base projects. |
| if (System.getenv("SRC")!=null) { |
| val srcDir = System.getenv("SRC") |
| FileReader(File("$srcDir/tools/gradle/wrapper/gradle-wrapper.properties")).apply { |
| val gradleWrapperProperties = Properties() |
| .also { properties -> properties.load(this) } |
| val relativePathDistribution = gradleWrapperProperties["distributionUrl"] |
| gradleRunner.withGradleDistribution( |
| File("$srcDir/tools/gradle/wrapper/$relativePathDistribution").toURI()) |
| } |
| } |
| return gradleRunner.build() |
| } |
| |
| override fun instantiateGiven(): GivenBuilder = GivenBuilder(scriptingLanguage) |
| } |