Rewrite tests infrastructure to Kotlin.

Split build and run logic. Build tests with Konan gradle plugin.
Add property for konan plugin to control generation of run tasks.
This commit is contained in:
Pavel Punegov
2019-01-18 17:12:39 +03:00
committed by Pavel Punegov
parent 29e0dfc029
commit 83aa459357
9 changed files with 1691 additions and 1181 deletions
File diff suppressed because it is too large Load Diff
@@ -25,7 +25,7 @@ import java.nio.file.Paths
import java.util.function.Function
import java.util.regex.Pattern
abstract class KonanTest extends JavaExec {
abstract class OldKonanTest extends JavaExec {
public boolean inDevelopersRun = false
public String source
@@ -79,7 +79,7 @@ abstract class KonanTest extends JavaExec {
}
}
KonanTest() {
OldKonanTest() {
// We don't build the compiler if a custom dist path is specified.
if (!project.ext.useCustomDist) {
dependsOn(project.rootProject.tasks['dist'])
@@ -164,104 +164,6 @@ abstract class KonanTest extends JavaExec {
return sourceFiles
}
String createTextForHelpers() {
def coroutinesPackage = "kotlin.coroutines"
def emptyContinuationBody =
"""
|override fun resumeWith(result: Result<Any?>) { result.getOrThrow() }
""".stripMargin()
def handleResultContinuationBody = """
|override fun resumeWith(result: Result<T>) { x(result.getOrThrow()) }
""".stripMargin()
def handleExceptionContinuationBody = """
|override fun resumeWith(result: Result<Any?>) {
| val exception = result.exceptionOrNull() ?: return
| x(exception)
|}
""".stripMargin()
return """
|package helpers
|import $coroutinesPackage.*
|
|fun <T> handleResultContinuation(x: (T) -> Unit): Continuation<T> = object: Continuation<T> {
| override val context = EmptyCoroutineContext
| $handleResultContinuationBody
|}
|
|
|fun handleExceptionContinuation(x: (Throwable) -> Unit): Continuation<Any?> = object: Continuation<Any?> {
| override val context = EmptyCoroutineContext
| $handleExceptionContinuationBody
|}
|
|open class EmptyContinuation(override val context: CoroutineContext = EmptyCoroutineContext) : Continuation<Any?> {
| companion object : EmptyContinuation()
| $emptyContinuationBody
|}
|
|abstract class ContinuationAdapter<in T> : Continuation<T> {
| override val context: CoroutineContext = EmptyCoroutineContext
| override fun resumeWith(result: Result<T>) {
| if (result.isSuccess) {
| resume(result.getOrThrow())
| } else {
| resumeWithException(result.exceptionOrNull()!!)
| }
| }
|
| abstract fun resumeWithException(exception: Throwable)
| abstract fun resume(value: T)
|}
|class StateMachineCheckerClass {
| private var counter = 0
| var finished = false
|
| var proceed: () -> Unit = {}
| fun reset() {
| counter = 0
| finished = false
| proceed = {}
| }
|
| suspend fun suspendHere() = suspendCoroutine<Unit> { c ->
| counter++
| proceed = { c.resume(Unit) }
| }
|
| fun check(numberOfSuspensions: Int) {
| for (i in 1..numberOfSuspensions) {
| if (counter != i) error("Wrong state-machine generated: suspendHere called should be called exactly once in one state. Expected " + i + ", got " + counter)
| proceed()
| }
| if (counter != numberOfSuspensions)
| error("Wrong state-machine generated: suspendHere called should be called exactly once in one state. Expected " + numberOfSuspensions + ", got " + counter)
| if (finished) error("Wrong state-machine generated: it is finished early")
| proceed()
| if (!finished) error("Wrong state-machine generated: it is not finished yet")
| }
|}
|val StateMachineChecker = StateMachineCheckerClass()
|object CheckStateMachineContinuation: ContinuationAdapter<Unit>() {
| override val context: CoroutineContext
| get() = EmptyCoroutineContext
|
| override fun resume(value: Unit) {
| StateMachineChecker.proceed = {
| StateMachineChecker.finished = true
| }
| }
|
| override fun resumeWithException(exception: Throwable) {
| throw exception
| }
|}
""".stripMargin()
}
// TODO refactor
List<String> buildCompileList() {
def result = []
@@ -272,7 +174,7 @@ abstract class KonanTest extends JavaExec {
if (srcText.contains('// WITH_COROUTINES')) {
def coroutineHelpersFileName = "$outputDirectory/helpers.kt"
createFile(coroutineHelpersFileName, createTextForHelpers())
createFile(coroutineHelpersFileName, CoroutineTestUtilKt.createTextForHelpers(true))
result.add(coroutineHelpersFileName)
}
@@ -389,332 +291,7 @@ abstract class KonanTest extends JavaExec {
}
}
abstract class ExtKonanTest extends KonanTest {
ExtKonanTest() {
super()
}
@Override
String buildExePath() {
// a single executable for all tests
return "$outputDirectory/program.tr"
}
// The same as its super() version but doesn't create a new dir for each test
@Override
void createOutputDirectory() {
if (outputDirectory != null) {
return
}
def outputSourceSet = project.findProperty(getOutputSourceSetName())
if (outputSourceSet != null) {
outputDirectory = outputSourceSet.absolutePath
project.file(outputDirectory).mkdirs()
} else {
outputDirectory = getTemporaryDir().absolutePath
}
}
}
/**
* Builds tests with TestRunner enabled
*/
class BuildKonanTest extends ExtKonanTest {
public List<String> compileList
public List<String> excludeList
BuildKonanTest() {
super()
}
@Override
List<String> buildCompileList() {
assert compileList != null
// convert exclude list to paths
def excludeFiles = new ArrayList<String>()
excludeList.each { excludeFiles.add(project.file(it).absolutePath) }
// create list of tests to compile
def compileFiles = new ArrayList<String>()
compileList.each {
def file = project.file(it)
if (file.isDirectory()) {
file.eachFileRecurse {
if (it.isFile() && it.name.endsWith(".kt") && !excludeFiles.contains(it.absolutePath)) {
compileFiles.add(it.absolutePath)
}
}
} else {
compileFiles.add(file.absolutePath)
}
}
compileFiles
}
@Override
void compileTest(List<String> filesToCompile, String exe) {
flags = flags ?: []
// compile with test runner enabled
flags.add("-tr")
runCompiler(filesToCompile, exe, flags)
}
@TaskAction
@Override
void executeTest() {
// only build tests
createOutputDirectory()
def program = buildExePath()
compileTest(buildCompileList(), program)
}
}
/**
* Runs test built with Konan's TestRunner
*/
class RunKonanTest extends ExtKonanTest {
/**
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
*/
public def inDevelopersRun = true
public def buildTaskName = 'buildKonanTests'
public def runnerLogger = Logger.SILENT
public def useFilter = true
enum Logger {
GTEST,
TEAMCITY,
SIMPLE,
SILENT
}
@Inject
RunKonanTest() {
super()
dependsOn(buildTaskName)
}
RunKonanTest(def depends) {
buildTaskName = depends
dependsOn(buildTaskName)
}
@Override
void compileTest(List<String> filesToCompile, String exe) {
// tests should be already compiled
}
@TaskAction
@Override
void executeTest() {
arguments = arguments ?: []
// Print only test's output
arguments.add("--ktest_logger=" + runnerLogger.toString())
if (useFilter) {
arguments.add("--ktest_filter=" + convertToPattern(source))
}
super.executeTest()
}
private String convertToPattern(String source) {
return source.replace('/', '.')
.replace(".kt", "")
.concat(".*")
}
}
class RunStdlibTest extends RunKonanTest {
/**
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
*/
public def inDevelopersRun = false
public def statistics = new Statistics()
RunStdlibTest() {
super('buildKonanStdlibTests')
}
@Override
void executeTest() {
try {
super.executeTest()
} finally {
def output = out.toString("UTF-8")
// NOTE: these regexs assert that GTEST output format is used
def matcher = (output =~ ~/\[==========\] Running ([0-9]*) tests from ([0-9]*) test cases \. .*/)
def testsTotal = 0
if (matcher) {
testsTotal = matcher[0][1] as Integer
}
matcher = (output =~ ~/\[ PASSED \] ([0-9]*) tests\./)
if (matcher) {
def n = matcher[0][1] as Integer
statistics.pass(n)
}
matcher = (output =~ ~/\[ FAILED \] ([0-9]*) tests.*/)
if (matcher) {
def n = matcher[0][1] as Integer
statistics.fail(n)
}
use(KonanTestSuiteReportKt) {
if (statistics.total == 0) {
statistics.error(testsTotal != 0 ? testsTotal : 1)
}
}
}
}
}
/**
* Compiles and executes test as a standalone binary
*/
class RunStandaloneKonanTest extends KonanTest {
/**
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
*/
public def inDevelopersRun = true
void compileTest(List<String> filesToCompile, String exe) {
runCompiler(filesToCompile, exe, flags?:[])
}
}
// This is another way to run the compiler.
// Don't use this task for regular testing as
// project.exec + a shell script isolate the jvm
// from IDEA. Use the RunKonanTest instead.
class RunDriverKonanTest extends KonanTest {
/**
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
*/
public def inDevelopersRun = true
RunDriverKonanTest() {
super()
// We don't build the compiler if a custom konan.home path is specified.
if (!project.hasProperty("konan.home")) {
dependsOn(project.rootProject.tasks['cross_dist'])
}
}
void compileTest(List<String> filesToCompile, String exe) {
runCompiler(filesToCompile, exe, flags?:[])
}
protected void runCompiler(List<String> filesToCompile, String output, List<String> moreArgs) {
def log = new ByteArrayOutputStream()
project.exec {
commandLine konanc
args = ["-output", output,
*filesToCompile,
*moreArgs,
*project.globalTestArgs]
if (project.testTarget) {
args "-target", target.visibleName
}
if (enableKonanAssertions) {
args "-ea"
}
standardOutput = log
errorOutput = log
}
def logString = log.toString("UTF-8")
project.file("${output}.compilation.log").write(logString)
println(logString)
}
}
class RunInteropKonanTest extends KonanTest {
/**
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
*/
public def inDevelopersRun = true
private String interop
private NamedNativeInteropConfig interopConf
void setInterop(String value) {
this.interop = value
this.interopConf = project.kotlinNativeInterop[value]
this.interopConf.target = target.visibleName
this.dependsOn(this.interopConf.genTask)
}
void compileTest(List<String> filesToCompile, String exe) {
String interopBc = exe + "-interop.bc"
runCompiler([interopConf.generatedSrcDir.absolutePath], interopBc, ['-produce', 'library'])
String interopStubsBc = new File(interopConf.nativeLibsDir, interop + "stubs.bc").absolutePath
List<String> linkerArguments = interopConf.linkerOpts // TODO: add arguments from .def file
List<String> compilerArguments = ["-library", interopBc, "-native-library", interopStubsBc] +
linkerArguments.collectMany { ["-linker-options", it] }
runCompiler(filesToCompile, exe, compilerArguments)
}
}
class LinkKonanTest extends KonanTest {
/**
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
*/
public def inDevelopersRun = true
protected String lib
void compileTest(List<String> filesToCompile, String exe) {
def libname = "testklib"
def klib = "$outputDirectory/$libname"
runCompiler(lib, klib, ['-produce', 'library'] + ((flags != null) ? flags :[]))
runCompiler(filesToCompile, exe, ['-library', klib] + ((flags != null) ? flags :[]))
}
}
class DynamicKonanTest extends KonanTest {
/**
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
*/
public def inDevelopersRun = true
protected String cSource
void compileTest(List<String> filesToCompile, String exe) {
def libname = "testlib"
def dylib = "$outputDirectory/$libname"
def realExe = "${exe}.${target.family.exeSuffix}"
runCompiler(filesToCompile, dylib, ['-produce', 'dynamic'] + ((flags != null) ? flags :[]))
runClang([cSource], realExe, ['-I', outputDirectory, '-L', outputDirectory, '-l', libname])
}
void runClang(List<String> cSources, String output, List<String> moreArgs) {
def log = new ByteArrayOutputStream()
project.execKonanClang(project.target) {
workingDir outputDirectory
executable "clang"
args cSources
args '-o', output
args moreArgs
args "-Wl,-rpath,$outputDirectory"
standardOutput = log
errorOutput = log
}
def logString = log.toString("UTF-8")
project.file("${output}.compilation.log").write(logString)
println(logString)
}
}
class RunExternalTestGroup extends RunStandaloneKonanTest {
class RunExternalTestGroup extends OldKonanTest {
/**
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
*/
@@ -972,7 +549,6 @@ fun runTest() {
if (!findLinesWithPrefixesRemoved(text, "// JVM_TARGET:").isEmpty()) { return false }
return true
}
}
@Override
@@ -22,20 +22,20 @@ import org.gradle.api.Project
import org.gradle.process.ExecResult
import org.gradle.process.ExecSpec
import org.gradle.util.ConfigureUtil
import org.jetbrains.kotlin.konan.target.AppleConfigurables
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.konan.target.PlatformManager
import org.jetbrains.kotlin.konan.target.Xcode
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.file.Path
import java.nio.file.Paths
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
/**
* A replacement of the standard exec {}
* A replacement of the standard `exec {}`
* @see org.gradle.api.Project.exec
*/
interface ExecutorService {
@@ -47,8 +47,8 @@ interface ExecutorService {
* Creates an ExecutorService depending on a test target -Ptest_target
*/
fun create(project: Project): ExecutorService {
val platformManager = project.rootProject.findProperty("platformManager") as PlatformManager
val testTarget = platformManager.targetManager(project.findProperty("testTarget") as String?).target
val platformManager = project.platformManager
val testTarget = project.testTarget
val platform = platformManager.platform(testTarget)
val absoluteTargetToolchain = platform.absoluteTargetToolchain
val absoluteTargetSysRoot = platform.absoluteTargetSysRoot
@@ -95,7 +95,7 @@ fun create(project: Project): ExecutorService {
}
}
data class ProcessOutput(val stdOut: String, val stdErr: String, val exitCode: Int)
data class ProcessOutput(var stdOut: String, var stdErr: String, var exitCode: Int)
/**
* Runs process using a given executor.
@@ -128,6 +128,44 @@ fun runProcess(executor: (Action<in ExecSpec>) -> ExecResult?,
fun runProcess(executor: (Action<in ExecSpec>) -> ExecResult?,
executable: String, vararg args: String) = runProcess(executor, executable, args.toList())
/**
* Runs process using a given executor.
*
* @param executor a method that is able to run a given executable, e.g. ExecutorService::execute
* @param executable a process executable to be run
* @param args arguments for a process
* @param input an input string to be passed through the standard input stream
*/
fun runProcessWithInput(executor: (Action<in ExecSpec>) -> ExecResult?,
executable: String, args: List<String>, input: String) : ProcessOutput {
val outStream = ByteArrayOutputStream()
val errStream = ByteArrayOutputStream()
val inStream = ByteArrayInputStream(input.toByteArray())
val execResult = executor(Action {
it.executable = executable
it.args = args.toList()
it.standardOutput = outStream
it.errorOutput = errStream
it.isIgnoreExitValue = true
it.standardInput = inStream
})
checkNotNull(execResult)
val stdOut = outStream.toString("UTF-8")
val stdErr = errStream.toString("UTF-8")
return ProcessOutput(stdOut, stdErr, execResult.exitValue)
}
/**
* The [ExecutorService] being set in the given project.
* @throws IllegalStateException if there are no executor in the project.
*/
val Project.executor: ExecutorService
get() = this.convention.plugins["executor"] as? ExecutorService ?: throw IllegalStateException("Executor wasn't found")
/**
* Creates a new executor service with additional action [actionParameter] executed after the main one.
* The following is an example how to pass an environment parameter
@@ -142,7 +180,25 @@ fun ExecutorService.add(actionParameter: Action<in ExecSpec>) = object : Executo
}
/**
* Returns Project's process executor
* Executes the [executable] with the given [arguments]
* and checks that the program finished with zero exit code.
*/
fun Project.executeAndCheck(executable: Path, arguments: List<String> = emptyList()) {
val (stdOut, stdErr, exitCode) = runProcess(
executor = executor::execute,
executable = executable.toString(),
args = arguments
)
println("""
|stdout: $stdOut
|stderr: $stdErr
""".trimMargin())
check(exitCode == 0) { "Execution failed with exit code: $exitCode "}
}
/**
* Returns [project]'s process executor.
* @see Project.exec
*/
fun localExecutor(project: Project) = { a: Action<in ExecSpec> -> project.exec(a) }
@@ -31,19 +31,17 @@ open class FrameworkTest : DefaultTask() {
@Input
var fullBitcode: Boolean = false
private val testOutput: String by lazy {
project.file(project.property("testOutputFramework")!!).absolutePath
}
private val testOutput: String = project.testOutputFramework
override fun configure(config: Closure<*>): Task {
super.configure(config)
val target = project.testTarget().name
val target = project.testTarget.name
// set crossdist build dependency if custom konan.home wasn't set
if (!(project.property("useCustomDist") as Boolean)) {
setRootDependency("${target}CrossDist", "${target}CrossDistRuntime", "commonDistRuntime", "distCompiler")
}
check(::frameworkName.isInitialized, { "Framework name should be set" })
check(::frameworkName.isInitialized) { "Framework name should be set" }
dependsOn(project.tasks.getByName("compileKonan$frameworkName"))
return this
}
@@ -52,7 +50,7 @@ open class FrameworkTest : DefaultTask() {
@TaskAction
fun run() {
val frameworkParentDirPath = "$testOutput/$frameworkName/${project.testTarget().name}"
val frameworkParentDirPath = "$testOutput/$frameworkName/${project.testTarget.name}"
val frameworkPath = "$frameworkParentDirPath/$frameworkName.framework"
val frameworkBinaryPath = "$frameworkPath/$frameworkName"
validateBitcodeEmbedding(frameworkBinaryPath)
@@ -78,14 +76,14 @@ open class FrameworkTest : DefaultTask() {
listOf(provider.toString(), swiftMain)
val options = listOf("-g", "-Xlinker", "-rpath", "-Xlinker", frameworkParentDirPath, "-F", frameworkParentDirPath)
val testExecutable = Paths.get(testOutput, frameworkName, "swiftTestExecutable")
compileSwift(project, project.testTarget(), sources, options, testExecutable, fullBitcode)
compileSwift(project, project.testTarget, sources, options, testExecutable, fullBitcode)
runTest(testExecutable)
}
private fun runTest(testExecutable: Path) {
val target = project.testTarget()
val platform = project.platformManager().platform(target)
val target = project.testTarget
val platform = project.platformManager.platform(target)
val configs = platform.configurables as AppleConfigurables
val swiftPlatform = when (target) {
KonanTarget.IOS_X64 -> "iphonesimulator"
@@ -114,7 +112,7 @@ open class FrameworkTest : DefaultTask() {
|stdout: $stdOut
|stderr: $stdErr
""".trimMargin())
check(exitCode == 0, { "Execution failed with exit code: $exitCode "})
check(exitCode == 0) { "Execution failed with exit code: $exitCode "}
}
private fun validateBitcodeEmbedding(frameworkBinary: String) {
@@ -122,8 +120,8 @@ open class FrameworkTest : DefaultTask() {
if (!fullBitcode) {
return
}
val testTarget = project.testTarget()
val configurables = project.platformManager().platform(testTarget).configurables as AppleConfigurables
val testTarget = project.testTarget
val configurables = project.platformManager.platform(testTarget).configurables as AppleConfigurables
val bitcodeBuildTool = "${configurables.absoluteAdditionalToolsDir}/bin/bitcode-build-tool"
val ldPath = "${configurables.absoluteTargetToolchain}/usr/bin/ld"
@@ -0,0 +1,437 @@
/*
* Copyright 2010-2019 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
import groovy.lang.Closure
import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction
import org.gradle.language.base.plugins.LifecycleBasePlugin
import org.gradle.process.ExecSpec
import org.jetbrains.kotlin.gradle.tasks.KotlinTest
import java.io.File
import java.io.ByteArrayOutputStream
import java.util.regex.Pattern
import org.jetbrains.kotlin.konan.target.HostManager
abstract class KonanTest : DefaultTask() {
enum class Logger {
EMPTY, // Built without test runner
GTEST, // Google test log output
TEAMCITY, // TeamCity log output
SIMPLE, // Prints simple messages of passed/failed tests
SILENT // Prints no log of passed/failed tests
}
var disabled: Boolean
get() = !enabled
set(value) { enabled = !value }
/**
* Test output directory. Used to store processed sources and binary artifacts.
*/
abstract val outputDirectory: String
/**
* Test logger to be used for the test built with TestRunner (`-tr` option).
*/
abstract var testLogger: Logger
/**
* Test executable arguments.
*/
@Input
var arguments = mutableListOf<String>()
/**
* Test executable.
*/
abstract val executable: String
/**
* Test source.
*/
lateinit var source: String
/**
* Sets test filtering to choose the exact test in the executable built with TestRunner.
*/
@Input
var useFilter = true
/**
* An action to be executed before the build.
* As this run task comes after the build task all actions for doFirst
* should be done before the build and not run.
*/
@Input @Optional
var doBefore: Action<in Task>? = null
@Suppress("UnstableApiUsage")
override fun configure(config: Closure<*>): Task {
super.configure(config)
// Set Gradle properties for the better navigation
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Kotlin/Native test infrastructure task"
if (testLogger != Logger.EMPTY) {
arguments.add("--ktest_logger=$testLogger")
}
if (useFilter && ::source.isInitialized) {
arguments.add("--ktest_filter=${source.convertToPattern()}")
}
this.dependsOnDist()
return this
}
@TaskAction
open fun run() = project.executeAndCheck(project.file(executable).toPath(), arguments)
// Converts to runner's pattern
private fun String.convertToPattern() = this.replace('/', '.').replace(".kt", "") + (".*")
internal fun ProcessOutput.print(prepend: String = "") {
if (project.verboseTest)
println(prepend + """
|stdout:
|$stdOut
|stderr:
|$stdErr
|exit code: $exitCode
""".trimMargin())
}
}
/**
* Create a test task of the given type. Supports configuration with Closure passed form build.gradle file.
*/
fun <T: KonanTest> Project.createTest(name: String, type: Class<T>, config: Closure<*>): T =
project.tasks.create(name, type).apply {
// Apply closure set in build.gradle to get all parameters.
this.configure(config)
if (enabled) {
// Configure test task.
val target = project.testTarget
// If run task depends on something, compile task should also depend on this.
val compileTask = project.tasks.getByName("compileKonan${name.capitalize()}${target.name.capitalize()}")
compileTask.sameDependenciesAs(this)
// Run task should depend on compile task
this.dependsOn(compileTask)
if (doBefore != null) compileTask.doFirst(doBefore!!)
compileTask.enabled = enabled
}
}
/**
* Task to run tests compiled with TestRunner.
* Runs tests with GTEST output and parses it to create statistics info
*/
open class KonanGTest : KonanTest() {
override val outputDirectory = "${project.testOutputStdlib}/$name"
// Use GTEST logger to parse test results later
override var testLogger = Logger.GTEST
override val executable: String
get() = "$outputDirectory/${project.testTarget.name}/$name.${project.testTarget.family.exeSuffix}"
var statistics = Statistics()
@TaskAction
override fun run() = runProcess(
executor = project.executor::execute,
executable = executable,
args = arguments
).let {
parse(it.stdOut)
it.print()
check(it.exitCode == 0) { "Test $executable exited with ${it.exitCode}" }
}
private fun parse(output: String) = statistics.apply {
Pattern.compile("\\[ PASSED ] ([0-9]*) tests\\.").matcher(output)
.apply { if (find()) pass(group(1).toInt()) }
Pattern.compile("\\[ FAILED ] ([0-9]*) tests.*").matcher(output)
.apply { if (find()) fail(group(1).toInt()) }
if (total == 0) {
// No test were run. Try to find if we've tried to run something
this.error(Pattern.compile("\\[={10}] Running ([0-9]*) tests from ([0-9]*) test cases\\..*")
.matcher(output)
.run { if (find()) group(1).toInt() else 1 })
}
}
}
/**
* Task to run tests built into a single predefined binary named `localTest`.
* Note: this task should depend on task that builds a test binary.
*/
open class KonanLocalTest : KonanTest() {
override val outputDirectory = project.testOutputLocal
// local tests built into a single binary with the known name
override val executable: String
get() = "$outputDirectory/${project.testTarget.name}/localTest.${project.testTarget.family.exeSuffix}"
override var testLogger = Logger.SILENT
@Input @Optional
var expectedExitStatus = 0
/**
* Should this test fail or not.
*/
@Input @Optional
var expectedFail = false
/**
* Used to validate output as a gold value.
*/
@Input @Optional
var goldValue: String? = null
/**
* Checks test's output against gold value and returns true if the output matches the expectation.
*/
@Input @Optional
var outputChecker: (String) -> Boolean = { str -> goldValue == null || goldValue == str }
/**
* Input test data to be passed to process' stdin.
*/
@Input @Optional
var testData: String? = null
/**
* Should compiler message be read and validated with output checker or gold value.
*/
@Input @Optional
var compilerMessages = false
@Input @Optional
var multiRuns = false
@Input @Optional
var multiArguments: List<List<String>>? = null
@TaskAction
override fun run() {
val times = if (multiRuns && multiArguments != null) multiArguments!!.size else 1
var output = ProcessOutput("", "", 0)
for (i in 1..times) {
val args = arguments + (multiArguments?.get(i - 1) ?: emptyList())
output += if (testData != null)
runProcessWithInput(project.executor::execute, executable, args, testData!!)
else
runProcess(project.executor::execute, executable, args)
}
if (compilerMessages) {
// TODO: as for now it captures output only in the driver task.
// It should capture output from the build task using Gradle's LoggerManager and LoggerOutput
val compilationLog = project.file("$executable.compilation.log").readText()
output.stdOut = compilationLog + output.stdOut
}
output.check()
output.print()
}
private operator fun ProcessOutput.plus(other: ProcessOutput) = ProcessOutput(
stdOut + other.stdOut,
stdErr + other.stdErr,
exitCode + other.exitCode)
private fun ProcessOutput.check() {
val exitCodeMismatch = exitCode != expectedExitStatus
if (exitCodeMismatch) {
val message = "Expected exit status: $expectedExitStatus, actual: $exitCode"
check(expectedFail) { """
|Test failed. $message
|stdout:
|$stdOut
|stderr:
|$stdErr
""".trimMargin()
}
println("Expected failure. $message")
}
val result = stdOut + stdErr
val goldValueMismatch = !outputChecker(result.replace(System.lineSeparator(), "\n"))
if (goldValueMismatch) {
val message = if (goldValue != null)
"Expected output: $goldValue, actual output: $result"
else
"Actual output doesn't match with output checker: $result"
check(expectedFail) { "Test failed. $message" }
println("Expected failure. $message")
}
check ((exitCodeMismatch || goldValueMismatch) || !expectedFail) { """
|Unexpected pass:
| * exit code mismatch: $exitCodeMismatch
| * gold value mismatch: $goldValueMismatch
| * expected fail: $expectedFail
""".trimMargin()
}
}
}
/**
* Executes a standalone tests provided with either @param executable or by the tasks @param name.
* The executable itself should be built by the konan plugin.
*/
open class KonanStandaloneTest : KonanLocalTest() {
init {
useFilter = false
}
override val outputDirectory: String
get() = "${project.testOutputLocal}/$name"
override var testLogger = Logger.EMPTY
override val executable: String
get() = "$outputDirectory/${project.testTarget.name}/$name.${project.testTarget.family.exeSuffix}"
@Input @Optional
var enableKonanAssertions = true
/**
* Compiler flags used to build a test.
*/
var flags: List<String> = listOf()
get() = if (enableKonanAssertions) field + "-ea" else field
fun getSources() = buildCompileList(outputDirectory)
}
/**
* This is another way to run the konanc compiler. It runs a konanc shell script.
*
* @note This task is not intended for regular testing as project.exec + a shell script isolate the jvm from IDEA.
* @see KonanLocalTest to be used as a regular task.
*/
open class KonanDriverTest : KonanStandaloneTest() {
override fun configure(config: Closure<*>): Task {
super.configure(config)
doBefore?.let { doFirst(it) }
return this
}
@TaskAction
override fun run() {
konan()
super.run()
}
private fun konan() {
val dist = project.rootProject.file(project.findProperty("org.jetbrains.kotlin.native.home") ?:
project.findProperty("konan.home") ?: "dist")
val konancDriver = if (HostManager.hostIsMingw) "konanc.bat" else "konanc"
val konanc = File("${dist.canonicalPath}/bin/$konancDriver").absolutePath
File(executable).parentFile.mkdirs()
val args = mutableListOf("-output", executable).apply {
if (project.testTarget != HostManager.host) {
add("-target")
add(project.testTarget.visibleName)
}
addAll(getSources())
addAll(flags)
addAll(project.globalTestArgs)
}
// run konanc compiler locally
runProcess(localExecutor(project), konanc, args).let {
it.print("Konanc compiler execution:")
project.file("$executable.compilation.log").run {
writeText(it.stdOut)
writeText(it.stdErr)
}
check(it.exitCode == 0) { "Compiler failed with exit code ${it.exitCode}" }
}
}
}
open class KonanInteropTest : KonanStandaloneTest() {
/**
* Name of the interop library
*/
@Input
lateinit var interop: String
}
open class KonanLinkTest : KonanStandaloneTest() {
@Input
lateinit var lib: String
}
/**
* Test task to check a library built by `-produce dynamic`.
* C source code should contain `testlib` as a reference to a testing library.
* It will be replaced then by the actual library name.
*/
open class KonanDynamicTest : KonanStandaloneTest() {
/**
* File path to the C source.
*/
@Input
lateinit var cSource: String
@TaskAction
override fun run() {
clang()
super.run()
}
// Replace testlib_api.h and all occurrences of the testlib with the actual name of the test
private fun processCSource(): String {
val sourceFile = File(cSource)
val res = sourceFile.readText()
.replace("#include \"testlib_api.h\"", "#include \"lib${name}_api.h\"")
.replace("testlib", "lib${name}")
val newFileName = "$outputDirectory/${sourceFile.name}"
println(newFileName)
File(newFileName).run {
createNewFile()
writeText(res)
}
return newFileName
}
private fun clang() {
val log = ByteArrayOutputStream()
val plugin = project.convention.getPlugin(ExecClang::class.java)
val execResult = plugin.execKonanClang(project.testTarget, Action<ExecSpec> {
it.workingDir = File(outputDirectory)
it.executable = "clang"
val artifactsDir = "$outputDirectory/${project.testTarget}"
it.args = listOf(processCSource(),
"-o", executable,
"-I", artifactsDir,
"-L", artifactsDir,
"-l", name,
"-Wl,-rpath,$artifactsDir")
it.standardOutput = log
it.errorOutput = log
it.isIgnoreExitValue = true
})
log.toString("UTF-8").also {
project.file("$executable.compilation.log").writeText(it)
println(it)
}
execResult.assertNormalExitValue()
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2010-2019 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
import java.nio.file.Paths
import java.util.regex.Pattern
/**
* Creates files from the given source file that may contain different test directives.
*
* @return list of file names to be compiled
*/
fun KonanTest.buildCompileList(outputDirectory: String): List<String> {
val result = mutableListOf<String>()
val srcFile = project.file(source)
// Remove diagnostic parameters in external tests.
val srcText = srcFile.readText().replace(Regex("<!.*?!>(.*?)<!>")) { match -> match.groupValues[1] }
if (srcText.contains("// WITH_COROUTINES")) {
val coroutineHelpersFileName = "$outputDirectory/helpers.kt"
createFile(coroutineHelpersFileName, createTextForHelpers(true))
result.add(coroutineHelpersFileName)
}
val filePattern = Pattern.compile("(?m)// *FILE: *(.*)")
val matcher = filePattern.matcher(srcText)
if (!matcher.find()) {
// There is only one file in the input
val filePath = "$outputDirectory/${srcFile.name}"
registerKtFile(result, filePath, srcText)
} else {
// There are several files
var processedChars = 0
while (true) {
val filePath = "$outputDirectory/${matcher.group(1)}"
val start = processedChars
val nextFileExists = matcher.find()
val end = if (nextFileExists) matcher.start() else srcText.length
val fileText = srcText.substring(start, end)
processedChars = end
registerKtFile(result, filePath, fileText)
if (!nextFileExists) break
}
}
return result
}
internal fun createFile(file: String, text: String) = Paths.get(file).run {
parent.toFile()
.takeUnless { it.exists() }
?.mkdirs()
toFile().writeText(text)
}
internal fun registerKtFile(sourceFiles: MutableList<String>, newFilePath: String, newFileContent: String) {
createFile(newFilePath, newFileContent)
if (newFilePath.endsWith(".kt")) {
sourceFiles.add(newFilePath)
}
}
@@ -1,11 +1,16 @@
/*
* Copyright 2010-2019 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
import org.gradle.api.Project
import org.gradle.api.Task
import org.jetbrains.kotlin.konan.target.AppleConfigurables
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.konan.target.PlatformManager
import java.io.FileInputStream
import java.io.IOException
import java.io.File
@@ -16,25 +21,108 @@ import java.util.Base64
import org.jetbrains.report.json.*
import java.nio.file.Path
//region Project properties.
val Project.platformManager
get() = findProperty("platformManager") as PlatformManager
val Project.testTarget
get() = findProperty("target") as KonanTarget
val Project.verboseTest
get() = hasProperty("test_verbose")
val Project.testOutputLocal
get() = (findProperty("testOutputLocal") as File).toString()
val Project.testOutputStdlib
get() = (findProperty("testOutputStdlib") as File).toString()
val Project.testOutputFramework
get() = (findProperty("testOutputFramework") as File).toString()
@Suppress("UNCHECKED_CAST")
val Project.globalTestArgs: List<String>
get() = with(findProperty("globalTestArgs")) {
if (this is Array<*>) this.toList() as List<String>
else this as List<String>
}
//endregion
fun Project.platformManager() = findProperty("platformManager") as PlatformManager
fun Project.testTarget() = findProperty("target") as KonanTarget
/**
* Ad-hoc signing of the specified path
* Ad-hoc signing of the specified path.
*/
fun codesign(project: Project, path: String) {
check(HostManager.hostIsMac, { "Apple specific code signing" })
check(HostManager.hostIsMac) { "Apple specific code signing" }
val (stdOut, stdErr, exitCode) = runProcess(executor = localExecutor(project), executable = "/usr/bin/codesign",
args = listOf("--verbose", "-s", "-", path))
check(exitCode == 0, {
"""
|Codesign failed with exitCode: $exitCode
|stdout: $stdOut
|stderr: $stdErr
""".trimMargin()
})
check(exitCode == 0) { """
|Codesign failed with exitCode: $exitCode
|stdout: $stdOut
|stderr: $stdErr
""".trimMargin()
}
}
/**
* Creates a list of file paths to be compiled from the given [compile] list with regard to [exclude] list.
*/
fun Project.getFilesToCompile(compile: List<String>, exclude: List<String>): List<String> {
// convert exclude list to paths
val excludeFiles = exclude.map { project.file(it).absolutePath }.toList()
// create list of tests to compile
return compile.flatMap { f ->
project.file(f)
.walk()
.filter { it.isFile && it.name.endsWith(".kt") && !excludeFiles.contains(it.absolutePath) }
.map(File::getAbsolutePath)
.asIterable()
}
}
//region Task dependency.
fun Project.dependsOnDist(taskName: String) {
project.tasks.getByName(taskName).dependsOnDist()
}
fun Task.dependsOnDist() {
val rootTasks = project.rootProject.tasks
// We don't build the compiler if a custom dist path is specified.
if (!(project.findProperty("useCustomDist") as Boolean)) {
dependsOn(rootTasks.getByName("dist"))
val target = project.testTarget
if (target != HostManager.host) {
// if a test_target property is set then tests should depend on a crossDist
// otherwise runtime components would not be build for a target.
dependsOn(rootTasks.getByName("${target.name}CrossDist"))
}
}
}
/**
* Sets the same dependencies for the receiver task from the given [task]
*/
fun String.sameDependenciesAs(task: Task) {
val t = task.project.tasks.getByName(this)
t.sameDependenciesAs(task)
}
/**
* Sets the same dependencies for the receiver task from the given [task]
*/
fun Task.sameDependenciesAs(task: Task) {
val dependencies = task.dependsOn.toList() // save to the list, otherwise it will cause cyclic dependency.
this.dependsOn(dependencies)
}
//endregion
// Run command line from string.
fun Array<String>.runCommand(workingDir: File = File("."),
timeoutAmount: Long = 60,
@@ -0,0 +1,138 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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
// This is almost a full copy of kotlin/compiler/tests-common/tests/org/jetbrains/kotlin/coroutineTestUtil.kt
// TODO: get it automatically as a dependency
fun createTextForHelpers(isReleaseCoroutines: Boolean): String {
val coroutinesPackage = "kotlin.coroutines"
val emptyContinuationBody =
if (isReleaseCoroutines)
"""
|override fun resumeWith(result: Result<Any?>) {
| result.getOrThrow()
|}
""".trimMargin()
else
"""
|override fun resume(data: Any?) {}
|override fun resumeWithException(exception: Throwable) { throw exception }
""".trimMargin()
val handleResultContinuationBody =
if (isReleaseCoroutines)
"""
|override fun resumeWith(result: Result<T>) {
| x(result.getOrThrow())
|}
""".trimMargin()
else
"""
|override fun resumeWithException(exception: Throwable) {
| throw exception
|}
|
|override fun resume(data: T) = x(data)
""".trimMargin()
val handleExceptionContinuationBody =
if (isReleaseCoroutines)
"""
|override fun resumeWith(result: Result<Any?>) {
| result.exceptionOrNull()?.let(x)
|}
""".trimMargin()
else
"""
|override fun resumeWithException(exception: Throwable) {
| x(exception)
|}
|
|override fun resume(data: Any?) {}
""".trimMargin()
val continuationAdapterBody =
if (isReleaseCoroutines)
"""
|override fun resumeWith(result: Result<T>) {
| if (result.isSuccess) {
| resume(result.getOrThrow())
| } else {
| resumeWithException(result.exceptionOrNull()!!)
| }
|}
|
|abstract fun resumeWithException(exception: Throwable)
|abstract fun resume(value: T)
""".trimMargin()
else
""
return """
|package helpers
|import $coroutinesPackage.*
|
|fun <T> handleResultContinuation(x: (T) -> Unit): Continuation<T> = object: Continuation<T> {
| override val context = EmptyCoroutineContext
| $handleResultContinuationBody
|}
|
|
|fun handleExceptionContinuation(x: (Throwable) -> Unit): Continuation<Any?> = object: Continuation<Any?> {
| override val context = EmptyCoroutineContext
| $handleExceptionContinuationBody
|}
|
|open class EmptyContinuation(override val context: CoroutineContext = EmptyCoroutineContext) : Continuation<Any?> {
| companion object : EmptyContinuation()
| $emptyContinuationBody
|}
|
|abstract class ContinuationAdapter<in T> : Continuation<T> {
| override val context: CoroutineContext = EmptyCoroutineContext
| $continuationAdapterBody
|}
|class StateMachineCheckerClass {
| private var counter = 0
| var finished = false
|
| var proceed: () -> Unit = {}
|
| suspend fun suspendHere() = suspendCoroutine<Unit> { c ->
| counter++
| proceed = { c.resume(Unit) }
| }
|
| fun check(numberOfSuspensions: Int) {
| for (i in 1..numberOfSuspensions) {
| if (counter != i) error("Wrong state-machine generated: suspendHere called should be called exactly once in one state. Expected " + i + ", got " + counter)
| proceed()
| }
| if (counter != numberOfSuspensions)
| error("Wrong state-machine generated: suspendHere called should be called exactly once in one state. Expected " + numberOfSuspensions + ", got " + counter)
| if (finished) error("Wrong state-machine generated: it is finished early")
| proceed()
| if (!finished) error("Wrong state-machine generated: it is not finished yet")
| }
|}
|val StateMachineChecker = StateMachineCheckerClass()
|object CheckStateMachineContinuation: ContinuationAdapter<Unit>() {
| override val context: CoroutineContext
| get() = EmptyCoroutineContext
|
| override fun resume(value: Unit) {
| StateMachineChecker.proceed = {
| StateMachineChecker.finished = true
| }
| }
|
| override fun resumeWithException(exception: Throwable) {
| throw exception
| }
|}
""".trimMargin()
}
@@ -270,7 +270,7 @@ open class KonanCompileProgramTask: KonanCompileTask() {
// Create tasks to run supported executables.
override fun init(config: KonanBuildingConfig<*>, destinationDir: File, artifactName: String, target: KonanTarget) {
super.init(config, destinationDir, artifactName, target)
if (!isCrossCompile) {
if (!isCrossCompile && !project.hasProperty("konanNoRun")) {
runTask = project.tasks.create("run${artifactName.capitalize()}", Exec::class.java).apply {
group = "run"
dependsOn(this@KonanCompileProgramTask)