From f644e394abb2504aa13815658997f605e27b891c Mon Sep 17 00:00:00 2001 From: Ilya Matveev Date: Tue, 6 Oct 2020 13:44:51 +0700 Subject: [PATCH] [Runtime testing] Automate downloading GoogleTest --- .gitignore | 3 + build-tools/build.gradle.kts | 4 + .../kotlin/testing/native/GitDownloadTask.kt | 170 ++++++++++++++++++ .../testing/native/RuntimeTestingPlugin.kt | 150 ++++++++++++++++ runtime/build.gradle.kts | 10 ++ 5 files changed, 337 insertions(+) create mode 100644 build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/GitDownloadTask.kt create mode 100644 build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/RuntimeTestingPlugin.kt diff --git a/.gitignore b/.gitignore index 2c8d4af31f7..0782064312d 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,6 @@ compile_commands.json # clangd caches .clangd/ + +# googletest framework used by runtime tests +runtime/googletest/ \ No newline at end of file diff --git a/build-tools/build.gradle.kts b/build-tools/build.gradle.kts index 7e08634c30a..de57d35d69e 100644 --- a/build-tools/build.gradle.kts +++ b/build-tools/build.gradle.kts @@ -87,6 +87,10 @@ gradlePlugin { id = "compile-to-bitcode" implementationClass = "org.jetbrains.kotlin.bitcode.CompileToBitcodePlugin" } + create("runtimeTesting") { + id = "runtime-testing" + implementationClass = "org.jetbrains.kotlin.testing.native.RuntimeTestingPlugin" + } } } diff --git a/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/GitDownloadTask.kt b/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/GitDownloadTask.kt new file mode 100644 index 00000000000..b576a08e5d4 --- /dev/null +++ b/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/GitDownloadTask.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +package org.jetbrains.kotlin.testing.native + +import org.gradle.api.DefaultTask +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import org.gradle.process.ExecResult +import org.gradle.process.ExecSpec +import org.jetbrains.kotlin.isNotEmpty +import java.io.File +import java.net.URL +import java.util.* +import javax.inject.Inject + +/** + * Clones the given revision of the given Git repository to the given directory. + */ +@Suppress("UnstableApiUsage") +open class GitDownloadTask @Inject constructor( + val repositoryProvider: Provider, + val revisionProvider: Provider, + val outputDirectoryProvider: Provider +) : DefaultTask() { + + private val repository: URL + get() = repositoryProvider.get() + + private val revision: String + get() = revisionProvider.get() + + private val outputDirectory: File + get() = outputDirectoryProvider.get() + + @Option(option = "refresh", + description = "Fetch and checkout the revision even if the output directory already contains it. " + + "All changes in the output directory will be overwritten") + val refresh: Property = project.objects.property(Boolean::class.java).apply { + set(false) + } + + private val upToDateChecker = UpToDateChecker() + + init { + outputs.upToDateWhen { upToDateChecker.isUpToDate() } + } + + private fun git( + vararg args: String, + ignoreExitValue: Boolean = false, + execConfiguration: ExecSpec.() -> Unit = {} + ): ExecResult = + project.exec { + it.executable = "git" + it.args(*args) + it.isIgnoreExitValue = ignoreExitValue + it.execConfiguration() + } + + private fun tryCloneBranch(): Boolean { + val execResult = git( + "clone", repository.toString(), + outputDirectory.absolutePath, + "--depth", "1", + "--branch", revision, + ignoreExitValue = true + ) + return execResult.exitValue == 0 + } + + private fun fetchByHash() { + git("init", outputDirectory.absolutePath) + git("fetch", repository.toString(), "--depth", "1", revision) { + workingDir(outputDirectory) + } + git("reset", "--hard", revision) { + workingDir(outputDirectory) + } + } + + @TaskAction + fun clone() { + // Gradle ignores outputs.upToDateWhen { ... } and reruns a task if the classpath of the task was changed + // So we have to perform the up-to-date check manually one more time in the task action. + if (upToDateChecker.isUpToDate()) { + logger.info("Skip cloning to avoid rewriting possible debug changes in ${outputDirectory.absolutePath}.") + return + } + + project.delete { + it.delete(outputDirectory) + } + + if (!tryCloneBranch()) { + logger.info("Cannot use the revision '$revision' to clone the repository. Trying to use init && fetch instead.") + fetchByHash() + } + + // Store info about used revision for the manual up-to-date check. + upToDateChecker.storeRevisionInfo() + } + + + /** + * This class performs manual UP-TO-DATE checking. + * + * We want to be able to edit downloaded sources for debug purposes. Thus this task should not rewrite changes in + * the output directory. + * + * Gradle does allow us to provide a custom logic to determine if task outputs are UP-TO-DATE or not (see + * Task.outputs.upToDateWhen). But Gradle still reruns a task if its classpath was changed. This may lead + * to rewriting our manual debug changes in downloaded sources if there are some changes in the + * `build-tools` project. + * + * So we have to manually check up-to-dateness in upToDateWhen and as a first step of the task action. + */ + private inner class UpToDateChecker() { + + private val revisionInfoFile: File + get() = outputDirectory.resolve(".revision") + + /** + * The download task should be executed in the following cases: + * + * - The output directory doesn't exist or is empty; + * - Repository or revision was changed since the last execution; + * - A user forced rerunning this tasks manually (see [GitDownloadTask.refresh]). + * + * In all other cases we consider the task UP-TO-DATE. + */ + fun isUpToDate(): Boolean { + return !refresh.get() && + outputDirectory.let { it.exists() && project.fileTree(it).isNotEmpty } && + noRevisionChanges() + } + + private fun noRevisionChanges(): Boolean = + revisionInfoFile.exists() && loadRevisionInfo() == RevisionInfo(repository, revision) + + fun storeRevisionInfo() { + val properties = Properties() + properties["repository"] = repository.toString() + properties["revision"] = revision + revisionInfoFile.bufferedWriter().use { + properties.store(it, null) + } + } + + private fun loadRevisionInfo(): RevisionInfo? { + return try { + val properties = Properties() + revisionInfoFile.bufferedReader().use { + properties.load(it) + } + RevisionInfo(properties.getProperty("repository"), properties.getProperty("revision")) + } catch (_ : Exception) { + null + } + } + } + + private data class RevisionInfo(val repository: String, val revision: String) { + constructor(repository: URL, revision: String): this(repository.toString(), revision) + } +} diff --git a/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/RuntimeTestingPlugin.kt b/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/RuntimeTestingPlugin.kt new file mode 100644 index 00000000000..6d7f437233f --- /dev/null +++ b/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/RuntimeTestingPlugin.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +package org.jetbrains.kotlin.testing.native + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.FileCollection +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.bitcode.CompileToBitcodeExtension +import org.jetbrains.kotlin.bitcode.CompileToBitcodePlugin +import org.jetbrains.kotlin.resolve +import java.io.File +import java.net.URL +import javax.inject.Inject + +@Suppress("UnstableApiUsage") +open class RuntimeTestingPlugin : Plugin { + override fun apply(target: Project): Unit = with(target) { + val extension = extensions.create(GOOGLE_TEST_EXTENSION_NAME, GoogleTestExtension::class.java, target) + val downloadTask = registerDownloadTask(extension) + + val googleTestRoot = project.provider { extension.sourceDirectory } + + createBitcodeTasks(googleTestRoot, listOf(downloadTask)) + } + + private fun Project.registerDownloadTask(extension: GoogleTestExtension): TaskProvider { + val task = tasks.register( + "downloadGoogleTest", + GitDownloadTask::class.java, + provider { URL(extension.repository) }, + provider { extension.revision }, + provider { extension.fetchDirectory } + ) + task.configure { + it.refresh.set(provider { extension.refresh }) + it.onlyIf { extension.localSourceRoot == null } + it.description = "Retrieves GoogleTest from the given repository" + it.group = "Google Test" + } + return task + } + + private fun Project.createBitcodeTasks( + googleTestRoot: Provider, + dependencies: Iterable> + ) { + pluginManager.withPlugin("compile-to-bitcode") { + val bitcodeExtension = + project.extensions.getByName(CompileToBitcodePlugin.EXTENSION_NAME) as CompileToBitcodeExtension + + bitcodeExtension.create("googletest") { + srcDirs = project.files( + googleTestRoot.resolve("googletest/src") + ) + headersDirs = project.files( + googleTestRoot.resolve("googletest/include"), + googleTestRoot.resolve("googletest") + ) + includeFiles = listOf("*.cc") + excludeFiles = listOf("gtest-all.cc", "gtest_main.cc") + // Original GTest sources contain an unused variable on Windows (kAlternatePathSeparatorString). + compilerArgs.add("-Wno-unused") + dependsOn(dependencies) + } + + bitcodeExtension.create("googlemock") { + srcDirs = project.files( + googleTestRoot.resolve("googlemock/src") + ) + headersDirs = project.files( + googleTestRoot.resolve("googlemock"), + googleTestRoot.resolve("googlemock/include"), + googleTestRoot.resolve("googletest/include") + ) + includeFiles = listOf("*.cc") + excludeFiles = listOf("gmock-all.cc", "gmock_main.cc") + dependsOn(dependencies) + } + } + } + + companion object { + internal const val GOOGLE_TEST_EXTENSION_NAME = "googletest" + } + +} + +/** + * A project extension to configure from where we get the GoogleTest framework. + */ +open class GoogleTestExtension @Inject constructor(private val project: Project) { + + /** + * A repository to fetch GoogleTest from. + */ + var repository: String = "https://github.com/google/googletest.git" + + /** + * A particular revision in the [repository] to be fetched. It can be a branch, a tag or a commit hash. + */ + var revision: String = "master" + + /** + * Fetch the [revision] even if the [fetchDirectory] already contains it. Overwrite all changes manually made in the output directory. + */ + var refresh: Boolean = false + + /** + * A directory to fetch the [revision] to. + */ + var fetchDirectory: File = project.file("googletest") + + internal var localSourceRoot: File? = null + + /** + * Use a local [directory] with GoogleTest instead of the fetched one. If set, the download task will not be executed. + */ + fun useLocalSources(directory: File) { + localSourceRoot = directory + } + + /** + * Use a local [directory] with GoogleTest instead of the fetched one. If set, the download task will not be executed. + */ + fun useLocalSources(directory: String) { + localSourceRoot = project.file(directory) + } + + /** + * A getter for directory that contains the GTest sources. + * Returns a local source directory if it's specified (see [useLocalSources]) or [fetchDirectory] otherwise. + */ + val sourceDirectory: File + get() = localSourceRoot ?: fetchDirectory + + /** + * A file collection with header directories for GoogleTest and GoogleMock. + * Useful to configure compilation against GTest. + */ + val headersDirs: FileCollection = project.files( + project.provider { sourceDirectory.resolve("googletest/include") }, + project.provider { sourceDirectory.resolve("googlemock/include") } + ) +} + diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index a719c921295..f9d691fbad8 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -3,10 +3,20 @@ * that can be found in the LICENSE file. */ import org.jetbrains.kotlin.* +import org.jetbrains.kotlin.testing.native.* import org.jetbrains.kotlin.bitcode.CompileToBitcode plugins { id("compile-to-bitcode") + id("runtime-testing") +} + +googletest { + // The latest release GTest (1.10.0) doesn't properly register skipped tests in an XML-report. + // Therefore we use a fixed commit form the master branch where this problem is already fixed. + // https://github.com/google/googletest/commit/07f4869221012b16b7f9ee685d94856e1fc9f361 + revision = "07f4869221012b16b7f9ee685d94856e1fc9f361" + refresh = project.hasProperty("refresh-gtest") } fun CompileToBitcode.includeRuntime() {