unit-tests: Create a simple test runner implementation

This commit is contained in:
Vasily Levchenko
2017-05-03 07:52:07 +03:00
committed by ilmat192
parent 77195e8e90
commit f26e5b5f02
18 changed files with 550 additions and 3 deletions
@@ -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<K2NativeCompilerArguments>() {
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
@@ -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 = "<path>", description = "Pack external binary within the klib")
var includeBinaries: Array<String>? = null
@@ -37,6 +37,8 @@ class KonanConfigKeys {
= CompilerConfigurationKey.create("enable backend phases")
val ENTRY: CompilerConfigurationKey<String?>
= CompilerConfigurationKey.create("fully qualified main() name")
val GENERATE_TEST_RUNNER: CompilerConfigurationKey<Boolean>
= CompilerConfigurationKey.create("generate test runner")
val INCLUDED_BINARY_FILES: CompilerConfigurationKey<List<String>>
= CompilerConfigurationKey.create("included binary file paths")
val LIBRARY_FILES: CompilerConfigurationKey<List<String>>
@@ -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)
}
@@ -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),
@@ -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
@@ -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<IrFunctionSymbol>()
val beforeFunctions = mutableListOf<IrFunctionSymbol>()
val afterFunctions = mutableListOf<IrFunctionSymbol>()
}
private inner class TopLevelTestSuite() {
val testFunctions = mutableListOf<IrFunctionSymbol>()
val beforeFunctions = mutableListOf<IrFunctionSymbol>()
val afterFunctions = mutableListOf<IrFunctionSymbol>()
val beforeClassFunctions = mutableListOf<IrFunctionSymbol>()
val afterClassFunctions = mutableListOf<IrFunctionSymbol>()
}
private inner class AnnotationCollector : IrElementVisitorVoid {
val testClasses = mutableMapOf<IrClassSymbol, TestClass>()
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 }
}
@@ -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<FunctionDescriptor>()
val beforeFunction = mutableListOf<FunctionDescriptor>()
val afterFunction = mutableListOf<FunctionDescriptor>()
val beforeClassFunction = mutableListOf<FunctionDescriptor>()
val afterClassFunction = mutableListOf<FunctionDescriptor>()
val testClasses = mutableListOf<ClassDescriptor>()
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 }
}
+6
View File
@@ -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()) {
}
}
}
+98
View File
@@ -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() }))
))
}()
+14
View File
@@ -0,0 +1,14 @@
import kotlin.test.Test
class A {
@Test
fun test() {
println("A.test")
}
}
@Test
fun test() {
println("test")
}
+4
View File
@@ -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')
@@ -0,0 +1,21 @@
package konan.test
class A {
fun test() { println("test") }
}
fun getTestSuites(): Collection<TestSuite> {
return listOf(
object: TestClass<A>("A") {
override fun createInstance() = A()
init {
registerTestCase(BasicTestCase("test", A::test))
}
}
)
}
fun main(args:Array<String>) {
TestRunner(getTestSuites()).run()
}
@@ -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)
}
@@ -0,0 +1,26 @@
package konan.test
class TestRunner(suites: Collection<TestSuite> = 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<TestSuite>().apply { addAll(suites) }
val suites: Collection<TestSuite> get() = _suites
fun register(suite: TestSuite) = _suites.add(suite)
fun register(suites: Iterable<TestSuite>) = _suites.addAll(suites)
fun register(vararg suites: TestSuite) = _suites.addAll(suites)
fun run() {
suites.forEach {
it.run(SimpleTestListener)
}
}
}
@@ -0,0 +1,97 @@
package konan.test
interface TestCase {
val name: String
}
interface TestSuite {
val name: String
val testCases: Map<String, TestCase>
fun run(listener: TestListener)
}
enum class FunctionKind {
BEFORE,
AFTER,
BEFORE_CLASS,
AFTER_CLASS
}
abstract class AbstractTestSuite<F: Function<Unit>>(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<String, BasicTestCase>()
override val testCases: Map<String, BasicTestCase>
get() = _testCases
private val specialFunctions = mutableMapOf<FunctionKind, MutableSet<F>>()
private fun Map<FunctionKind, Set<F>>.getFunctions(type: FunctionKind) =
specialFunctions.getOrPut(type) { mutableSetOf() }
val before: Collection<F> get() = specialFunctions.getFunctions(FunctionKind.BEFORE)
val after: Collection<F> get() = specialFunctions.getFunctions(FunctionKind.AFTER)
// TODO: Must be in companions. Support it.
val beforeClass: Collection<F> get() = specialFunctions.getFunctions(FunctionKind.BEFORE_CLASS)
val afterClass: Collection<F> get() = specialFunctions.getFunctions(FunctionKind.AFTER_CLASS)
protected fun registerTestCase(testCase: BasicTestCase) = _testCases.put(testCase.name, testCase)
protected fun registerTestCases(testCases: Collection<BasicTestCase>) = 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<T>(name: String): AbstractTestSuite<T.() -> 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() }
}
}
@@ -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
+1
View File
@@ -28,3 +28,4 @@ include ':backend.native:tests'
include ':shared'
include ':tools:kotlin-native-gradle-plugin'
include ':utilities'
include ':ext:junit'