[Runtime testing] Automate downloading GoogleTest

This commit is contained in:
Ilya Matveev
2020-10-06 13:44:51 +07:00
committed by Ilya Matveev
parent 51784c0e34
commit f644e394ab
5 changed files with 337 additions and 0 deletions
+3
View File
@@ -68,3 +68,6 @@ compile_commands.json
# clangd caches
.clangd/
# googletest framework used by runtime tests
runtime/googletest/
+4
View File
@@ -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"
}
}
}
@@ -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<URL>,
val revisionProvider: Provider<String>,
val outputDirectoryProvider: Provider<File>
) : 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<Boolean> = 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)
}
}
@@ -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<Project> {
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<GitDownloadTask> {
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<File>,
dependencies: Iterable<TaskProvider<*>>
) {
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") }
)
}
+10
View File
@@ -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() {