[IR][tests] Make ABI compatibility tests less verbose, part 1

This commit is contained in:
Dmitriy Dolovov
2022-08-23 14:50:48 +02:00
parent 437d6b8391
commit db1c8cc7a7
3 changed files with 174 additions and 11 deletions
+151
View File
@@ -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<Any?>)
fun expectSuccess(block: Block<String>) // OK is expected
fun <T : Any> expectSuccess(expectedOutcome: T, block: Block<T>)
}
const val OK_STATUS = "OK"
sealed interface ErrorMessagePattern
private typealias Block<T> = () -> 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<Test>()
override fun prefixed(prefix: String) = PrefixOfErrorMessage(prefix)
override fun nonImplementedCallable(callableTypeAndName: String, classifierTypeAndName: String) =
NonImplementedCallable(callableTypeAndName, classifierTypeAndName)
override fun expectFailure(errorMessagePattern: ErrorMessagePattern, block: Block<Any?>) {
tests += FailingTest(errorMessagePattern, block)
}
override fun expectSuccess(block: Block<String>) = expectSuccess(OK_STATUS, block)
override fun <T : Any> expectSuccess(expectedOutcome: T, block: Block<T>) {
tests += SuccessfulTest(expectedOutcome, block)
}
fun check() {
check(tests.isNotEmpty()) { "No ABI tests configured" }
}
fun runTests(): String {
val testFailures: List<TestFailure> = 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<Any?>) : Test
private class SuccessfulTest(val expectedOutcome: Any, val block: Block<Any>) : 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)
@@ -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__"
}
@@ -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<KtFile>().toList()
}
private fun File.binJsFile(name: String): File = File(this, "$name.js")