From 74fdfa17f00024e7c330b962caf05a4299644bc3 Mon Sep 17 00:00:00 2001 From: Ilya Matveev Date: Thu, 14 Sep 2017 12:09:02 +0300 Subject: [PATCH] unit-tests: Support for companion object tests --- .../backend/konan/lower/TestProcessor.kt | 235 ++++++++++++++---- .../src/main/kotlin/konan/test/TestSuite.kt | 72 ++++-- 2 files changed, 238 insertions(+), 69 deletions(-) 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 index 014eb68cb2c..96a30ed4b69 100644 --- 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 @@ -1,11 +1,13 @@ package org.jetbrains.kotlin.backend.konan.lower +import kotlinx.coroutines.experimental.newCoroutineContext import org.jetbrains.kotlin.backend.common.FileLoweringPass import org.jetbrains.kotlin.backend.common.descriptors.replace import org.jetbrains.kotlin.backend.common.ir.createFakeOverrideDescriptor import org.jetbrains.kotlin.backend.common.lower.SimpleMemberScope import org.jetbrains.kotlin.backend.common.lower.SymbolWithIrBuilder import org.jetbrains.kotlin.backend.common.lower.createIrBuilder +import org.jetbrains.kotlin.backend.common.reportWarning import org.jetbrains.kotlin.backend.konan.KonanBackendContext import org.jetbrains.kotlin.backend.konan.descriptors.isAbstract import org.jetbrains.kotlin.backend.konan.descriptors.synthesizedName @@ -25,6 +27,7 @@ import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.declarations.impl.IrClassImpl import org.jetbrains.kotlin.ir.declarations.impl.IrConstructorImpl import org.jetbrains.kotlin.ir.declarations.impl.IrFunctionImpl +import org.jetbrains.kotlin.ir.expressions.IrGetObjectValue import org.jetbrains.kotlin.ir.expressions.impl.* import org.jetbrains.kotlin.ir.symbols.* import org.jetbrains.kotlin.ir.symbols.impl.IrClassSymbolImpl @@ -39,6 +42,7 @@ import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.serialization.KonanIr import org.jetbrains.kotlin.types.KotlinType import java.io.File @@ -61,6 +65,13 @@ internal class TestProcessor (val context: KonanBackendContext): FileLoweringPas private val IrFile.fileName get() = name.substringAfterLast(File.separatorChar) private val IrFile.topLevelSuiteName get() = "Tests in file: $fileName" + private fun MutableList.registerFunction( + function: IrFunctionSymbol, + kinds: Collection) = kinds.forEach { add(TestFunction(function, it)) } + + private fun MutableList.registerFunction(function: IrFunctionSymbol, kind: FunctionKind) = + add(TestFunction(function, kind)) + private fun IrStatementsBuilder.generateFunctionRegistration( receiver: IrValueSymbol, registerTestCase: IrFunctionSymbol, @@ -107,18 +118,69 @@ internal class TestProcessor (val context: KonanBackendContext): FileLoweringPas // region Classes for annotation collection. // TODO: Support ignore + // TODO: Rework containing checks. internal enum class FunctionKind(annotationNameString: String, runtimeKindString: String) { TEST("kotlin.test.Test", "") { override val runtimeKindName: Name get() = throw NotImplementedError() + override fun canBeIn(classDescriptor: ClassDescriptor, context: KonanBackendContext) = + classDescriptor.checkInstanceFunctionAnnotation(annotationFqName, context) }, - BEFORE("kotlin.test.Before", "BEFORE"), - AFTER("kotlin.test.After", "AFTER"), - BEFORE_CLASS("kotlin.test.BeforeClass", "BEFORE_CLASS"), - AFTER_CLASS("kotlin.test.AfterClass", "AFTER_CLASS"); + BEFORE("kotlin.test.Before", "BEFORE") { + override fun canBeIn(classDescriptor: ClassDescriptor, context: KonanBackendContext) = + classDescriptor.checkInstanceFunctionAnnotation(annotationFqName, context) + }, + AFTER("kotlin.test.After", "AFTER") { + override fun canBeIn(classDescriptor: ClassDescriptor, context: KonanBackendContext) = + classDescriptor.checkInstanceFunctionAnnotation(annotationFqName, context) + }, + BEFORE_CLASS("kotlin.test.BeforeClass", "BEFORE_CLASS") { + override fun canBeIn(classDescriptor: ClassDescriptor, context: KonanBackendContext) = + classDescriptor.checkCompanionFunctionAnnotation(annotationFqName, context) + }, + AFTER_CLASS("kotlin.test.AfterClass", "AFTER_CLASS") { + override fun canBeIn(classDescriptor: ClassDescriptor, context: KonanBackendContext) = + classDescriptor.checkCompanionFunctionAnnotation(annotationFqName, context) + }; val annotationFqName = FqName(annotationNameString) open val runtimeKindName = Name.identifier(runtimeKindString) + abstract fun canBeIn(classDescriptor: ClassDescriptor, context: KonanBackendContext): Boolean + + companion object { + private fun KonanBackendContext.warn(msg: String) = reportCompilationWarning(msg) + + private fun ClassDescriptor.checkInstanceFunctionAnnotation(annotation: FqName, + context: KonanBackendContext) = + when { + isInner -> { + context.warn("Annotation $annotation is not allowed for methods of an inner class") + false + } + isAbstract() -> { + context.warn("Annotation $annotation is not allowed for methods of an abstract class") + false + } + isCompanionObject -> { + context.warn("Annotation $annotation is not allowed for methods of a companion object") + false + } + constructors.none { it.valueParameters.size == 0 } -> { + context.warn("Test class has no default constructor: ${fqNameSafe}") + false + } + else -> true + } + + private fun ClassDescriptor.checkCompanionFunctionAnnotation(annotation: FqName, + context: KonanBackendContext): Boolean { + if (!isCompanionObject) { + context.warn("Annotation $annotation is only allowed for methods of a companion object") + return false + } + return true + } + } } private val FunctionKind.runtimeKind: IrEnumEntrySymbol @@ -126,30 +188,27 @@ internal class TestProcessor (val context: KonanBackendContext): FileLoweringPas private data class TestFunction(val function: IrFunctionSymbol, val kind: FunctionKind) - private class ClassSuite(val ownerClass: IrClassSymbol) { + private inner class TestClass(val ownerClass: IrClassSymbol) { + var companion: IrClassSymbol? = null val functions = mutableListOf() - fun registerFunction(function: IrFunctionSymbol, kinds: Collection) = kinds.forEach { - functions.add(TestFunction(function, it)) - } + val hasCompanionFunctions get() = companion != null + + fun registerFunction(function: IrFunctionSymbol, kinds: Collection) = + functions.registerFunction(function, kinds) + fun registerFUnction(function: IrFunctionSymbol, kind: FunctionKind) = + functions.registerFunction(function, kind) } private inner class AnnotationCollector : IrElementVisitorVoid { - val testClasses = mutableMapOf() + val testClasses = mutableMapOf() val topLevelFunctions = mutableListOf() - private fun MutableMap.getTestSuite(key: IrClassSymbol) = - getOrPut(key) { ClassSuite(key) } + private fun MutableMap.getTestClass(key: IrClassSymbol) = + getOrPut(key) { TestClass(key) } - private fun MutableMap.getTestSuite(key: ClassDescriptor) = - getTestSuite(symbols.symbolTable.referenceClass(key)) - - private fun MutableList.registerFunction( - function: IrFunctionSymbol, - kinds: Collection) = kinds.forEach { add(TestFunction(function, it)) } - - private fun ClassDescriptor.canContainTests() = - !isInner && constructors.any { it.valueParameters.size == 0 } && !isAbstract() + private fun MutableMap.getTestClass(key: ClassDescriptor) = + getTestClass(symbols.symbolTable.referenceClass(key)) override fun visitElement(element: IrElement) { element.acceptChildrenVoid(this) @@ -157,24 +216,38 @@ internal class TestProcessor (val context: KonanBackendContext): FileLoweringPas fun IrFunctionSymbol.hasAnnotatoin(fqName: FqName) = descriptor.annotations.any { it.fqName == fqName } + fun registerClassFunction(classDescriptor: ClassDescriptor, + function: IrFunctionSymbol, + kinds: Collection) { + kinds.filter { it.canBeIn(classDescriptor, context) }.forEach { + when (it) { + FunctionKind.TEST, FunctionKind.BEFORE, FunctionKind.AFTER -> + testClasses.getTestClass(classDescriptor).registerFUnction(function, it) + FunctionKind.AFTER_CLASS, FunctionKind.BEFORE_CLASS -> { + assert(classDescriptor.isCompanionObject) + val containingClass = classDescriptor.containingDeclaration as ClassDescriptor + val testClass = testClasses.getTestClass(containingClass) + testClass.companion = symbols.symbolTable.referenceClass(classDescriptor) + testClass.registerFUnction(function, it) + } + } + } + } + // TODO: Use symbols instead of containingDeclaration when such information is available. override fun visitFunction(declaration: IrFunction) { - val functionSymbol = declaration.symbol - val kinds = FunctionKind.values().filter { functionSymbol.hasAnnotatoin(it.annotationFqName) } + val symbol = declaration.symbol + val owner = declaration.descriptor.containingDeclaration + + val kinds = FunctionKind.values().filter { symbol.hasAnnotatoin(it.annotationFqName) } if (kinds.isEmpty()) { return } - val owner = declaration.descriptor.containingDeclaration - when { - owner is PackageFragmentDescriptor -> - topLevelFunctions.registerFunction(functionSymbol, kinds) - owner is ClassDescriptor && owner.canContainTests() -> - testClasses.getTestSuite(owner).registerFunction(declaration.symbol, kinds) - owner is ClassDescriptor && !owner.canContainTests() -> - context.reportCompilationWarning("Class cannot contain unit-tests: ${owner.fqNameSafe}") - else -> - UnsupportedOperationException("Cannot create test function $declaration (defined in $owner") + when (owner) { + is PackageFragmentDescriptor -> topLevelFunctions.registerFunction(symbol, kinds) + is ClassDescriptor -> registerClassFunction(owner, symbol, kinds) + else -> UnsupportedOperationException("Cannot create test function $declaration (defined in $owner") } } } @@ -182,6 +255,56 @@ internal class TestProcessor (val context: KonanBackendContext): FileLoweringPas //region Symbol and IR builders + private inner class CompanionGetterBuilder(val testCompanion: IrClassSymbol, val testSuite: IrClassSymbol) + : SymbolWithIrBuilder() { + + val getterName = Name.identifier("getCompanion") + val superFunction = baseClassSuiteDescriptor + .unsubstitutedMemberScope + .getContributedFunctions(getterName, NoLookupLocation.FROM_BACKEND) + .single { it.valueParameters.isEmpty() } + + override fun buildIr(): IrFunction = IrFunctionImpl( + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + TEST_SUITE_GENERATED_MEMBER, + symbol).apply { + val builder = context.createIrBuilder(symbol) + createParameterDeclarations() + body = builder.irBlockBody { + +irReturn(IrGetObjectValueImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, + testCompanion.descriptor.defaultType, testCompanion) + ) + } + } + + override fun doInitialize() { + val descriptor = symbol.descriptor as SimpleFunctionDescriptorImpl + descriptor.initialize( + null, + testSuite.descriptor.thisAsReceiverParameter, + emptyList(), + emptyList(), + testCompanion.descriptor.defaultType, + Modality.FINAL, + Visibilities.PROTECTED + ).apply { + overriddenDescriptors += superFunction + } + } + + override fun buildSymbol() = IrSimpleFunctionSymbolImpl( + SimpleFunctionDescriptorImpl.create( + /* containingDeclaration = */ testSuite.descriptor, + /* annotations = */ Annotations.EMPTY, + /* name = */ getterName, + /* kind = */ CallableMemberDescriptor.Kind.SYNTHESIZED, + /* source = */ SourceElement.NO_SOURCE + ) + ) + + } + /** Builds the BaseClassSuite.createInstance() function. */ private inner class InstanceGetterBuilder(val testClass: IrClassSymbol, val testSuite: IrClassSymbol) : SymbolWithIrBuilder() { @@ -236,13 +359,13 @@ internal class TestProcessor (val context: KonanBackendContext): FileLoweringPas * method(s) annotated with @Test). The test suite class is a subclass of ClassTestSuite * where T is the test class. */ - private inner class ClassSuiteConstructorBuilder(val testClass: IrClassSymbol, + private inner class ClassSuiteConstructorBuilder(val suiteName: String, + val testClassType: KotlinType, + val testCompanionType: KotlinType, val testSuite: IrClassSymbol, val functions: Collection) : SymbolWithIrBuilder() { - val suiteName = testClass.descriptor.fqNameSafe.toString() - private fun IrClassSymbol.getFunction(name: String, predicate: (FunctionDescriptor) -> Boolean) = symbols.symbolTable.referenceFunction(descriptor.unsubstitutedMemberScope .getContributedFunctions(Name.identifier(name), NoLookupLocation.FROM_BACKEND) @@ -273,9 +396,8 @@ internal class TestProcessor (val context: KonanBackendContext): FileLoweringPas endOffset = UNDEFINED_OFFSET, symbol = symbols.symbolTable.referenceConstructor(superConstructor), descriptor = superConstructor, - typeArguments = mapOf( - superConstructor.typeParameters[0] to testClass.descriptor.defaultType - ) + typeArguments = mapOf(superConstructor.typeParameters[0] to testClassType, + superConstructor.typeParameters[1] to testCompanionType) ).apply { putValueArgument(0, IrConstImpl.string( UNDEFINED_OFFSET, @@ -312,15 +434,24 @@ internal class TestProcessor (val context: KonanBackendContext): FileLoweringPas * annotated with @Test). The test suite class is a subclass of ClassTestSuite where T is the test class. */ private inner class ClassSuiteBuilder(val testClass: IrClassSymbol, + val testCompanion: IrClassSymbol?, val containingDeclaration: DeclarationDescriptor, val functions: Collection) : SymbolWithIrBuilder() { + val suiteName = testClass.descriptor.fqNameSafe.toString() val suiteClassName = testClass.descriptor.name.synthesizeSuiteClassName() - val superType = baseClassSuiteDescriptor.defaultType.replace(listOf(testClass.descriptor.defaultType)) - val constructorBuilder = ClassSuiteConstructorBuilder(testClass, symbol, functions) + val testClassType = testClass.descriptor.defaultType + val testCompanionType = testCompanion?.descriptor?.defaultType ?: context.irBuiltIns.nothing + + val superType = baseClassSuiteDescriptor.defaultType.replace(listOf(testClassType, testCompanionType)) + + val constructorBuilder = ClassSuiteConstructorBuilder( + suiteName, testClassType,testCompanionType, symbol, functions + ) val instanceGetterBuilder = InstanceGetterBuilder(testClass, symbol) + val companionGetterBuilder = testCompanion?.let { CompanionGetterBuilder(it, symbol) } override fun buildIr() = IrClassImpl( @@ -331,6 +462,7 @@ internal class TestProcessor (val context: KonanBackendContext): FileLoweringPas createParameterDeclarations() addMember(constructorBuilder.ir) addMember(instanceGetterBuilder.ir) + companionGetterBuilder?.let { addMember(it.ir) } addFakeOverrides() } @@ -342,10 +474,10 @@ internal class TestProcessor (val context: KonanBackendContext): FileLoweringPas .unsubstitutedMemberScope .getContributedDescriptors() .map { - if (it == instanceGetterBuilder.superFunction) { - instanceGetterBuilder.symbol.descriptor - } else { - it.createFakeOverrideDescriptor(symbol.descriptor as ClassDescriptorImpl) + when { + it == instanceGetterBuilder.superFunction -> instanceGetterBuilder.symbol.descriptor + it == companionGetterBuilder?.superFunction -> companionGetterBuilder.symbol.descriptor + else -> it.createFakeOverrideDescriptor(symbol.descriptor as ClassDescriptorImpl) } }.filterNotNull() @@ -355,6 +487,7 @@ internal class TestProcessor (val context: KonanBackendContext): FileLoweringPas constructorBuilder.initialize() instanceGetterBuilder.initialize() + companionGetterBuilder?.initialize() } override fun buildSymbol(): org.jetbrains.kotlin.ir.symbols.IrClassSymbol = IrClassSymbolImpl( @@ -372,8 +505,11 @@ internal class TestProcessor (val context: KonanBackendContext): FileLoweringPas //endregion // region IR generation methods - private fun generateClassSuite(irFile: IrFile, testClass: IrClassSymbol, functions: Collection) = - with(ClassSuiteBuilder(testClass, irFile.packageFragmentDescriptor, functions)) { + private fun generateClassSuite(irFile: IrFile, testClass: TestClass) = + with(ClassSuiteBuilder(testClass.ownerClass, + testClass.companion, + irFile.packageFragmentDescriptor, + testClass.functions)) { initialize() irFile.declarations.add(ir) irFile.addTopLevelInitializer( @@ -396,11 +532,14 @@ internal class TestProcessor (val context: KonanBackendContext): FileLoweringPas }) } + // TODO: Support tests in objects // TODO: Support beforeTest/afterTest for companions. private fun createTestSuites(irFile: IrFile, annotationCollector: AnnotationCollector) { - annotationCollector.testClasses.forEach { _, testClass -> - generateClassSuite(irFile, testClass.ownerClass, testClass.functions) + annotationCollector.testClasses.filter { + it.value.functions.any { it.kind == FunctionKind.TEST } + }.forEach { _, testClass -> + generateClassSuite(irFile, testClass) } if (annotationCollector.topLevelFunctions.isNotEmpty()) { generateTopLevelSuite(irFile, annotationCollector.topLevelFunctions) diff --git a/runtime/src/main/kotlin/konan/test/TestSuite.kt b/runtime/src/main/kotlin/konan/test/TestSuite.kt index 09c4f4d34a4..275b6ce461d 100644 --- a/runtime/src/main/kotlin/konan/test/TestSuite.kt +++ b/runtime/src/main/kotlin/konan/test/TestSuite.kt @@ -36,24 +36,10 @@ abstract class AbstractTestSuite>(override val name: String): override val testCases: Map> get() = _testCases - private val specialFunctions = mutableMapOf>() - private fun Map>.getFunctions(type: TestFunctionKind) = - specialFunctions.getOrPut(type) { mutableSetOf() } - - val before: Collection get() = specialFunctions.getFunctions(TestFunctionKind.BEFORE) - val after: Collection get() = specialFunctions.getFunctions(TestFunctionKind.AFTER) - - // TODO: Must be in companions. Support it. - val beforeClass: Collection get() = specialFunctions.getFunctions(TestFunctionKind.BEFORE_CLASS) - val afterClass: Collection get() = specialFunctions.getFunctions(TestFunctionKind.AFTER_CLASS) - private fun registerTestCase(testCase: BasicTestCase) = _testCases.put(testCase.name, testCase) fun registerTestCase(name: String, testFunction: F) = registerTestCase(createTestCase(name, testFunction)) fun createTestCase(name: String, testFunction: F) = BasicTestCase(name, this, testFunction) - fun registerFunction(type: TestFunctionKind, function: F) = - specialFunctions.getFunctions(type).add(function) - protected abstract fun doBeforeClass() protected abstract fun doAfterClass() @@ -81,15 +67,47 @@ abstract class AbstractTestSuite>(override val name: String): } } -abstract class BaseClassSuite(name: String): AbstractTestSuite Unit>(name) { +abstract class BaseClassSuite(name: String): AbstractTestSuite Unit>(name) { - abstract fun createInstance(): T + // These two methods are overrided in a test suite class generated by the compiler. + abstract fun createInstance(): INSTANCE + open fun getCompanion(): COMPANION = throw NotImplementedError("Test class has no companion object") - // TODO: What about companions? - override fun doBeforeClass() {} // = beforeClass.forEach { createInstance().it() } - override fun doAfterClass() {} // = afterClass.forEach { createInstance().it() } + companion object { + val INSTANCE_KINDS = listOf(TestFunctionKind.BEFORE, TestFunctionKind.AFTER) + val COMPANION_KINDS = listOf(TestFunctionKind.BEFORE_CLASS, TestFunctionKind.AFTER_CLASS) + } - override fun doTest(testCase: BasicTestCase Unit>) { + private val instanceFunctions = mutableMapOf Unit>>() + private fun getInstanceFunctions(kind: TestFunctionKind): MutableCollection Unit> { + check(kind in INSTANCE_KINDS) + return instanceFunctions.getOrPut(kind) { mutableSetOf() } + } + + private val companionFunction = mutableMapOf Unit>>() + private fun getCompanionFunctions(kind: TestFunctionKind): MutableCollection Unit> { + check(kind in COMPANION_KINDS) + return companionFunction.getOrPut(kind) { mutableSetOf() } + } + + val before: Collection Unit> get() = getInstanceFunctions(TestFunctionKind.BEFORE) + val after: Collection Unit> get() = getInstanceFunctions(TestFunctionKind.AFTER) + + val beforeClass: Collection Unit> get() = getCompanionFunctions(TestFunctionKind.BEFORE_CLASS) + val afterClass: Collection Unit> get() = getCompanionFunctions(TestFunctionKind.AFTER_CLASS) + + @Suppress("UNCHECKED_CAST") + fun registerFunction(kind: TestFunctionKind, function: Function1<*, Unit>) = + when (kind) { + in INSTANCE_KINDS -> getInstanceFunctions(kind).add(function as INSTANCE.() -> Unit) + in COMPANION_KINDS -> getCompanionFunctions(kind).add(function as COMPANION.() -> Unit) + else -> throw IllegalArgumentException("Unknown function kind: $kind") + } + + override fun doBeforeClass() = beforeClass.forEach { getCompanion().it() } + override fun doAfterClass() = afterClass.forEach { getCompanion().it() } + + override fun doTest(testCase: BasicTestCase Unit>) { val instance = createInstance() val testFunction = testCase.testFunction try { @@ -101,7 +119,19 @@ abstract class BaseClassSuite(name: String): AbstractTestSuite Unit>( } } -class TopLevelSuite(name: String): AbstractTestSuite<() -> Unit>(name) { +private typealias TopLevelFun = () -> Unit + +class TopLevelSuite(name: String): AbstractTestSuite(name) { + + private val specialFunctions = mutableMapOf>() + private fun getFunctions(type: TestFunctionKind) = specialFunctions.getOrPut(type) { mutableSetOf() } + + val before: Collection get() = getFunctions(TestFunctionKind.BEFORE) + val after: Collection get() = getFunctions(TestFunctionKind.AFTER) + val beforeClass: Collection get() = getFunctions(TestFunctionKind.BEFORE_CLASS) + val afterClass: Collection get() = getFunctions(TestFunctionKind.AFTER_CLASS) + + fun registerFunction(kind: TestFunctionKind, function: TopLevelFun) = getFunctions(kind).add(function) override fun doBeforeClass() = beforeClass.forEach { it() } override fun doAfterClass() = afterClass.forEach { it() }