[Native][tests] Dump the list of all available tests during compilation

^KT-50316
This commit is contained in:
Dmitriy Dolovov
2022-02-10 17:15:51 +03:00
parent 703aea1362
commit 56510fbc6c
7 changed files with 96 additions and 32 deletions
@@ -386,6 +386,7 @@ class K2Native : CLICompiler<K2NativeCompilerArguments>() {
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)
}
}
@@ -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 = "<path>",
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
@@ -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<ClassId, MutableCollection<String>>()
val reflectionTypes: KonanReflectionTypes by lazy(PUBLICATION) {
KonanReflectionTypes(moduleDescriptor)
}
@@ -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)
@@ -172,6 +172,8 @@ class KonanConfigKeys {
val LAZY_IR_FOR_CACHES: CompilerConfigurationKey<Boolean> = CompilerConfigurationKey.create("use lazy IR for cached libraries")
val MEANINGFUL_BRIDGE_NAMES: CompilerConfigurationKey<Boolean> = CompilerConfigurationKey.create("enable meaningful bridge names")
val PARTIAL_LINKAGE: CompilerConfigurationKey<Boolean> = CompilerConfigurationKey.create("allows some symbols in klibs be missed")
val TEST_DUMP_OUTPUT_PATH: CompilerConfigurationKey<String?> = CompilerConfigurationKey.create("path to a file to dump the list of all available tests")
}
}
@@ -348,6 +348,30 @@ internal val dependenciesLowerPhase = NamedCompilerPhase(
}
})
internal val dumpTestsPhase = makeCustomPhase<Context, IrModuleFragment>(
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<Context, IrModuleFragment>(
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))
@@ -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<TestFunction>.registerFunction(
function: IrFunction,
kinds: Collection<Pair<FunctionKind, /* ignored: */ Boolean>>) =
@@ -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<TestFunction>()
fun registerFunction(
function: IrFunction,
kinds: Collection<Pair<FunctionKind, /* ignored: */ Boolean>>
) = 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<IrClass, TestClass>()
val topLevelFunctions = mutableListOf<TestFunction>()
val topLevelSuiteClassId: ClassId by lazy {
ClassId(irFile.fqName, PackagePartClassUtils.getFilePartShortName(irFile.fileName).let(Name::identifier))
}
val topLevelSuiteName: String get() = topLevelSuiteClassId.asFqNameString()
private fun MutableMap<IrClass, TestClass>.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<T> where T is the test class.
*/
private fun buildClassSuite(testClass: IrClass,
testCompanion: IrClass?,
functions: Collection<TestFunction>,
irFile: IrFile
private fun buildClassSuite(
suiteName: String,
testClass: IrClass,
testCompanion: IrClass?,
functions: Collection<TestFunction>,
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<TestFunction>): IrExpression? {
private fun generateTopLevelSuite(irFile: IrFile, topLevelSuiteName: String, functions: Collection<TestFunction>): 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<IrExpression>()
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)
}
}