[Runtime testing] Automate downloading GoogleTest
This commit is contained in:
committed by
Ilya Matveev
parent
51784c0e34
commit
f644e394ab
@@ -68,3 +68,6 @@ compile_commands.json
|
||||
|
||||
# clangd caches
|
||||
.clangd/
|
||||
|
||||
# googletest framework used by runtime tests
|
||||
runtime/googletest/
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+150
@@ -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") }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user