From f26e5b5f02e980a8055e9ed7618732c67920a67f Mon Sep 17 00:00:00 2001 From: Vasily Levchenko Date: Wed, 3 May 2017 07:52:07 +0300 Subject: [PATCH] unit-tests: Create a simple test runner implementation --- .../org/jetbrains/kotlin/cli/bc/K2Native.kt | 3 + .../cli/bc/K2NativeCompilerArguments.kt | 7 +- .../backend/konan/KonanConfigurationKeys.kt | 2 + .../kotlin/backend/konan/KonanLower.kt | 4 + .../kotlin/backend/konan/KonanPhases.kt | 1 + .../kotlin/backend/konan/llvm/EntryPoint.kt | 16 ++- .../backend/konan/lower/TestProcessor.kt | 123 ++++++++++++++++++ .../backend/konan/lower/UnitProcessor.kt | 95 ++++++++++++++ backend.native/tests/build.gradle | 6 + backend.native/tests/testing/simple.kt | 98 ++++++++++++++ backend.native/tests/testing/smoke.kt | 14 ++ build.gradle | 4 + .../src/main/kotlin/konan/test/Launcher.kt | 21 +++ .../main/kotlin/konan/test/TestListener.kt | 8 ++ .../src/main/kotlin/konan/test/TestRunner.kt | 26 ++++ .../src/main/kotlin/konan/test/TestSuite.kt | 97 ++++++++++++++ .../src/main/kotlin/kotlin/test/Annotation.kt | 27 ++++ settings.gradle | 1 + 18 files changed, 550 insertions(+), 3 deletions(-) create mode 100644 backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/TestProcessor.kt create mode 100644 backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/UnitProcessor.kt create mode 100644 backend.native/tests/testing/simple.kt create mode 100644 backend.native/tests/testing/smoke.kt create mode 100644 runtime/src/main/kotlin/konan/test/Launcher.kt create mode 100644 runtime/src/main/kotlin/konan/test/TestListener.kt create mode 100644 runtime/src/main/kotlin/konan/test/TestRunner.kt create mode 100644 runtime/src/main/kotlin/konan/test/TestSuite.kt create mode 100644 runtime/src/main/kotlin/kotlin/test/Annotation.kt diff --git a/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt b/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt index 74cb4569740..70529028724 100644 --- a/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt +++ b/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt @@ -25,6 +25,7 @@ import org.jetbrains.kotlin.cli.common.CLICompiler import org.jetbrains.kotlin.cli.common.CLITool import org.jetbrains.kotlin.cli.common.ExitCode import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.ERROR +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.WARNING import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.config.CompilerConfiguration @@ -133,6 +134,8 @@ class K2Native : CLICompiler() { put(ENABLE_ASSERTIONS, arguments.enableAssertions) + put(GENERATE_TEST_RUNNER, arguments.generateTestRunner) + // We need to download dependencies only if we use them ( = there are files to compile). put(CHECK_DEPENDENCIES, if (configuration.kotlinSourceRoots.isNotEmpty()) { true diff --git a/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt b/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt index d5cd95e640b..3f606fbcc97 100644 --- a/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt +++ b/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt @@ -24,11 +24,14 @@ class K2NativeCompilerArguments : CommonCompilerArguments() { // Prepend them with a single dash. // Keep the list lexically sorted. + @Argument(value = "-enable_assertions", shortName = "-ea", description = "Enable runtime assertions in generated code") + var enableAssertions: Boolean = false + @Argument(value = "-g", description = "Enable emitting debug information") var debug: Boolean = false - @Argument(value = "-enable_assertions", shortName = "-ea", description = "Enable runtime assertions in generated code") - var enableAssertions: Boolean = false + @Argument(value = "-generate_test_runner", shortName = "-tr", description = "Produce a runner for unit tests") + var generateTestRunner: Boolean = false @Argument(value = "-includeBinary", shortName = "-ib", valueDescription = "", description = "Pack external binary within the klib") var includeBinaries: Array? = null diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt index 51291ef3fc2..132d01018fc 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt @@ -37,6 +37,8 @@ class KonanConfigKeys { = CompilerConfigurationKey.create("enable backend phases") val ENTRY: CompilerConfigurationKey = CompilerConfigurationKey.create("fully qualified main() name") + val GENERATE_TEST_RUNNER: CompilerConfigurationKey + = CompilerConfigurationKey.create("generate test runner") val INCLUDED_BINARY_FILES: CompilerConfigurationKey> = CompilerConfigurationKey.create("included binary file paths") val LIBRARY_FILES: CompilerConfigurationKey> diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanLower.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanLower.kt index 4be73f8d7eb..4181f2cef49 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanLower.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanLower.kt @@ -40,6 +40,10 @@ internal class KonanLower(val context: Context) { fun lowerModule(irModule: IrModuleFragment) { val phaser = PhaseManager(context) + //phaser.phase(KonanPhase.TEST_PROCESSOR) { + // TestProcessor(context).process(irModule) + //} + phaser.phase(KonanPhase.LOWER_SPECIAL_CALLS) { irModule.files.forEach(SpecialCallsLowering(context)::lower) } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanPhases.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanPhases.kt index 9fbc83213e4..cd1afae1857 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanPhases.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanPhases.kt @@ -28,6 +28,7 @@ enum class KonanPhase(val description: String, /* */ SERIALIZER("Serialize descriptor tree and inline IR bodies"), /* */ BACKEND("All backend"), /* ... */ LOWER("IR Lowering"), + /* ... ... */ TEST_PROCESSOR("Unit test processor"), // TODO: It is not a lowering. Move into a separate phase before lowering? /* ... ... */ LOWER_SPECIAL_CALLS("Special calls processing before inlining"), /* ... ... */ LOWER_INLINE_CONSTRUCTORS("Inline constructors transformation", LOWER_SPECIAL_CALLS), /* ... ... */ LOWER_INLINE("Functions inlining", LOWER_INLINE_CONSTRUCTORS, LOWER_SPECIAL_CALLS), diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/EntryPoint.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/EntryPoint.kt index f10b9582946..12b7c306bd6 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/EntryPoint.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/EntryPoint.kt @@ -20,6 +20,8 @@ import org.jetbrains.kotlin.backend.konan.reportCompilationError import org.jetbrains.kotlin.backend.konan.Context import org.jetbrains.kotlin.backend.konan.KonanConfigKeys import org.jetbrains.kotlin.backend.konan.descriptors.isArray +import org.jetbrains.kotlin.backend.konan.report +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.incremental.components.NoLookupLocation @@ -36,7 +38,18 @@ internal fun findMainEntryPoint(context: Context): FunctionDescriptor? { val config = context.config.configuration if (config.get(KonanConfigKeys.PRODUCE) != CompilerOutputKind.PROGRAM) return null - val entryPoint = FqName(config.get(KonanConfigKeys.ENTRY) ?: defaultEntryName) + val userEntryName = config.get(KonanConfigKeys.ENTRY) + val entryPoint: FqName + if (config.getBoolean(KonanConfigKeys.GENERATE_TEST_RUNNER)) { + entryPoint = FqName(testEntryName) + if (userEntryName != null) { + config.report(CompilerMessageSeverity.WARNING, + "Custom entry point is ignored if test runner is generated" + ) + } + } else { + entryPoint = FqName(userEntryName ?: defaultEntryName) + } val entryName = entryPoint.shortName() val packageName = entryPoint.parent() @@ -57,6 +70,7 @@ internal fun findMainEntryPoint(context: Context): FunctionDescriptor? { } private val defaultEntryName = "main" +private val testEntryName = "konan.test.main" private val defaultEntryPackage = FqName.ROOT diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/TestProcessor.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/TestProcessor.kt new file mode 100644 index 00000000000..e35d247b26d --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/TestProcessor.kt @@ -0,0 +1,123 @@ +package org.jetbrains.kotlin.backend.konan.lower + +import org.jetbrains.kotlin.backend.common.FileLoweringPass +import org.jetbrains.kotlin.backend.common.IrElementVisitorVoidWithContext +import org.jetbrains.kotlin.backend.konan.KonanBackendContext +import org.jetbrains.kotlin.backend.konan.descriptors.contributedMethods +import org.jetbrains.kotlin.backend.konan.llvm.symbolName +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol +import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid +import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid +import org.jetbrains.kotlin.name.FqName + +internal class TestProcessor (val context: KonanBackendContext): FileLoweringPass { + + // TODO: Replace with symbols + companion object { + val fqNameTestAnnotation = FqName("kotlin.test.Test") + val fqNameBeforeAnnotation = FqName("kotlin.test.Before") + val fqNameAfterAnnotation = FqName("kotlin.test.After") + val fqNameBeforeClassAnnotation = FqName("kotlin.test.BeforeClass") + val fqNameAfterClassAnnotation = FqName("kotlin.test.AfterClass") + val fqNameIgnoreAnnotation = FqName("kotlin.test.Ignore") + } + + private inner class TestClass(val clazz: IrClassSymbol) { + val testFunctions = mutableListOf() + val beforeFunctions = mutableListOf() + val afterFunctions = mutableListOf() + } + + private inner class TopLevelTestSuite() { + val testFunctions = mutableListOf() + val beforeFunctions = mutableListOf() + val afterFunctions = mutableListOf() + val beforeClassFunctions = mutableListOf() + val afterClassFunctions = mutableListOf() + } + + private inner class AnnotationCollector : IrElementVisitorVoid { + val testClasses = mutableMapOf() + val topLevelTestSuite = TopLevelTestSuite() + + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + + + +// override fun visitClass(declaration: IrClass) { +// val doNotprocess = declaration.descriptor.staticScope.contributedMethods.none { it.testFunction() } +// if (!doNotprocess) { +// testClasses.add(declaration.symbol) +// +// +// +// declaration.acceptChildrenVoid(object:IrElementVisitorVoid{ +// override fun visitElement(element: IrElement) { +// element.acceptChildrenVoid(this) +// } +// +// override fun visitFunction(declaration: IrFunction) { +// processFunction(declaration.descriptor) +// } +// }) +// } +// } + + override fun visitFunction(declaration: IrFunction) { + val descriptor = declaration.descriptor + val symbol = declaration.symbol + + // TODO: Make it smarter + //if (descriptor.test()) testFunctions.add(symbol) + //if (descriptor.before()) beforeFunctions.add(symbol) + //if (descriptor.after()) afterFunctions.add(symbol) + //if (descriptor.beforeClass()) beforeClassFunctions.add(symbol) + //if (descriptor.afterClass()) afterClassFunctions.add(symbol) + + + } + + } + + + override fun lower(irFile: IrFile) { + irFile.acceptChildrenVoid(AnnotationCollector()) + /** + * TODO: for all test classes generate registration... + */ + } + + private fun FunctionDescriptor.isTestFunction() = + annotations.any { annotation -> + hasTestAnnotation(annotation) + } + + + private fun hasTestAnnotation(annotation: AnnotationDescriptor): Boolean { + return annotation.fqName == fqNameTestAnnotation || + annotation.fqName == fqNameBeforeAnnotation || + annotation.fqName == fqNameAfterAnnotation || + annotation.fqName == fqNameBeforeClassAnnotation || + annotation.fqName == fqNameAfterClassAnnotation + } + + fun FunctionDescriptor.isAfter() = testAnnotationFqName(fqNameAfterAnnotation) + fun FunctionDescriptor.isBefore() = testAnnotationFqName(fqNameBeforeAnnotation) + fun FunctionDescriptor.isAfterClass() = testAnnotationFqName(fqNameAfterClassAnnotation) + fun FunctionDescriptor.isBeforeClass() = testAnnotationFqName(fqNameBeforeClassAnnotation) + fun FunctionDescriptor.isTest() = testAnnotationFqName(fqNameTestAnnotation) + + fun FunctionDescriptor.testAnnotationFqName(fqName:FqName) = annotations.any { it.fqName == fqNameTestAnnotation } + +} \ No newline at end of file diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/UnitProcessor.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/UnitProcessor.kt new file mode 100644 index 00000000000..7ee40f4f5cf --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/UnitProcessor.kt @@ -0,0 +1,95 @@ +package org.jetbrains.kotlin.backend.konan.lower + +import org.jetbrains.kotlin.backend.common.FileLoweringPass +import org.jetbrains.kotlin.backend.konan.KonanBackendContext +import org.jetbrains.kotlin.backend.konan.descriptors.contributedMethods +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid +import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid +import org.jetbrains.kotlin.name.FqName + +internal class UnitProcessor (val context: KonanBackendContext):FileLoweringPass { + val fqNameTestAnnotation = FqName("org.junit.Test") + val fqNameBeforeAnnotation = FqName("org.junit.Before") + val fqNameAfterAnnotation = FqName("org.junit.After") + val fqNameBeforeClassAnnotation = FqName("org.junit.BeforeClass") + val fqNameAfterClassAnnotation = FqName("org.junit.AfterClass") + + override fun lower(irFile: IrFile) { + val testFunction = mutableListOf() + val beforeFunction = mutableListOf() + val afterFunction = mutableListOf() + val beforeClassFunction = mutableListOf() + val afterClassFunction = mutableListOf() + val testClasses = mutableListOf() + irFile.acceptChildrenVoid(object: IrElementVisitorVoid { + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + + override fun visitClass(declaration: IrClass) { + val classDescriptor = declaration.descriptor + val doNotprocess = declaration.descriptor.staticScope.contributedMethods.none{it.junitFunction()} + if (!doNotprocess) { + testClasses.add(declaration.descriptor) + declaration.acceptChildrenVoid(object:IrElementVisitorVoid{ + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + + override fun visitFunction(declaration: IrFunction) { + processFunction(declaration.descriptor) + } + }) + } + } + + override fun visitFunction(declaration: IrFunction) { + processFunction(declaration.descriptor) + } + + private fun processFunction(descriptor: FunctionDescriptor) { + when { + descriptor.test() -> testFunction.add(descriptor) + descriptor.before() -> beforeFunction.add(descriptor) + descriptor.after() -> afterFunction.add(descriptor) + descriptor.beforeClass() -> beforeClassFunction.add(descriptor) + descriptor.afterClass() -> afterClassFunction.add(descriptor) + } + } + }) + /** + * TODO: for all test classes generate registration... + */ + + } + + private fun FunctionDescriptor.junitFunction() = + annotations.any { annotation -> + hasJunitAnnotation(annotation) + } + + + private fun hasJunitAnnotation(annotation: AnnotationDescriptor): Boolean { + return annotation.fqName == fqNameTestAnnotation && + annotation.fqName == fqNameBeforeAnnotation && + annotation.fqName == fqNameAfterAnnotation && + annotation.fqName == fqNameBeforeClassAnnotation && + annotation.fqName == fqNameAfterClassAnnotation + } + + fun FunctionDescriptor.after() = testAnnotationFqName(fqNameAfterAnnotation) + fun FunctionDescriptor.before() = testAnnotationFqName(fqNameBeforeAnnotation) + fun FunctionDescriptor.afterClass() = testAnnotationFqName(fqNameAfterClassAnnotation) + fun FunctionDescriptor.beforeClass() = testAnnotationFqName(fqNameBeforeClassAnnotation) + fun FunctionDescriptor.test() = testAnnotationFqName(fqNameTestAnnotation) + + fun FunctionDescriptor.testAnnotationFqName(fqName:FqName) = annotations.any { it.fqName == fqNameTestAnnotation } + +} \ No newline at end of file diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index b24fddb7785..2d43c27d7fe 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -1950,6 +1950,11 @@ task deserialized_members(type: LinkKonanTest) { source = "serialization/deserialize_members.kt" } +task testing_smoke(type: RunKonanTest) { + source = "testing/smoke.kt" + flags = [] // TODO: Add flag to generate test-runner +} + // Just check that the driver is able to produce runnable binaries. task driver0(type: RunDriverKonanTest) { goldValue = "Hello, world!\n" @@ -2121,3 +2126,4 @@ if (isMac()) { } } } + diff --git a/backend.native/tests/testing/simple.kt b/backend.native/tests/testing/simple.kt new file mode 100644 index 00000000000..efe702c2ad6 --- /dev/null +++ b/backend.native/tests/testing/simple.kt @@ -0,0 +1,98 @@ +import org.junit.* + + +@Test +fun simple() {} + +class SimpleTestA{ + companion object { + @BeforeClass + fun beforeClass() { + println("before class SimpleTestA") + } + + @AfterClass + fun afterClass() { + println("after class SimpleTestA") + } + } + @Before + fun before() { + println("SimpleTestA::before") + } + + @After + fun after() { + println("SimpleTestA::after") + } + + @Test + fun a() { + println("SimpleTestA::a") + } + + @Test + fun b() { + println("SimpleTestA::b") + } +} + +class SimpleTestB{ + companion object { + @BeforeClass + fun beforeClass() { + println("before class SimpleTestB") + } + + @AfterClass + fun afterClass() { + println("after class SimpleTestB") + } + } + + @Before + fun before() { + println("SimpleTestB::before") + } + + @After + fun after() { + println("SimpleTestB::after") + } + + @Test + fun a() { + println("SimpleTestB::a") + } + + @Test + fun b() { + println("SimpleTestB::b") + } + +} + + +// ---- This should be generated... +private val test = { + val objA = SimpleTestA() + + val objB = SimpleTestB() + + TestRunner.register(listOf( + TestSuite( + beforeClass = { SimpleTestA.beforeClass() }, + afterClass = { SimpleTestA.afterClass() }, + before = { objA.before() }, + after = { objA.after() }, + funcs = arrayOf({ objA.a() }, { objA.b() })), + TestSuite( + beforeClass = { SimpleTestB.beforeClass() }, + afterClass = { SimpleTestB.afterClass() }, + before = { objB.before() }, + after = { objB.after() }, + funcs = arrayOf({ objB.a() }, { objB.b() })) + + )) +}() + diff --git a/backend.native/tests/testing/smoke.kt b/backend.native/tests/testing/smoke.kt new file mode 100644 index 00000000000..b2817a9d4bc --- /dev/null +++ b/backend.native/tests/testing/smoke.kt @@ -0,0 +1,14 @@ +import kotlin.test.Test + +class A { + @Test + fun test() { + println("A.test") + } +} + +@Test +fun test() { + println("test") +} + diff --git a/build.gradle b/build.gradle index a6fc64f33c8..b0d3109c5d9 100644 --- a/build.gradle +++ b/build.gradle @@ -258,6 +258,10 @@ targetList.each { target -> rename("${target}Start.bc", 'start.bc') into("klib/stdlib/targets/$target/native") } + from(project(':runtime').file("build/${target}Test.bc")) { + rename("${target}Test.bc", 'test.bc') + into("klib/stdlib/targets/$target/native") + } if (target == 'wasm32') { from(project(':runtime').file('src/launcher/js')) { into('klib/stdlib/targets/wasm32/included') diff --git a/runtime/src/main/kotlin/konan/test/Launcher.kt b/runtime/src/main/kotlin/konan/test/Launcher.kt new file mode 100644 index 00000000000..5461f2c66d7 --- /dev/null +++ b/runtime/src/main/kotlin/konan/test/Launcher.kt @@ -0,0 +1,21 @@ +package konan.test + +class A { + fun test() { println("test") } +} + +fun getTestSuites(): Collection { + return listOf( + object: TestClass("A") { + override fun createInstance() = A() + + init { + registerTestCase(BasicTestCase("test", A::test)) + } + } + ) +} + +fun main(args:Array) { + TestRunner(getTestSuites()).run() +} diff --git a/runtime/src/main/kotlin/konan/test/TestListener.kt b/runtime/src/main/kotlin/konan/test/TestListener.kt new file mode 100644 index 00000000000..0739a38ec1c --- /dev/null +++ b/runtime/src/main/kotlin/konan/test/TestListener.kt @@ -0,0 +1,8 @@ +package konan.test + +interface TestListener { + fun pass(testCase: TestCase) + fun fail(testCase: TestCase, e: AssertionError) + fun error(testCase: TestCase, e: Throwable) + fun ignore(testCase: TestCase) +} \ No newline at end of file diff --git a/runtime/src/main/kotlin/konan/test/TestRunner.kt b/runtime/src/main/kotlin/konan/test/TestRunner.kt new file mode 100644 index 00000000000..c2d7aaa23b0 --- /dev/null +++ b/runtime/src/main/kotlin/konan/test/TestRunner.kt @@ -0,0 +1,26 @@ +package konan.test + +class TestRunner(suites: Collection = emptyList()) { + + object SimpleTestListener: TestListener { + override fun pass(testCase: TestCase) = println("Pass: $testCase") + override fun fail(testCase: TestCase, e: AssertionError) = println("Fail: $testCase (${e.message})") + override fun error(testCase: TestCase, e: Throwable) = println("Error: $testCase (${e.message})") + override fun ignore(testCase: TestCase) = println("Ignore: $testCase") + + } + + private val _suites = mutableListOf().apply { addAll(suites) } + val suites: Collection get() = _suites + + fun register(suite: TestSuite) = _suites.add(suite) + fun register(suites: Iterable) = _suites.addAll(suites) + fun register(vararg suites: TestSuite) = _suites.addAll(suites) + + fun run() { + suites.forEach { + it.run(SimpleTestListener) + } + } +} + diff --git a/runtime/src/main/kotlin/konan/test/TestSuite.kt b/runtime/src/main/kotlin/konan/test/TestSuite.kt new file mode 100644 index 00000000000..1420d106554 --- /dev/null +++ b/runtime/src/main/kotlin/konan/test/TestSuite.kt @@ -0,0 +1,97 @@ +package konan.test + +interface TestCase { + val name: String +} + +interface TestSuite { + val name: String + val testCases: Map + fun run(listener: TestListener) +} + +enum class FunctionKind { + BEFORE, + AFTER, + BEFORE_CLASS, + AFTER_CLASS +} + +abstract class AbstractTestSuite>(override val name: String): TestSuite { + override fun toString(): String = name + + inner class BasicTestCase(override val name: String, val testFunction: F): TestCase { + override fun toString(): String = name + } + + private val _testCases = mutableMapOf() + override val testCases: Map + get() = _testCases + + private val specialFunctions = mutableMapOf>() + private fun Map>.getFunctions(type: FunctionKind) = + specialFunctions.getOrPut(type) { mutableSetOf() } + + val before: Collection get() = specialFunctions.getFunctions(FunctionKind.BEFORE) + val after: Collection get() = specialFunctions.getFunctions(FunctionKind.AFTER) + + // TODO: Must be in companions. Support it. + val beforeClass: Collection get() = specialFunctions.getFunctions(FunctionKind.BEFORE_CLASS) + val afterClass: Collection get() = specialFunctions.getFunctions(FunctionKind.AFTER_CLASS) + + protected fun registerTestCase(testCase: BasicTestCase) = _testCases.put(testCase.name, testCase) + protected fun registerTestCases(testCases: Collection) = testCases.forEach { registerTestCase(it) } + protected fun registerTestCases(vararg testCases: BasicTestCase) = registerTestCases(testCases.toList()) + + protected fun registerFunction(type: FunctionKind, function: F) = + specialFunctions.getFunctions(type).add(function) + + protected abstract fun doBeforeClass() + protected abstract fun doAfterClass() + + protected abstract fun doTest(testCase: BasicTestCase) + + override fun run(listener: TestListener) { + doBeforeClass() + testCases.values.forEach { + try { + doTest(it) + // TODO: merge catches? + } catch (e: AssertionError) { + listener.fail(it, e) + } catch (e: Throwable) { + listener.error(it, e) + } + listener.pass(it) + } + doAfterClass() + } +} + +abstract class TestClass(name: String): AbstractTestSuite Unit>(name) { + + abstract fun createInstance(): T + + // TODO: What about companions? + override fun doBeforeClass() {} // = beforeClass.forEach { createInstance().it() } + override fun doAfterClass() {} // = afterClass.forEach { createInstance().it() } + + override fun doTest(testCase: BasicTestCase) { + val instance = createInstance() + val testFunction = testCase.testFunction + before.forEach { instance.it() } + instance.testFunction() + after.forEach { instance.it() } + } +} + +class TopLevelTestSuite(name: String): AbstractTestSuite<() -> Unit>(name) { + override fun doBeforeClass() = beforeClass.forEach { it() } + override fun doAfterClass() = afterClass.forEach { it() } + + override fun doTest(testCase: BasicTestCase) { + before.forEach { it() } + testCase.testFunction() + after.forEach { it() } + } +} \ No newline at end of file diff --git a/runtime/src/main/kotlin/kotlin/test/Annotation.kt b/runtime/src/main/kotlin/kotlin/test/Annotation.kt new file mode 100644 index 00000000000..8385425081c --- /dev/null +++ b/runtime/src/main/kotlin/kotlin/test/Annotation.kt @@ -0,0 +1,27 @@ +package kotlin.test + +// TODO: Move test code in a separate library. + +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.FUNCTION) +annotation class Test + +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.FUNCTION) +annotation class BeforeClass + +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.FUNCTION) +annotation class AfterClass + +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.FUNCTION) +annotation class Before + +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.FUNCTION) +annotation class After + +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +annotation class Ignore diff --git a/settings.gradle b/settings.gradle index 24b2f4efa1d..4e18753effc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,3 +28,4 @@ include ':backend.native:tests' include ':shared' include ':tools:kotlin-native-gradle-plugin' include ':utilities' +include ':ext:junit' \ No newline at end of file