diff --git a/compiler/testData/klibABI/__utils__/__utils__.kt b/compiler/testData/klibABI/__utils__/__utils__.kt new file mode 100644 index 00000000000..d5eff25477f --- /dev/null +++ b/compiler/testData/klibABI/__utils__/__utils__.kt @@ -0,0 +1,151 @@ +@file:Suppress("PackageDirectoryMismatch", "unused") + +package abitestutils + +import abitestutils.ThrowableKind.* + +/** API **/ + +interface TestBuilder { + fun prefixed(prefix: String): ErrorMessagePattern + fun nonImplementedCallable(callableTypeAndName: String, classifierTypeAndName: String): ErrorMessagePattern + + fun expectFailure(errorMessagePattern: ErrorMessagePattern, block: Block) + fun expectSuccess(block: Block) // OK is expected + fun expectSuccess(expectedOutcome: T, block: Block) +} + +const val OK_STATUS = "OK" + +sealed interface ErrorMessagePattern + +private typealias Block = () -> T + +fun abiTest(init: TestBuilder.() -> Unit): String { + val builder = TestBuilderImpl() + builder.init() + builder.check() + return builder.runTests() +} + +/** Implementation **/ + +private class TestBuilderImpl : TestBuilder { + private val tests = mutableListOf() + + override fun prefixed(prefix: String) = PrefixOfErrorMessage(prefix) + + override fun nonImplementedCallable(callableTypeAndName: String, classifierTypeAndName: String) = + NonImplementedCallable(callableTypeAndName, classifierTypeAndName) + + override fun expectFailure(errorMessagePattern: ErrorMessagePattern, block: Block) { + tests += FailingTest(errorMessagePattern, block) + } + + override fun expectSuccess(block: Block) = expectSuccess(OK_STATUS, block) + + override fun expectSuccess(expectedOutcome: T, block: Block) { + tests += SuccessfulTest(expectedOutcome, block) + } + + fun check() { + check(tests.isNotEmpty()) { "No ABI tests configured" } + } + + fun runTests(): String { + val testFailures: List = tests.mapIndexedNotNull { serialNumber, test -> + val testFailureDetails: TestFailureDetails? = when (test) { + is FailingTest -> { + try { + test.block() + TestSuccessfulButMustFail + } catch (t: Throwable) { + when (t.throwableKind) { + IR_LINKAGE_ERROR -> test.checkIrLinkageErrorMessage(t) + EXCEPTION -> TestFailedWithException(t) + NON_EXCEPTION -> throw t // Something totally unexpected. Rethrow. + } + } + } + + is SuccessfulTest -> { + try { + val result = test.block() + if (result == test.expectedOutcome) + null // Success. + else + TestMismatchedExpectation(test.expectedOutcome, result) + } catch (t: Throwable) { + if (t.throwableKind == NON_EXCEPTION) + throw t // Something totally unexpected. Rethrow. + else + TestFailedWithException(t) + } + } + } + + if (testFailureDetails != null) TestFailure(serialNumber, testFailureDetails) else null + } + + return if (testFailures.isEmpty()) OK_STATUS else testFailures.joinToString(prefix = "\n", separator = "\n", postfix = "\n") + } +} + +private sealed interface AbstractErrorMessagePattern : ErrorMessagePattern { + fun checkIrLinkageErrorMessage(errorMessage: String?): TestFailureDetails? +} + +private class PrefixOfErrorMessage(prefix: String) : AbstractErrorMessagePattern { + init { + check(prefix.isNotBlank()) { "Prefix is blank: [$prefix]" } + } + + private val fullPrefix = "$prefix because it uses unlinked symbols" + + override fun checkIrLinkageErrorMessage(errorMessage: String?) = + if (errorMessage?.startsWith(fullPrefix) == true) + null // Success. + else + TestMismatchedExpectation(fullPrefix, errorMessage) +} + +private class NonImplementedCallable(callableTypeAndName: String, classifierTypeAndName: String) : AbstractErrorMessagePattern { + init { + check(callableTypeAndName.isNotBlank() && ' ' in callableTypeAndName) { "Invalid callable type & name: [$callableTypeAndName]" } + check(classifierTypeAndName.isNotBlank() && ' ' in classifierTypeAndName) { "Invalid classifier type & name: [$classifierTypeAndName]" } + } + + private val fullMessage = "Abstract $callableTypeAndName is not implemented in non-abstract $classifierTypeAndName" + + override fun checkIrLinkageErrorMessage(errorMessage: String?) = + if (errorMessage == fullMessage) + null // Success. + else + TestMismatchedExpectation(fullMessage, errorMessage) +} + +private sealed interface Test +private class FailingTest(val errorMessagePattern: ErrorMessagePattern, val block: Block) : Test +private class SuccessfulTest(val expectedOutcome: Any, val block: Block) : Test + +private class TestFailure(val serialNumber: Int, val details: TestFailureDetails) { + override fun toString() = "#$serialNumber: ${details.description}" +} + +private sealed class TestFailureDetails(val description: String) +private object TestSuccessfulButMustFail : TestFailureDetails("Test is successful but was expected to fail.") +private class TestFailedWithException(t: Throwable) : TestFailureDetails("Test unexpectedly failed with exception: $t") +private class TestMismatchedExpectation(expectedOutcome: Any, actualOutcome: Any?) : + TestFailureDetails("EXPECTED: $expectedOutcome, ACTUAL: $actualOutcome") + +private enum class ThrowableKind { IR_LINKAGE_ERROR, EXCEPTION, NON_EXCEPTION } + +private val Throwable.throwableKind: ThrowableKind + get() = when { + this::class.simpleName == "IrLinkageError" -> IR_LINKAGE_ERROR + this is Exception -> EXCEPTION + else -> NON_EXCEPTION + } + +private fun FailingTest.checkIrLinkageErrorMessage(t: Throwable) = + (errorMessagePattern as AbstractErrorMessagePattern).checkIrLinkageErrorMessage(t.message) diff --git a/compiler/tests-common/tests/org/jetbrains/kotlin/klib/KlibABITestUtils.kt b/compiler/tests-common/tests/org/jetbrains/kotlin/klib/KlibABITestUtils.kt index c7c8adf0729..e38bbaa8e90 100644 --- a/compiler/tests-common/tests/org/jetbrains/kotlin/klib/KlibABITestUtils.kt +++ b/compiler/tests-common/tests/org/jetbrains/kotlin/klib/KlibABITestUtils.kt @@ -48,11 +48,14 @@ object KlibABITestUtils { val moduleBuildDirs = createModuleDirs(buildDir, moduleName) // Populate the source dir with *.kt files. - val moduleSourceDir = moduleBuildDirs.sourceDir.apply { mkdirs() } - moduleTestDir.walk().filter { it.isFile && it.extension == "kt" }.forEach { sourceFile -> - val destFile = moduleSourceDir.resolve(sourceFile.relativeTo(moduleTestDir)) - destFile.parentFile.mkdirs() - sourceFile.copyTo(destFile) + copySources(from = moduleTestDir, to = moduleBuildDirs.sourceDir) + + // Include ABI utils into the main module. + if (moduleName == MAIN_MODULE_NAME) { + val utilsDir = testDir.parentFile.resolve(ABI_UTILS_DIR) + KtUsefulTestCase.assertExists(utilsDir) + + copySources(from = utilsDir, to = moduleBuildDirs.sourceDir) } val moduleOutputDir = moduleBuildDirs.outputDir.apply { mkdirs() } @@ -101,6 +104,14 @@ object KlibABITestUtils { buildBinaryAndRun(mainModuleKlibFile, allKlibs) } + private fun copySources(from: File, to: File) { + from.walk().filter { it.isFile && it.extension == "kt" }.forEach { sourceFile -> + val destFile = to.resolve(sourceFile.relativeTo(from)) + destFile.parentFile.mkdirs() + sourceFile.copyTo(destFile) + } + } + fun createModuleDirs(buildDir: File, moduleName: String): ModuleBuildDirs { val moduleBuildDir = buildDir.resolve(moduleName) @@ -125,4 +136,5 @@ object KlibABITestUtils { ) const val MAIN_MODULE_NAME = "main" + private const val ABI_UTILS_DIR = "__utils__" } diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/AbstractJsKLibABITestCase.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/AbstractJsKLibABITestCase.kt index 5253ea0391a..fc57197fff0 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/AbstractJsKLibABITestCase.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/AbstractJsKLibABITestCase.kt @@ -218,12 +218,12 @@ abstract class AbstractJsKLibABITestCase : KtUsefulTestCase() { val psiManager = PsiManager.getInstance(project) val fileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL) as CoreLocalFileSystem - val sourceFile = sourceDir.listFiles()!!.first() - val vFile = fileSystem.findFileByIoFile(sourceFile) ?: error("Virtual File for $sourceFile not found") - - val provider = SingleRootFileViewProvider(psiManager, vFile) - val allfiles = provider.allFiles - return allfiles.mapNotNull { it as? KtFile } + return sourceDir.walkTopDown().filter { file -> + file.isFile && file.extension == "kt" + }.flatMap { file -> + val virtualFile = fileSystem.findFileByIoFile(file) ?: error("VirtualFile for $file not found") + SingleRootFileViewProvider(psiManager, virtualFile).allFiles + }.filterIsInstance().toList() } private fun File.binJsFile(name: String): File = File(this, "$name.js")