[K/N] Refactor runtime testing tasks

Merge-request: KT-MR-6360
Merged-by: Alexander Shabalin <Alexander.Shabalin@jetbrains.com>
This commit is contained in:
Alexander Shabalin
2022-06-25 09:55:27 +00:00
committed by Space
parent bb493ccb3e
commit eaad75fd52
6 changed files with 588 additions and 345 deletions
@@ -5,25 +5,33 @@
package org.jetbrains.kotlin.bitcode
import org.gradle.api.Action
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.kotlin.dsl.*
import org.gradle.language.base.plugins.LifecycleBasePlugin
import org.jetbrains.kotlin.ExecClang
import org.jetbrains.kotlin.cpp.CompilationDatabaseExtension
import org.jetbrains.kotlin.cpp.CompilationDatabasePlugin
import org.jetbrains.kotlin.cpp.CompileToExecutable
import org.jetbrains.kotlin.cpp.RunGTest
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.konan.target.PlatformManager
import org.jetbrains.kotlin.konan.target.SanitizerKind
import org.jetbrains.kotlin.konan.target.supportedSanitizers
import org.jetbrains.kotlin.testing.native.CompileNativeTest
import org.jetbrains.kotlin.testing.native.GoogleTestExtension
import org.jetbrains.kotlin.testing.native.RunNativeTest
import org.jetbrains.kotlin.testing.native.RuntimeTestingPlugin
import org.jetbrains.kotlin.utils.Maybe
import org.jetbrains.kotlin.utils.asMaybe
import java.io.File
import javax.inject.Inject
private abstract class RunGTestSemaphore : BuildService<BuildServiceParameters.None>
private abstract class CompileTestsSemaphore : BuildService<BuildServiceParameters.None>
/**
* A plugin creating extensions to compile
*/
@@ -42,6 +50,21 @@ open class CompileToBitcodeExtension @Inject constructor(val project: Project) {
private val compilationDatabase = project.extensions.getByType<CompilationDatabaseExtension>()
private val execClang = project.extensions.getByType<ExecClang>()
private val platformManager = project.extensions.getByType<PlatformManager>()
// googleTestExtension is only used if testsGroup is used.
private val googleTestExtension by lazy { project.extensions.getByType<GoogleTestExtension>() }
// A shared service used to limit parallel execution of test binaries.
private val runGTestSemaphore = project.gradle.sharedServices.registerIfAbsent("runGTestSemaphore", RunGTestSemaphore::class.java) {
// Probably can be made configurable if test reporting moves away from simple gtest stdout dumping.
maxParallelUsages.set(1)
}
// TODO: remove when tests compilation does not consume so much memory.
private val compileTestsSemaphore = project.gradle.sharedServices.registerIfAbsent("compileTestsSemaphore", CompileTestsSemaphore::class.java) {
maxParallelUsages.set(5)
}
private val targetList = with(project) {
provider { (rootProject.project(":kotlin-native").property("targetList") as? List<*>)?.filterIsInstance<String>() ?: emptyList() } // TODO: Can we make it better?
@@ -50,14 +73,20 @@ open class CompileToBitcodeExtension @Inject constructor(val project: Project) {
private val allMainModulesTasks by lazy {
val name = project.name.capitalized
targetList.get().associateBy(keySelector = { it }, valueTransform = {
project.tasks.register("${it}$name")
project.tasks.register("${it}$name") {
description = "Build all main modules of $name for $it"
group = BUILD_TASK_GROUP
}
})
}
private val allTestsTasks by lazy {
val name = project.name.capitalized
targetList.get().associateBy(keySelector = { it }, valueTransform = {
project.tasks.register("${it}${name}Tests")
project.tasks.register("${it}${name}Tests") {
description = "Runs all $name tests for $it"
group = VERIFICATION_TASK_GROUP
}
})
}
@@ -79,7 +108,6 @@ open class CompileToBitcodeExtension @Inject constructor(val project: Project) {
fun module(name: String, srcRoot: File = project.file("src/$name"), outputGroup: String = "main", configurationBlock: CompileToBitcode.() -> Unit = {}) {
targetList.get().forEach { targetName ->
val platformManager = project.rootProject.project(":kotlin-native").findProperty("platformManager") as PlatformManager
val target = platformManager.targetByName(targetName)
val sanitizers: List<SanitizerKind?> = target.supportedSanitizers() + listOf(null)
val allMainModulesTask = allMainModulesTasks[targetName]!!
@@ -90,13 +118,11 @@ open class CompileToBitcodeExtension @Inject constructor(val project: Project) {
headersDirs = srcDirs + project.files(srcRoot.resolve("headers"))
this.sanitizer = sanitizer
group = BasePlugin.BUILD_GROUP
val sanitizerDescription = when (sanitizer) {
null -> ""
SanitizerKind.ADDRESS -> " with ASAN"
SanitizerKind.THREAD -> " with TSAN"
when (outputGroup) {
"test" -> group = VERIFICATION_BUILD_TASK_GROUP
"main" -> group = BUILD_TASK_GROUP
}
description = "Compiles '$name' to bitcode for $targetName$sanitizerDescription"
description = "Compiles '$name' to bitcode for $targetName${sanitizer.description}"
dependsOn(":kotlin-native:dependencies:update")
configurationBlock()
}
@@ -110,28 +136,38 @@ open class CompileToBitcodeExtension @Inject constructor(val project: Project) {
}
}
abstract class TestsGroup @Inject constructor(
val target: KonanTarget,
private val _sanitizer: Maybe<SanitizerKind>,
) {
val sanitizer
get() = _sanitizer.orNull
abstract val testedModules: ListProperty<String>
abstract val testSupportModules: ListProperty<String>
abstract val testLauncherModule: Property<String>
}
private fun createTestTask(
project: Project,
testName: String,
testedTaskNames: List<String>,
sanitizer: SanitizerKind?,
): Task {
val platformManager = project.project(":kotlin-native").findProperty("platformManager") as PlatformManager
val googleTestExtension = project.extensions.getByName(RuntimeTestingPlugin.GOOGLE_TEST_EXTENSION_NAME) as GoogleTestExtension
val testedTasks = testedTaskNames.map {
project.tasks.getByName(it) as CompileToBitcode
testTaskName: String,
testsGroup: TestsGroup,
) {
val target = testsGroup.target
val sanitizer = testsGroup.sanitizer
val testName = fullTaskName(testTaskName, target.name, sanitizer)
val testedTasks = testsGroup.testedModules.get().map {
val name = fullTaskName(it, target.name, sanitizer)
project.tasks.getByName(name) as CompileToBitcode
}
val target = testedTasks.map {
it.target
}.distinct().single()
val konanTarget = platformManager.targetByName(target)
val compileToBitcodeTasks = testedTasks.mapNotNull {
val name = "${it.name}TestBitcode"
val task = project.tasks.findByName(name) as? CompileToBitcode
?: project.tasks.create(name, CompileToBitcode::class.java, "${it.folderName}Tests", target, "test").apply {
?: project.tasks.create(name, CompileToBitcode::class.java, "${it.folderName}Tests", target.name, "test").apply {
srcDirs = it.srcDirs
headersDirs = it.headersDirs + googleTestExtension.headersDirs
group = VERIFICATION_BUILD_TASK_GROUP
description = "Compiles '${it.name}' tests to bitcode for $target${sanitizer.description}"
this.sanitizer = sanitizer
excludeFiles = emptyList()
includeFiles = listOf("**/*Test.cpp", "**/*TestSupport.cpp", "**/*Test.mm", "**/*TestSupport.mm")
@@ -139,71 +175,81 @@ open class CompileToBitcodeExtension @Inject constructor(val project: Project) {
dependsOn("downloadGoogleTest")
compilerArgs.addAll(it.compilerArgs)
addToCompdb(this, konanTarget)
addToCompdb(this, target)
}
if (task.inputFiles.count() == 0) null
else task
}
// TODO: Consider using sanitized versions.
val testFrameworkTasks = listOf(project.tasks.getByName(fullTaskName("googletest", target, null)) as CompileToBitcode, project.tasks.getByName(fullTaskName("googlemock", target, null)) as CompileToBitcode)
val testFrameworkTasks = testsGroup.testSupportModules.get().map {
val name = fullTaskName(it, target.name, sanitizer)
project.tasks.getByName(name) as CompileToBitcode
}
val testSupportTask = project.tasks.getByName(fullTaskName("test_support", target, sanitizer)) as CompileToBitcode
val testSupportTask = testsGroup.testLauncherModule.get().let {
val name = fullTaskName(it, target.name, sanitizer)
project.tasks.getByName(name) as CompileToBitcode
}
val mimallocEnabled = testedTaskNames.any { it.contains("mimalloc", ignoreCase = true) }
val compileTask = project.tasks.create(
"${testName}Compile",
CompileNativeTest::class.java,
testName,
konanTarget,
testSupportTask.outFile,
platformManager,
mimallocEnabled,
).apply {
val tasksToLink = (compileToBitcodeTasks + testedTasks + testFrameworkTasks)
this.sanitizer = sanitizer
this.inputFiles.setFrom(tasksToLink.map { it.outFile })
val compileTask = project.tasks.register<CompileToExecutable>("${testName}Compile") {
description = "Compile tests group '$testTaskName' for $target${sanitizer.description}"
group = VERIFICATION_BUILD_TASK_GROUP
this.target.set(target)
this.sanitizer.set(sanitizer)
this.outputFile.set(project.layout.buildDirectory.file("bin/test/${target}/$testName${target.executableExtension}"))
this.llvmLinkFirstStageOutputFile.set(project.layout.buildDirectory.file("bitcode/test/$target/$testName-firstStage.bc"))
this.llvmLinkOutputFile.set(project.layout.buildDirectory.file("bitcode/test/$target/$testName.bc"))
this.compilerOutputFile.set(project.layout.buildDirectory.file("obj/$target/$testName.o"))
this.mimallocEnabled.set(testsGroup.testedModules.get().any { it.contains("mimalloc") })
this.mainFile.set(testSupportTask.outFile)
dependsOn(testSupportTask)
val tasksToLink = (compileToBitcodeTasks + testedTasks + testFrameworkTasks)
this.inputFiles.setFrom(tasksToLink.map { it.outFile })
dependsOn(tasksToLink)
usesService(compileTestsSemaphore)
}
val runTask = project.tasks.create(
testName,
RunNativeTest::class.java,
testName,
compileTask.outputFile,
).apply {
this.sanitizer = sanitizer
val runTask = project.tasks.register<RunGTest>(testName) {
description = "Runs tests group '$testTaskName' for $target${sanitizer.description}"
group = VERIFICATION_TASK_GROUP
this.testName.set(testName)
executable.set(compileTask.flatMap { it.outputFile })
dependsOn(compileTask)
reportFileUnprocessed.set(project.layout.buildDirectory.file("testReports/$testName/report.xml"))
reportFile.set(project.layout.buildDirectory.file("testReports/$testName/report-with-prefixes.xml"))
filter.set(project.findProperty("gtest_filter") as? String)
tsanSuppressionsFile.set(project.layout.projectDirectory.file("tsan_suppressions.txt"))
usesService(runGTestSemaphore)
}
return runTask
allTestsTasks[target.name]!!.configure {
dependsOn(runTask)
}
}
fun testsGroup(
testTaskName: String,
testedTaskNames: List<String>,
action: Action<in TestsGroup>,
) {
val platformManager = project.rootProject.project(":kotlin-native").findProperty("platformManager") as PlatformManager
targetList.get().forEach { targetName ->
val target = platformManager.targetByName(targetName)
platformManager.enabled.forEach { target ->
val sanitizers: List<SanitizerKind?> = target.supportedSanitizers() + listOf(null)
val allTestsTask = allTestsTasks[targetName]!!
sanitizers.forEach { sanitizer ->
val name = fullTaskName(testTaskName, targetName, sanitizer)
val testedNames = testedTaskNames.map {
fullTaskName(it, targetName, sanitizer)
}
val task = createTestTask(project, name, testedNames, sanitizer)
allTestsTask.configure {
dependsOn(task)
val instance = project.objects.newInstance(TestsGroup::class.java, target, sanitizer.asMaybe).apply {
testSupportModules.convention(listOf("googletest", "googlemock"))
testLauncherModule.convention("test_support")
action.execute(this)
}
createTestTask(testTaskName, instance)
}
}
}
companion object {
private const val COMPILATION_DATABASE_TASK_NAME = "CompilationDatabase"
const val BUILD_TASK_GROUP = LifecycleBasePlugin.BUILD_GROUP
const val VERIFICATION_TASK_GROUP = LifecycleBasePlugin.VERIFICATION_GROUP
const val VERIFICATION_BUILD_TASK_GROUP = "verification build"
@OptIn(ExperimentalStdlibApi::class)
private val String.capitalized: String
@@ -220,5 +266,18 @@ open class CompileToBitcodeExtension @Inject constructor(val project: Project) {
SanitizerKind.THREAD -> "_TSAN"
}
private val SanitizerKind?.description
get() = when (this) {
null -> ""
SanitizerKind.ADDRESS -> " with ASAN"
SanitizerKind.THREAD -> " with TSAN"
}
private val KonanTarget.executableExtension
get() = when (this) {
is KonanTarget.MINGW_X64 -> ".exe"
is KonanTarget.MINGW_X86 -> ".exe"
else -> ""
}
}
}
@@ -0,0 +1,278 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.cpp
import org.gradle.api.*
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.logging.Logging
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import org.gradle.kotlin.dsl.getByType
import org.gradle.process.ExecOperations
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkerExecutor
import org.jetbrains.kotlin.ExecClang
import org.jetbrains.kotlin.execLlvmUtility
import org.jetbrains.kotlin.konan.target.*
import java.io.OutputStream
import javax.inject.Inject
private abstract class CompileToExecutableJob : WorkAction<CompileToExecutableJob.Parameters> {
interface Parameters : WorkParameters {
val mainFile: RegularFileProperty
val inputFiles: ConfigurableFileCollection
val llvmLinkFirstStageOutputFile: RegularFileProperty
val llvmLinkOutputFile: RegularFileProperty
val compilerOutputFile: RegularFileProperty
val outputFile: RegularFileProperty
// TODO: Figure out a way to pass KonanTarget, but it is used as a key into PlatformManager,
// so object identity matters, and platform managers are different between project and worker sides.
val targetName: Property<String>
val clangFlags: ListProperty<String>
val linkCommands: ListProperty<List<String>>
val platformManager: Property<PlatformManager>
}
@get:Inject
abstract val execOperations: ExecOperations
@get:Inject
abstract val objects: ObjectFactory
private val target: KonanTarget by lazy {
parameters.platformManager.get().targetByName(parameters.targetName.get())
}
private fun llvmLink() {
with(parameters) {
llvmLinkFirstStageOutputFile.asFile.get().parentFile.mkdirs()
// The runtime provides our implementations for some standard functions (see StdCppStubs.cpp).
// We need to internalize these symbols to avoid clashes with symbols provided by the C++ stdlib.
// But llvm-link -internalize is kinda broken: it links modules one by one and can't see usages
// of a symbol in subsequent modules. So it will mangle such symbols causing "unresolved symbol"
// errors at the link stage. So we have to run llvm-link twice: the first one links all modules
// except the one containing the entry point to a single *.bc without internalization. The second
// run internalizes this big module and links it with a module containing the entry point.
execOperations.execLlvmUtility(platformManager.get(), "llvm-link") {
args = listOf("-o", llvmLinkFirstStageOutputFile.asFile.get().absolutePath) + inputFiles.map { it.absolutePath }
}
llvmLinkOutputFile.asFile.get().parentFile.mkdirs()
execOperations.execLlvmUtility(platformManager.get(), "llvm-link") {
args = listOf("-o", llvmLinkOutputFile.asFile.get().absolutePath, mainFile.asFile.get().absolutePath, llvmLinkFirstStageOutputFile.asFile.get().absolutePath, "-internalize")
}
}
}
private fun compile() {
with(parameters) {
val execClang = ExecClang.create(objects, platformManager.get())
val args = clangFlags.get() + listOf(llvmLinkOutputFile.asFile.get().absolutePath, "-o", compilerOutputFile.asFile.get().absolutePath)
compilerOutputFile.asFile.get().parentFile.mkdirs()
if (target.family.isAppleFamily) {
execClang.execToolchainClang(target) {
executable = "clang++"
this.args = args
}
} else {
execClang.execBareClang {
executable = "clang++"
this.args = args
}
}
}
}
private fun link() {
with(parameters) {
outputFile.asFile.get().parentFile.mkdirs()
val logging = Logging.getLogger(CompileToExecutableJob::class.java)
for (command in linkCommands.get()) {
execOperations.exec {
commandLine(command)
if (!logging.isInfoEnabled && command[0].endsWith("dsymutil")) {
// Suppress dsymutl's warnings.
// See: https://bugs.swift.org/browse/SR-11539.
val nullOutputStream = object : OutputStream() {
override fun write(b: Int) {}
}
errorOutput = nullOutputStream
}
}
}
}
}
override fun execute() {
llvmLink()
compile()
link()
}
}
/**
* Compile bitcode files to binary executable.
*
* Takes bitcode files [inputFiles] and bitcode file [mainFile] with `main()` entrypoint and produces executable [outputFile].
*
* This works in 4 stages:
* 1. `llvm-link` all [inputFiles] into [llvmLinkFirstStageOutputFile].
* 2. `llvm-link` [llvmLinkFirstStageOutputFile] and [mainFile] with `-internalize` into [llvmLinkOutputFile].
* 3. `clang++` [llvmLinkOutputFile] into object file [compilerOutputFile].
* 4. Link [compilerOutputFile] into executable [outputFile].
*
* @see CompileToBitcodePlugin
*/
abstract class CompileToExecutable : DefaultTask() {
private val platformManager = project.extensions.getByType<PlatformManager>()
/**
* Target for which to compile.
*/
@get:Input
abstract val target: Property<KonanTarget>
/**
* Sanitizer for which to compile.
*/
@get:Input
@get:Optional
abstract val sanitizer: Property<SanitizerKind>
// TODO: Should be replaced by a list of libraries to be linked with.
/**
* Controls whether linker should add library dependencies.
*/
@get:Input
abstract val mimallocEnabled: Property<Boolean>
/**
* Bitcode file with the `main()` entrypoint.
*/
@get:InputFile
abstract val mainFile: RegularFileProperty
/**
* Bitcode files.
*
* Bitcode file with main should be put into [mainFile] instead.
*/
@get:SkipWhenEmpty
@get:InputFiles
abstract val inputFiles: ConfigurableFileCollection
/**
* Internal file with first stage llvm-link result.
*/
@get:Internal
abstract val llvmLinkFirstStageOutputFile: RegularFileProperty
/**
* Internal file with final stage llvm-link result.
*/
@get:Internal
abstract val llvmLinkOutputFile: RegularFileProperty
/**
* Internal file with compiler result.
*/
@get:Internal
abstract val compilerOutputFile: RegularFileProperty
/**
* Final executable.
*/
@get:OutputFile
abstract val outputFile: RegularFileProperty
/**
* Extra args to the compiler.
*/
@get:Input
abstract val compilerArgs: ListProperty<String>
/**
* Extra args to the linker.
*/
@get:Input
abstract val linkerArgs: ListProperty<String>
@get:Input
protected val linkCommands: Provider<List<List<String>>> = project.provider {
// Getting link commands requires presence of a target toolchain.
// Thus we cannot get them at the configuration stage because the toolchain may be not downloaded yet.
platformManager.platform(target.get()).linker.finalLinkCommands(
listOf(compilerOutputFile.asFile.get().absolutePath),
outputFile.asFile.get().absolutePath,
listOf(),
linkerArgs.get(),
optimize = false,
debug = true,
kind = LinkerOutputKind.EXECUTABLE,
outputDsymBundle = outputFile.asFile.get().absolutePath + ".dSYM",
needsProfileLibrary = false,
mimallocEnabled = mimallocEnabled.get(),
sanitizer = sanitizer.orNull
).map { it.argsWithExecutable }
}
@get:Inject
protected abstract val workerExecutor: WorkerExecutor
@TaskAction
fun compile() {
val workQueue = workerExecutor.noIsolation()
val defaultClangFlags = buildClangFlags(platformManager.platform(target.get()).configurables)
val sanitizerFlags = when (sanitizer.orNull) {
null -> listOf()
SanitizerKind.ADDRESS -> listOf("-fsanitize=address")
SanitizerKind.THREAD -> listOf("-fsanitize=thread")
}
workQueue.submit(CompileToExecutableJob::class.java) {
mainFile.set(this@CompileToExecutable.mainFile)
inputFiles.from(this@CompileToExecutable.inputFiles)
llvmLinkFirstStageOutputFile.set(this@CompileToExecutable.llvmLinkFirstStageOutputFile)
llvmLinkOutputFile.set(this@CompileToExecutable.llvmLinkOutputFile)
compilerOutputFile.set(this@CompileToExecutable.compilerOutputFile)
outputFile.set(this@CompileToExecutable.outputFile)
targetName.set(this@CompileToExecutable.target.get().name)
clangFlags.addAll(defaultClangFlags + compilerArgs.get() + sanitizerFlags)
linkCommands.set(this@CompileToExecutable.linkCommands)
platformManager.set(this@CompileToExecutable.platformManager)
}
}
}
/**
* Returns a list of Clang -cc1 arguments (including -cc1 itself) that are used for bitcode compilation in Kotlin/Native.
*
* See also: [org.jetbrains.kotlin.backend.konan.BitcodeCompiler]
*/
private fun buildClangFlags(configurables: Configurables): List<String> = mutableListOf<String>().apply {
require(configurables is ClangFlags)
addAll(configurables.clangFlags)
addAll(configurables.clangNooptFlags)
val targetTriple = if (configurables is AppleConfigurables) {
configurables.targetTriple.withOSVersion(configurables.osVersionMin)
} else {
configurables.targetTriple
}
addAll(listOf("-triple", targetTriple.toString()))
}.toList()
@@ -0,0 +1,133 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.cpp
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.process.ExecOperations
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkerExecutor
import javax.inject.Inject
private abstract class RunGTestJob : WorkAction<RunGTestJob.Parameters> {
interface Parameters : WorkParameters {
val testName: Property<String>
val executable: RegularFileProperty
val reportFile: RegularFileProperty
val reportFileUnprocessed: RegularFileProperty
val filter: Property<String>
val tsanSuppressionsFile: RegularFileProperty
}
@get:Inject
abstract val execOperations: ExecOperations
override fun execute() {
// TODO: Use ExecutorService to allow running it on targets other than host.
// TODO: Try to make it like other gradle test tasks: report progress in a way gradle understands instead of dumping stdout of gtest.
with(parameters) {
reportFileUnprocessed.asFile.get().parentFile.mkdirs()
execOperations.exec {
executable = this@with.executable.asFile.get().absolutePath
filter.orNull?.also {
args("--gtest_filter=${it}")
}
args("--gtest_output=xml:${reportFileUnprocessed.asFile.get().absolutePath}")
tsanSuppressionsFile.orNull?.also {
environment("TSAN_OPTIONS", "suppressions=${it.asFile.absolutePath}")
}
}
reportFile.asFile.get().parentFile.mkdirs()
// TODO: Better to use proper XML parsing.
var contents = reportFileUnprocessed.asFile.get().readText()
contents = contents.replace("<testsuite name=\"", "<testsuite name=\"${testName.get()}.")
contents = contents.replace("classname=\"", "classname=\"${testName.get()}.")
reportFile.asFile.get().writeText(contents)
}
}
}
/**
* Run [googletest](https://google.github.io/googletest) test binary from [executable].
*
* Test reports are placed in [reportFileUnprocessed] and in [reportFile] (decorates each test with [testName]).
*
* @see CompileToBitcodePlugin
*/
abstract class RunGTest : DefaultTask() {
/**
* Decorating test names in the report.
*
* Useful when CI merges different test results of the same test but for different test targets.
*/
@get:Input
abstract val testName: Property<String>
/**
* Test executable
*/
@get:InputFile
abstract val executable: RegularFileProperty
/**
* Test report with each test name decorated with [testName].
*/
@get:OutputFile
abstract val reportFile: RegularFileProperty
/**
* Undecorated test report.
*/
@get:OutputFile
abstract val reportFileUnprocessed: RegularFileProperty
/**
* Run a subset of tests.
*
* Follows [googletest](https://google.github.io/googletest/advanced.html#running-a-subset-of-the-tests) syntax:
* a ':'-separated list of glob patterns.
*
* Examples:
* * `SomeTest*` - run every test starting with `SomeTest`.
* * `SomeTest*:*stress*` - run every test starting with `SomeTest` and also every test containing `stress`.
* * `SomeTest*:-SomeTest.flakyTest` - Run every test starting with `SomeTest` except `SomeTest.flakyTest`.
*/
@get:Input
@get:Optional
abstract val filter: Property<String>
/**
* Suppression rules for TSAN.
*/
@get:InputFile
@get:Optional
abstract val tsanSuppressionsFile: RegularFileProperty
@get:Inject
protected abstract val workerExecutor: WorkerExecutor
@TaskAction
fun run() {
val workQueue = workerExecutor.noIsolation()
workQueue.submit(RunGTestJob::class.java) {
testName.set(this@RunGTest.testName)
executable.set(this@RunGTest.executable)
reportFile.set(this@RunGTest.reportFile)
reportFileUnprocessed.set(this@RunGTest.reportFileUnprocessed)
filter.set(this@RunGTest.filter)
tsanSuppressionsFile.set(this@RunGTest.tsanSuppressionsFile)
}
}
}
@@ -1,271 +0,0 @@
/*
* 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 groovy.lang.Closure
import java.io.File
import javax.inject.Inject
import org.gradle.api.*
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.logging.Logging
import org.gradle.api.tasks.*
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.process.ExecOperations
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkerExecutor
import org.jetbrains.kotlin.ExecClang
import org.jetbrains.kotlin.execLlvmUtility
import org.jetbrains.kotlin.konan.target.*
import java.io.OutputStream
interface CompileNativeTestParameters : WorkParameters {
var mainFile: File
var inputFiles: List<File>
var llvmLinkOutputFile: File
var compilerOutputFile: File
var targetName: String
var compilerArgs: List<String>
var linkCommands: List<List<String>>
val platformManager: Property<PlatformManager>
}
abstract class CompileNativeTestJob : WorkAction<CompileNativeTestParameters> {
@get:Inject
abstract val execOperations: ExecOperations
@get:Inject
abstract val objects: ObjectFactory
private fun llvmLink() {
with(parameters) {
val tmpOutput = File.createTempFile("runtimeTests", ".bc").apply {
deleteOnExit()
}
// The runtime provides our implementations for some standard functions (see StdCppStubs.cpp).
// We need to internalize these symbols to avoid clashes with symbols provided by the C++ stdlib.
// But llvm-link -internalize is kinda broken: it links modules one by one and can't see usages
// of a symbol in subsequent modules. So it will mangle such symbols causing "unresolved symbol"
// errors at the link stage. So we have to run llvm-link twice: the first one links all modules
// except the one containing the entry point to a single *.bc without internalization. The second
// run internalizes this big module and links it with a module containing the entry point.
execOperations.execLlvmUtility(platformManager.get(), "llvm-link") {
args = listOf("-o", tmpOutput.absolutePath) + inputFiles.map { it.absolutePath }
}
execOperations.execLlvmUtility(platformManager.get(), "llvm-link") {
args = listOf(
"-o", llvmLinkOutputFile.absolutePath,
mainFile.absolutePath,
tmpOutput.absolutePath,
"-internalize"
)
}
}
}
private fun compile() {
with(parameters) {
val execClang = ExecClang.create(objects, platformManager.get())
val target = platformManager.get().targetByName(targetName)
val clangFlags = buildClangFlags(platformManager.get().platform(target).configurables)
if (target.family.isAppleFamily) {
execClang.execToolchainClang(target) {
executable = "clang++"
this.args = clangFlags + compilerArgs + listOf(llvmLinkOutputFile.absolutePath, "-o", compilerOutputFile.absolutePath)
}
} else {
execClang.execBareClang {
executable = "clang++"
this.args = clangFlags + compilerArgs + listOf(llvmLinkOutputFile.absolutePath, "-o", compilerOutputFile.absolutePath)
}
}
}
}
private fun link() {
val logging = Logging.getLogger(CompileNativeTestJob::class.java)
with(parameters) {
for (command in linkCommands) {
execOperations.exec {
commandLine(command)
if (!logging.isInfoEnabled && command[0].endsWith("dsymutil")) {
// Suppress dsymutl's warnings.
// See: https://bugs.swift.org/browse/SR-11539.
val nullOutputStream = object: OutputStream() {
override fun write(b: Int) {}
}
errorOutput = nullOutputStream
}
}
}
}
}
override fun execute() {
llvmLink()
compile()
link()
}
}
abstract class CompileNativeTest @Inject constructor(
baseName: String,
@Input val target: KonanTarget,
@InputFile val mainFile: File,
private val platformManager: PlatformManager,
private val mimallocEnabled: Boolean,
) : DefaultTask() {
@SkipWhenEmpty
@InputFiles
val inputFiles: ConfigurableFileCollection = project.files()
@OutputFile
var llvmLinkOutputFile: File = project.buildDir.resolve("bitcode/test/${target.name}/$baseName.bc")
@OutputFile
var compilerOutputFile: File = project.buildDir.resolve("bin/test/${target.name}/$baseName.o")
private val executableExtension: String = when (target) {
is KonanTarget.MINGW_X64 -> ".exe"
is KonanTarget.MINGW_X86 -> ".exe"
else -> ""
}
@OutputFile
var outputFile: File = project.buildDir.resolve("bin/test/${target.name}/$baseName$executableExtension")
@Input
val clangArgs = mutableListOf<String>()
@Input
val linkerArgs = mutableListOf<String>()
@Input @Optional
var sanitizer: SanitizerKind? = null
private val sanitizerFlags = when (sanitizer) {
null -> listOf()
SanitizerKind.ADDRESS -> listOf("-fsanitize=address")
SanitizerKind.THREAD -> listOf("-fsanitize=thread")
}
@get:Input
val linkCommands: List<List<String>>
get() {
// Getting link commands requires presence of a target toolchain.
// Thus we cannot get them at the configuration stage because the toolchain may be not downloaded yet.
val linker = platformManager.platform(target).linker
return linker.finalLinkCommands(
listOf(compilerOutputFile.absolutePath),
outputFile.absolutePath,
listOf(),
linkerArgs,
optimize = false,
debug = true,
kind = LinkerOutputKind.EXECUTABLE,
outputDsymBundle = outputFile.absolutePath + ".dSYM",
needsProfileLibrary = false,
mimallocEnabled = mimallocEnabled,
sanitizer = sanitizer
).map { it.argsWithExecutable }
}
@get:Inject
abstract val workerExecutor: WorkerExecutor
@TaskAction
fun compile() {
val workQueue = workerExecutor.noIsolation()
val parameters = { it: CompileNativeTestParameters ->
it.mainFile = mainFile
it.inputFiles = inputFiles.map { it }
it.llvmLinkOutputFile = llvmLinkOutputFile
it.compilerOutputFile = compilerOutputFile
it.targetName = target.name
it.compilerArgs = clangArgs + sanitizerFlags
it.linkCommands = linkCommands
it.platformManager.set(platformManager)
}
workQueue.submit(CompileNativeTestJob::class.java, parameters)
}
}
open class RunNativeTest @Inject constructor(
@Input val testName: String,
@InputFile val inputFile: File,
) : DefaultTask() {
@Internal
var workingDir: File = project.buildDir.resolve("testReports/$testName")
@OutputFile
var outputFile: File = workingDir.resolve("report.xml")
@OutputFile
var outputFileWithPrefixes: File = workingDir.resolve("report-with-prefixes.xml")
@Input @Optional
var filter: String? = project.findProperty("gtest_filter") as? String
@Input @Optional
var sanitizer: SanitizerKind? = null
@InputFile
var tsanSuppressionsFile = project.file("tsan_suppressions.txt")
@TaskAction
fun run() {
workingDir.mkdirs()
// Do not run this in workers, because we don't want this task to run in parallel.
project.exec {
executable = inputFile.absolutePath
if (filter != null) {
args("--gtest_filter=${filter}")
}
args("--gtest_output=xml:${outputFile.absolutePath}")
when (sanitizer) {
SanitizerKind.THREAD -> {
environment("TSAN_OPTIONS", "suppressions=${tsanSuppressionsFile.absolutePath}")
}
else -> {} // no action required
}
}
// TODO: Better to use proper XML parsing.
var contents = outputFile.readText()
contents = contents.replace("<testsuite name=\"", "<testsuite name=\"${testName}.")
contents = contents.replace("classname=\"", "classname=\"${testName}.")
outputFileWithPrefixes.writeText(contents)
}
}
/**
* Returns a list of Clang -cc1 arguments (including -cc1 itself) that are used for bitcode compilation in Kotlin/Native.
*
* See also: [org.jetbrains.kotlin.backend.konan.BitcodeCompiler]
*/
private fun buildClangFlags(configurables: Configurables): List<String> = mutableListOf<String>().apply {
require(configurables is ClangFlags)
addAll(configurables.clangFlags)
addAll(configurables.clangNooptFlags)
val targetTriple = if (configurables is AppleConfigurables) {
configurables.targetTriple.withOSVersion(configurables.osVersionMin)
} else {
configurables.targetTriple
}
addAll(listOf("-triple", targetTriple.toString()))
}.toList()
@@ -0,0 +1,23 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.utils
/**
* Wrapper over [T]?.
*
* Useful for some generalized functions that you want to pass [T]? into, but they forbid nullable types.
* For example, [ObjectFactory.newInstance][org.gradle.api.model.ObjectFactory.newInstance] cannot be used
* to construct a type that has [T]? as a constructor argument.
*
* @property orNull get underlying value.
*/
data class Maybe<T>(val orNull: T?)
/**
* Construct [Maybe]<[T]> from [T]?.
*/
inline val <T> T?.asMaybe
get() = Maybe(this)
+29 -8
View File
@@ -4,6 +4,7 @@
*/
import org.jetbrains.kotlin.*
import org.jetbrains.kotlin.bitcode.CompileToBitcode
import org.jetbrains.kotlin.bitcode.CompileToBitcodeExtension
import org.jetbrains.kotlin.gradle.plugin.konan.tasks.KonanCacheTask
import org.jetbrains.kotlin.konan.properties.loadProperties
import org.jetbrains.kotlin.konan.properties.saveProperties
@@ -182,28 +183,48 @@ bitcode {
onlyIf { targetSupportsThreads(target) }
}
testsGroup("std_alloc_runtime_tests", listOf("main", "legacy_memory_manager", "strict", "std_alloc", "objc"))
testsGroup("std_alloc_runtime_tests") {
testedModules.addAll("main", "legacy_memory_manager", "strict", "std_alloc", "objc")
}
testsGroup("mimalloc_runtime_tests", listOf("main", "legacy_memory_manager", "strict", "mimalloc", "opt_alloc", "objc"))
testsGroup("mimalloc_runtime_tests") {
testedModules.addAll("main", "legacy_memory_manager", "strict", "mimalloc", "opt_alloc", "objc")
}
testsGroup("experimentalMM_mimalloc_runtime_tests", listOf("main", "experimental_memory_manager", "common_gc", "same_thread_ms_gc", "mimalloc", "opt_alloc", "objc"))
testsGroup("experimentalMM_mimalloc_runtime_tests") {
testedModules.addAll("main", "experimental_memory_manager", "common_gc", "same_thread_ms_gc", "mimalloc", "opt_alloc", "objc")
}
testsGroup("experimentalMM_std_alloc_runtime_tests", listOf("main", "experimental_memory_manager", "common_gc", "same_thread_ms_gc", "std_alloc", "objc"))
testsGroup("experimentalMM_std_alloc_runtime_tests") {
testedModules.addAll("main", "experimental_memory_manager", "common_gc", "same_thread_ms_gc", "std_alloc", "objc")
}
testsGroup("experimentalMM_cms_mimalloc_runtime_tests", listOf("main", "experimental_memory_manager", "common_gc", "concurrent_ms_gc", "mimalloc", "opt_alloc", "objc"))
testsGroup("experimentalMM_cms_mimalloc_runtime_tests") {
testedModules.addAll("main", "experimental_memory_manager", "common_gc", "concurrent_ms_gc", "mimalloc", "opt_alloc", "objc")
}
testsGroup("experimentalMM_cms_std_alloc_runtime_tests", listOf("main", "experimental_memory_manager", "common_gc", "concurrent_ms_gc", "std_alloc", "objc"))
testsGroup("experimentalMM_cms_std_alloc_runtime_tests") {
testedModules.addAll("main", "experimental_memory_manager", "common_gc", "concurrent_ms_gc", "std_alloc", "objc")
}
testsGroup("experimentalMM_noop_mimalloc_runtime_tests", listOf("main", "experimental_memory_manager", "common_gc", "noop_gc", "mimalloc", "opt_alloc", "objc"))
testsGroup("experimentalMM_noop_mimalloc_runtime_tests") {
testedModules.addAll("main", "experimental_memory_manager", "common_gc", "noop_gc", "mimalloc", "opt_alloc", "objc")
}
testsGroup("experimentalMM_noop_std_alloc_runtime_tests", listOf("main", "experimental_memory_manager", "common_gc", "noop_gc", "std_alloc", "objc"))
testsGroup("experimentalMM_noop_std_alloc_runtime_tests") {
testedModules.addAll("main", "experimental_memory_manager", "common_gc", "noop_gc", "std_alloc", "objc")
}
}
val hostRuntime by tasks.registering {
description = "Build all main runtime modules for host"
group = CompileToBitcodeExtension.BUILD_TASK_GROUP
dependsOn("${hostName}Runtime")
}
val hostRuntimeTests by tasks.registering {
description = "Runs all runtime tests for host"
group = CompileToBitcodeExtension.VERIFICATION_TASK_GROUP
dependsOn("${hostName}RuntimeTests")
}