From eaad75fd52ac4a0b0deca02d1aaa9b4872c4dcfb Mon Sep 17 00:00:00 2001 From: Alexander Shabalin Date: Sat, 25 Jun 2022 09:55:27 +0000 Subject: [PATCH] [K/N] Refactor runtime testing tasks Merge-request: KT-MR-6360 Merged-by: Alexander Shabalin --- .../kotlin/bitcode/CompileToBitcodePlugin.kt | 191 +++++++----- .../kotlin/cpp/CompileToExecutable.kt | 278 ++++++++++++++++++ .../org/jetbrains/kotlin/cpp/RunGTest.kt | 133 +++++++++ .../kotlin/testing/native/NativeTest.kt | 271 ----------------- .../org/jetbrains/kotlin/utils/Maybe.kt | 23 ++ kotlin-native/runtime/build.gradle.kts | 37 ++- 6 files changed, 588 insertions(+), 345 deletions(-) create mode 100644 kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/CompileToExecutable.kt create mode 100644 kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/RunGTest.kt delete mode 100644 kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/NativeTest.kt create mode 100644 kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/utils/Maybe.kt diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/bitcode/CompileToBitcodePlugin.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/bitcode/CompileToBitcodePlugin.kt index ffeb4b04524..0fe803b3eae 100644 --- a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/bitcode/CompileToBitcodePlugin.kt +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/bitcode/CompileToBitcodePlugin.kt @@ -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 +private abstract class CompileTestsSemaphore : BuildService + /** * A plugin creating extensions to compile */ @@ -42,6 +50,21 @@ open class CompileToBitcodeExtension @Inject constructor(val project: Project) { private val compilationDatabase = project.extensions.getByType() private val execClang = project.extensions.getByType() + private val platformManager = project.extensions.getByType() + + // googleTestExtension is only used if testsGroup is used. + private val googleTestExtension by lazy { project.extensions.getByType() } + + // 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() ?: 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 = 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, + ) { + val sanitizer + get() = _sanitizer.orNull + abstract val testedModules: ListProperty + abstract val testSupportModules: ListProperty + abstract val testLauncherModule: Property + } + private fun createTestTask( - project: Project, - testName: String, - testedTaskNames: List, - 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("${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(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, + action: Action, ) { - 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 = 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 -> "" + } } } diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/CompileToExecutable.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/CompileToExecutable.kt new file mode 100644 index 00000000000..eec9c04b438 --- /dev/null +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/CompileToExecutable.kt @@ -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 { + 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 + val clangFlags: ListProperty + val linkCommands: ListProperty> + val platformManager: Property + } + + @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() + + /** + * Target for which to compile. + */ + @get:Input + abstract val target: Property + + /** + * Sanitizer for which to compile. + */ + @get:Input + @get:Optional + abstract val sanitizer: Property + + // 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 + + /** + * 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 + + /** + * Extra args to the linker. + */ + @get:Input + abstract val linkerArgs: ListProperty + + @get:Input + protected val linkCommands: Provider>> = 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 = mutableListOf().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() diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/RunGTest.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/RunGTest.kt new file mode 100644 index 00000000000..0bb370bffed --- /dev/null +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/RunGTest.kt @@ -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 { + interface Parameters : WorkParameters { + val testName: Property + val executable: RegularFileProperty + val reportFile: RegularFileProperty + val reportFileUnprocessed: RegularFileProperty + val filter: Property + 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(" + + /** + * 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 + + /** + * 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) + } + } +} diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/NativeTest.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/NativeTest.kt deleted file mode 100644 index 513763953f4..00000000000 --- a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/NativeTest.kt +++ /dev/null @@ -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 - var llvmLinkOutputFile: File - var compilerOutputFile: File - var targetName: String - var compilerArgs: List - var linkCommands: List> - val platformManager: Property -} - -abstract class CompileNativeTestJob : WorkAction { - @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() - - @Input - val linkerArgs = mutableListOf() - - @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> - 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(" = mutableListOf().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() \ No newline at end of file diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/utils/Maybe.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/utils/Maybe.kt new file mode 100644 index 00000000000..7b86dde23bd --- /dev/null +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/utils/Maybe.kt @@ -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(val orNull: T?) + +/** + * Construct [Maybe]<[T]> from [T]?. + */ +inline val T?.asMaybe + get() = Maybe(this) diff --git a/kotlin-native/runtime/build.gradle.kts b/kotlin-native/runtime/build.gradle.kts index 146c6d33049..f352c96ac69 100644 --- a/kotlin-native/runtime/build.gradle.kts +++ b/kotlin-native/runtime/build.gradle.kts @@ -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") }