diff --git a/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt b/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt index e5f9a7f50db..4bb428d24de 100644 --- a/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt +++ b/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt @@ -386,6 +386,7 @@ class K2Native : CLICompiler() { putIfNotNull(RUNTIME_LOGS, arguments.runtimeLogs) putIfNotNull(BUNDLE_ID, parseBundleId(arguments, outputKind, configuration)) put(MEANINGFUL_BRIDGE_NAMES, arguments.meaningfulBridgeNames) + arguments.testDumpOutputPath?.let { put(TEST_DUMP_OUTPUT_PATH, it) } put(PARTIAL_LINKAGE, arguments.partialLinkage) } } diff --git a/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt b/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt index 486964093d9..d8b9faefd17 100644 --- a/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt +++ b/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt @@ -361,6 +361,13 @@ class K2NativeCompilerArguments : CommonCompilerArguments() { @Argument(value = "-Xmeaningful-bridge-names", description = "(Unstable) Produce meaningful names for reverse bridges. Useful only for compiler tests.") var meaningfulBridgeNames: Boolean = false + @Argument( + value = "-Xdump-tests-to", + valueDescription = "", + description = "Path to a file to dump the list of all available tests" + ) + var testDumpOutputPath: String? = null + @Argument(value = "-Xlazy-ir-for-caches", valueDescription = "{disable|enable}", description = "Use lazy IR for cached libraries") var lazyIrForCaches: String? = null diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Context.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Context.kt index 9a416ff008a..d079b144a20 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Context.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Context.kt @@ -57,6 +57,7 @@ import org.jetbrains.kotlin.konan.target.KonanTarget import org.jetbrains.kotlin.library.SerializedIrModule import org.jetbrains.kotlin.resolve.descriptorUtil.isEffectivelyExternal import org.jetbrains.kotlin.konan.library.KonanLibraryLayout +import org.jetbrains.kotlin.name.ClassId internal class InlineFunctionOriginInfo(val irFunction: IrFunction, val irFile: IrFile, val startOffset: Int, val endOffset: Int) @@ -290,6 +291,9 @@ internal class Context(config: KonanConfig) : KonanBackendContext(config) { getLocalClassName(source)?.let { name -> putLocalClassName(destination, name) } } + /* test suite class -> test function names */ + val testCasesToDump = mutableMapOf>() + val reflectionTypes: KonanReflectionTypes by lazy(PUBLICATION) { KonanReflectionTypes(moduleDescriptor) } diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt index 51955315c2b..4af60c21c02 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt @@ -327,6 +327,8 @@ class KonanConfig(val project: Project, val configuration: CompilerConfiguration internal val unitSuspendFunctionObjCExport: UnitSuspendFunctionObjCExport get() = configuration.get(BinaryOptions.unitSuspendFunctionObjCExport) ?: UnitSuspendFunctionObjCExport.LEGACY + + internal val testDumpFile: File? = configuration[KonanConfigKeys.TEST_DUMP_OUTPUT_PATH]?.let(::File) } fun CompilerConfiguration.report(priority: CompilerMessageSeverity, message: String) diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt index 09f7dc3d145..7713a2d645c 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt @@ -172,6 +172,8 @@ class KonanConfigKeys { val LAZY_IR_FOR_CACHES: CompilerConfigurationKey = CompilerConfigurationKey.create("use lazy IR for cached libraries") val MEANINGFUL_BRIDGE_NAMES: CompilerConfigurationKey = CompilerConfigurationKey.create("enable meaningful bridge names") val PARTIAL_LINKAGE: CompilerConfigurationKey = CompilerConfigurationKey.create("allows some symbols in klibs be missed") + val TEST_DUMP_OUTPUT_PATH: CompilerConfigurationKey = CompilerConfigurationKey.create("path to a file to dump the list of all available tests") + } } diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ToplevelPhases.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ToplevelPhases.kt index 320b77f0d4b..84aff74f8f3 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ToplevelPhases.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ToplevelPhases.kt @@ -348,6 +348,30 @@ internal val dependenciesLowerPhase = NamedCompilerPhase( } }) +internal val dumpTestsPhase = makeCustomPhase( + name = "dumpTestsPhase", + description = "Dump the list of all available tests", + op = { context, _ -> + val testDumpFile = context.config.testDumpFile + requireNotNull(testDumpFile) + + if (context.testCasesToDump.isEmpty()) { + testDumpFile.writeText("") + return@makeCustomPhase + } + + testDumpFile.writeText( + context.testCasesToDump.asSequence() + .flatMap { (suiteClassId, functionNames) -> + val suiteName = suiteClassId.asString() + functionNames.asSequence().map { "$suiteName:$it" } + } + .sorted() + .joinToString(separator = "\n") + ) + } +) + internal val entryPointPhase = makeCustomPhase( name = "addEntryPoint", description = "Add entry point for program", @@ -533,6 +557,7 @@ private val backendCodegen = namedUnitPhase( allLoweringsPhase then // Lower current module first. dependenciesLowerPhase then // Then lower all libraries in topological order. // With that we guarantee that inline functions are unlowered while being inlined. + dumpTestsPhase then entryPointPhase then exportInternalAbiPhase then useInternalAbiPhase then @@ -602,6 +627,7 @@ internal fun PhaseConfig.konanPhasesConfig(config: KonanConfig) { disableUnless(objectFilesPhase, config.produce.involvesLinkStage) disableUnless(linkerPhase, config.produce.involvesLinkStage) disableIf(testProcessorPhase, getNotNull(KonanConfigKeys.GENERATE_TEST_RUNNER) == TestRunnerKind.NONE) + disableIf(dumpTestsPhase, getNotNull(KonanConfigKeys.GENERATE_TEST_RUNNER) == TestRunnerKind.NONE || config.testDumpFile == null) disableUnless(buildDFGPhase, getBoolean(KonanConfigKeys.OPTIMIZATION)) disableUnless(devirtualizationAnalysisPhase, getBoolean(KonanConfigKeys.OPTIMIZATION)) disableUnless(devirtualizationPhase, getBoolean(KonanConfigKeys.OPTIMIZATION)) diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/TestProcessor.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/TestProcessor.kt index 112b0325138..5033e1d01e4 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/TestProcessor.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/TestProcessor.kt @@ -40,6 +40,7 @@ import org.jetbrains.kotlin.ir.util.addChild import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid import org.jetbrains.kotlin.load.kotlin.PackagePartClassUtils +import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.resolve.descriptorUtil.module @@ -71,13 +72,6 @@ internal class TestProcessor (val context: Context) { private val IrFile.fileName get() = name.substringAfterLast('/') - private val IrFile.topLevelSuiteName: String - get() { - val packageFqName = fqName - val shortFileName = PackagePartClassUtils.getFilePartShortName(fileName) - return if (packageFqName.isRoot) shortFileName else "$packageFqName.$shortFileName" - } - private fun MutableList.registerFunction( function: IrFunction, kinds: Collection>) = @@ -101,7 +95,7 @@ internal class TestProcessor (val context: Context) { // Call registerTestCase(name: String, testFunction: () -> Unit) method. +irCall(registerTestCase).apply { dispatchReceiver = irGet(receiver) - putValueArgument(0, irString(it.function.name.identifier)) + putValueArgument(0, irString(it.functionName)) putValueArgument(1, IrFunctionReferenceImpl( it.function.startOffset, it.function.endOffset, @@ -159,19 +153,19 @@ internal class TestProcessor (val context: Context) { private fun IrType.isTestFunctionKind() = classifierOrNull == symbols.testFunctionKind private data class TestFunction( - val function: IrFunction, - val kind: FunctionKind, - val ignored: Boolean - ) + val function: IrFunction, + val kind: FunctionKind, + val ignored: Boolean + ) { + val functionName: String get() = function.name.identifier + } private inner class TestClass(val ownerClass: IrClass) { var companion: IrClass? = null val functions = mutableListOf() - fun registerFunction( - function: IrFunction, - kinds: Collection> - ) = functions.registerFunction(function, kinds) + val suiteClassId: ClassId = ownerClass.classId ?: error(ownerClass.render()) + val suiteName: String get() = suiteClassId.asFqNameString() fun registerFunction(function: IrFunction, kind: FunctionKind, ignored: Boolean) = functions.registerFunction(function, kind, ignored) @@ -179,7 +173,12 @@ internal class TestProcessor (val context: Context) { private inner class AnnotationCollector(val irFile: IrFile) : IrElementVisitorVoid { val testClasses = mutableMapOf() + val topLevelFunctions = mutableListOf() + val topLevelSuiteClassId: ClassId by lazy { + ClassId(irFile.fqName, PackagePartClassUtils.getFilePartShortName(irFile.fileName).let(Name::identifier)) + } + val topLevelSuiteName: String get() = topLevelSuiteClassId.asFqNameString() private fun MutableMap.getTestClass(key: IrClass) = getOrPut(key) { TestClass(key) } @@ -474,10 +473,12 @@ internal class TestProcessor (val context: Context) { * Builds a test suite class representing a test class (any class in the original IrFile with method(s) * annotated with @Test). The test suite class is a subclass of ClassTestSuite where T is the test class. */ - private fun buildClassSuite(testClass: IrClass, - testCompanion: IrClass?, - functions: Collection, - irFile: IrFile + private fun buildClassSuite( + suiteName: String, + testClass: IrClass, + testCompanion: IrClass?, + functions: Collection, + irFile: IrFile ): IrClass { return IrClassImpl( testClass.startOffset, testClass.endOffset, @@ -506,8 +507,7 @@ internal class TestProcessor (val context: Context) { } val constructor = buildClassSuiteConstructor( - testClass.fqNameForIrSerialization.toString(), testClassType, testCompanionType, - symbol, this, functions, testClass.ignored + suiteName, testClassType, testCompanionType, symbol, this, functions, testClass.ignored ) val instanceGetter: IrFunction @@ -535,19 +535,19 @@ internal class TestProcessor (val context: Context) { // region IR generation methods private fun generateClassSuite(irFile: IrFile, testClass: TestClass) = - with(buildClassSuite(testClass.ownerClass, testClass.companion, testClass.functions, irFile)) { + with(buildClassSuite(testClass.suiteName, testClass.ownerClass, testClass.companion, testClass.functions, irFile)) { val irConstructor = constructors.single() val irBuilder = context.createIrBuilder(irFile.symbol, testClass.ownerClass.startOffset, testClass.ownerClass.endOffset) irBuilder.irCall(irConstructor) } /** Check if this fqName already used or not. */ - private fun checkSuiteName(irFile: IrFile, name: String): Boolean { - if (topLevelSuiteNames.contains(name)) { + private fun checkTopLevelSuiteName(irFile: IrFile, topLevelSuiteName: String): Boolean { + if (topLevelSuiteNames.contains(topLevelSuiteName)) { context.reportCompilationError("Package '${irFile.fqName}' has top-level test " + "functions in several files with the same name: '${irFile.fileName}'") } - topLevelSuiteNames.add(name) + topLevelSuiteNames.add(topLevelSuiteName) return true } @@ -570,16 +570,15 @@ internal class TestProcessor (val context: Context) { && it.valueParameters[2].type.isBoolean() } - private fun generateTopLevelSuite(irFile: IrFile, functions: Collection): IrExpression? { + private fun generateTopLevelSuite(irFile: IrFile, topLevelSuiteName: String, functions: Collection): IrExpression? { val irBuilder = context.createIrBuilder(irFile.symbol, SYNTHETIC_OFFSET, SYNTHETIC_OFFSET) - val suiteName = irFile.topLevelSuiteName - if (!checkSuiteName(irFile, suiteName)) { + if (!checkTopLevelSuiteName(irFile, topLevelSuiteName)) { return null } return irBuilder.irBlock { val constructorCall = irCall(topLevelSuiteConstructor).apply { - putValueArgument(0, irString(suiteName)) + putValueArgument(0, irString(topLevelSuiteName)) } val testSuiteVal = irTemporary(constructorCall, "topLevelTestSuite") generateFunctionRegistration(testSuiteVal, @@ -591,14 +590,17 @@ internal class TestProcessor (val context: Context) { private fun createTestSuites(irFile: IrFile, annotationCollector: AnnotationCollector) { val statements = mutableListOf() + annotationCollector.testClasses.filter { it.value.functions.any { it.kind == FunctionKind.TEST } - }.forEach { _, testClass -> + }.forEach { (_, testClass) -> statements.add(generateClassSuite(irFile, testClass)) } + if (annotationCollector.topLevelFunctions.isNotEmpty()) { - generateTopLevelSuite(irFile, annotationCollector.topLevelFunctions)?.let { statements.add(it) } + generateTopLevelSuite(irFile, annotationCollector.topLevelSuiteName, annotationCollector.topLevelFunctions)?.let { statements.add(it) } } + if (statements.isNotEmpty()) { context.irFactory.buildFun { startOffset = SYNTHETIC_OFFSET @@ -618,6 +620,25 @@ internal class TestProcessor (val context: Context) { } // endregion + // region test functions to be dumped + private fun recordTestFunctions(annotationCollector: AnnotationCollector) { + if (context.config.testDumpFile == null) return + + fun recordFunction(suiteClassId: ClassId, function: TestFunction) { + if (function.kind == FunctionKind.TEST) + context.testCasesToDump.computeIfAbsent(suiteClassId) { mutableListOf() } += function.functionName + } + + annotationCollector.topLevelFunctions.forEach { function -> + recordFunction(annotationCollector.topLevelSuiteClassId, function) + } + + annotationCollector.testClasses.values.forEach { testClass -> + testClass.functions.forEach { function -> recordFunction(testClass.suiteClassId, function) } + } + } + // endregion + private fun shouldProcessFile(irFile: IrFile): Boolean = irFile.packageFragmentDescriptor.module.let { // Process test annotations in source libraries too. it == context.moduleDescriptor || it in context.getIncludedLibraryDescriptors() @@ -632,5 +653,6 @@ internal class TestProcessor (val context: Context) { val annotationCollector = AnnotationCollector(irFile) irFile.acceptChildrenVoid(annotationCollector) createTestSuites(irFile, annotationCollector) + recordTestFunctions(annotationCollector) } }