[Native][tests] Extract all external source-transformation logic into AbstractExternalNativeBlackBoxTest

This abstract class is used only for generated external codegen box tests that may have custom source transformers.
This commit is contained in:
Dmitriy Dolovov
2021-12-13 12:02:59 +03:00
parent 72bb38dc68
commit a440864de4
10 changed files with 122 additions and 114 deletions
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.generators.tests
import org.jetbrains.kotlin.generators.generateTestGroupSuiteWithJUnit5
import org.jetbrains.kotlin.generators.model.annotation
import org.jetbrains.kotlin.konan.blackboxtest.AbstractExternalNativeBlackBoxTest
import org.jetbrains.kotlin.konan.blackboxtest.AbstractNativeBlackBoxTest
import org.jetbrains.kotlin.konan.blackboxtest.support.group.UseExtTestCaseGroupProvider
import org.jetbrains.kotlin.konan.blackboxtest.support.group.UseStandardTestCaseGroupProvider
@@ -19,7 +20,7 @@ fun main() {
generateTestGroupSuiteWithJUnit5 {
// External box tests.
testGroup("native/native.tests/tests-gen", "compiler/testData") {
testClass<AbstractNativeBlackBoxTest>(
testClass<AbstractExternalNativeBlackBoxTest>(
suiteTestClassName = "ExternalTestGenerated",
annotations = listOf(external(), provider<UseExtTestCaseGroupProvider>())
) {
@@ -0,0 +1,27 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.konan.blackboxtest
import com.intellij.testFramework.TestDataFile
import org.jetbrains.kotlin.konan.blackboxtest.support.util.ExternalSourceTransformer
import org.jetbrains.kotlin.konan.blackboxtest.support.util.ExternalSourceTransformers
import org.jetbrains.kotlin.konan.blackboxtest.support.util.ExternalSourceTransformersProvider
import org.jetbrains.kotlin.konan.blackboxtest.support.util.ThreadSafeCache
import org.jetbrains.kotlin.konan.blackboxtest.support.util.getAbsoluteFile
import java.io.File
abstract class AbstractExternalNativeBlackBoxTest : ExternalSourceTransformersProvider, AbstractNativeBlackBoxTest() {
private val registeredSourceTransformers: ThreadSafeCache<File, MutableList<ExternalSourceTransformer>> = ThreadSafeCache()
override fun getSourceTransformers(testDataFile: File): ExternalSourceTransformers? = registeredSourceTransformers[testDataFile]
/**
* Called directly from test class constructor.
*/
fun register(@TestDataFile testDataFilePath: String, sourceTransformer: ExternalSourceTransformer) {
registeredSourceTransformers.computeIfAbsent(getAbsoluteFile(testDataFilePath)) { mutableListOf() } += sourceTransformer
}
}
@@ -7,31 +7,17 @@ package org.jetbrains.kotlin.konan.blackboxtest
import com.intellij.testFramework.TestDataFile
import org.jetbrains.kotlin.konan.blackboxtest.support.*
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageName
import org.jetbrains.kotlin.konan.blackboxtest.support.TestCaseId
import org.jetbrains.kotlin.konan.blackboxtest.support.TestRun
import org.jetbrains.kotlin.konan.blackboxtest.support.TestRunProvider
import org.jetbrains.kotlin.konan.blackboxtest.support.util.*
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TreeNode
import org.jetbrains.kotlin.konan.blackboxtest.support.util.getAbsoluteFile
import org.jetbrains.kotlin.konan.blackboxtest.support.util.joinPackageNames
import org.jetbrains.kotlin.konan.blackboxtest.support.util.prependPackageName
import org.junit.jupiter.api.DynamicNode
import org.junit.jupiter.api.DynamicTest.dynamicTest
import org.junit.jupiter.api.extension.ExtendWith
import java.io.File
@ExtendWith(NativeBlackBoxTestSupport::class)
abstract class AbstractNativeBlackBoxTest {
internal lateinit var testRunProvider: TestRunProvider
private val toBeRegistered = mutableListOf<Pair<File, List<(String) -> String>>>()
internal fun onRunProviderSet() {
for ((file, transformer) in toBeRegistered) {
testRunProvider.setProcessors(file, transformer)
}
}
fun register(@TestDataFile testDataFilePath: String, sourceTransformers: List<(String) -> String>) =
toBeRegistered.add(File(testDataFilePath) to sourceTransformers)
fun register(@TestDataFile testDataFilePath: String, sourceTransformer: (String) -> String) =
register(testDataFilePath, listOf(sourceTransformer))
/**
* Run JUnit test.
@@ -14,6 +14,7 @@ import org.jetbrains.kotlin.test.TestMetadata
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertEquals
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.jetbrains.kotlin.utils.addToStdlib.cast
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
@@ -32,7 +33,6 @@ class NativeBlackBoxTestSupport : BeforeEachCallback {
*/
override fun beforeEach(extensionContext: ExtensionContext): Unit = with(extensionContext) {
enclosingTestInstance.testRunProvider = getOrCreateTestRunProvider()
enclosingTestInstance.onRunProviderSet()
// Set the essential compiler property.
System.setProperty("kotlin.native.home", getOrCreateGlobalSettings().kotlinNativeHome.path)
@@ -46,9 +46,15 @@ class NativeBlackBoxTestSupport : BeforeEachCallback {
val enclosingTestClass = enclosingTestClass
return root.getStore(NAMESPACE).getOrComputeIfAbsent(enclosingTestClass.sanitizedName) {
val (testSettings, testSettingsAnnotation) = computeTestSettings(enclosingTestClass)
val requiredSettings: Set<KClass<*>> =
/* Always required */ setOf(GlobalSettings::class, Binaries::class) +/* Custom */ testSettings.requiredSettings
val computedTestSettings = computeTestSettings(enclosingTestClass)
val requiredSettings: Set<KClass<*>> = buildSet {
// Always required:
this += GlobalSettings::class
this += Binaries::class
// Custom:
addAll(computedTestSettings.testSettings.requiredSettings)
}
val globalSettings = getOrCreateGlobalSettings()
@@ -62,7 +68,7 @@ class NativeBlackBoxTestSupport : BeforeEachCallback {
}
})
val testCaseGroupProvider = createTestCaseGroupProvider(settings, testSettings, testSettingsAnnotation)
val testCaseGroupProvider = createTestCaseGroupProvider(settings, computedTestSettings, enclosingTestInstance)
TestRunProvider(settings, testCaseGroupProvider)
}.cast()
@@ -80,11 +86,11 @@ class NativeBlackBoxTestSupport : BeforeEachCallback {
GlobalSettings()
}.cast()
private fun computeTestSettings(enclosingTestClass: Class<*>): Pair<TestSettings, Annotation?> {
val findTestSettings: Class<*>.() -> Pair<TestSettings, Annotation>? = {
private fun computeTestSettings(enclosingTestClass: Class<*>): ComputedTestSettings {
val findTestSettings: Class<*>.() -> ComputedTestSettings? = {
annotations.asSequence().mapNotNull { annotation ->
val testSettings = annotation.annotationClass.findAnnotation<TestSettings>() ?: return@mapNotNull null
testSettings to annotation
ComputedTestSettings(testSettings, annotation)
}.firstOrNull()
}
@@ -147,29 +153,29 @@ class NativeBlackBoxTestSupport : BeforeEachCallback {
private fun createTestCaseGroupProvider(
settings: Settings,
testSettings: TestSettings,
testSettingsAnnotation: Annotation?
computedTestSettings: ComputedTestSettings,
enclosingTestInstance: AbstractNativeBlackBoxTest
): TestCaseGroupProvider {
// Try to find a constructor that accepts the setting and the annotation.
if (testSettingsAnnotation != null) {
val testSettingsAnnotationClass = testSettingsAnnotation.annotationClass
testSettings.providerClass.constructors.asSequence()
.forEach { c ->
val (p1, p2) = c.parameters.takeIf { it.size == 2 } ?: return@forEach
if (p1.hasTypeOf<Settings>() && p2.hasTypeOf(testSettingsAnnotationClass))
return c.call(settings, testSettingsAnnotation).cast()
}
val (testSettings: TestSettings, testSettingsAnnotation: Annotation?) = computedTestSettings
val providerClass: KClass<out TestCaseGroupProvider> = testSettings.providerClass
// Assumption: For simplicitys sake TestCaseGroupProvider has just one constructor.
val constructor = providerClass.constructors.singleOrNull()
?: fail { "No or multiple constructors found for $providerClass" }
val testSettingsAnnotationClass: KClass<out Annotation>? = testSettingsAnnotation?.annotationClass
val sourceTransformersProvider = enclosingTestInstance.safeAs<ExternalSourceTransformersProvider>()
val arguments = constructor.parameters.map { parameter ->
when {
parameter.hasTypeOf<Settings>() -> settings
sourceTransformersProvider != null && parameter.hasTypeOf<ExternalSourceTransformersProvider>() -> sourceTransformersProvider
testSettingsAnnotationClass != null && parameter.hasTypeOf(testSettingsAnnotationClass) -> testSettingsAnnotation
else -> fail { "Can't provide all arguments for $constructor" }
}
}
// ... or the settings at least.
testSettings.providerClass.constructors.asSequence()
.forEach { c ->
val p = c.parameters.singleOrNull() ?: return@forEach
if (p.hasTypeOf<Settings>())
return c.call(settings).cast()
}
fail { "No suitable constructor for ${testSettings.providerClass}" }
return constructor.call(*arguments.toTypedArray()).cast()
}
private inline fun <reified T : Any> KParameter.hasTypeOf(): Boolean = hasTypeOf(T::class)
@@ -178,3 +184,5 @@ class NativeBlackBoxTestSupport : BeforeEachCallback {
private val TestMetadata.testRoot: File get() = getAbsoluteFile(localPath = value)
}
}
private data class ComputedTestSettings(val testSettings: TestSettings, val annotation: Annotation?)
@@ -17,15 +17,14 @@ import org.jetbrains.kotlin.konan.blackboxtest.support.runner.LocalTestNameExtra
import org.jetbrains.kotlin.konan.blackboxtest.support.runner.LocalTestRunner
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.GlobalSettings
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.Settings
import org.jetbrains.kotlin.konan.blackboxtest.support.util.*
import org.jetbrains.kotlin.konan.blackboxtest.support.util.ThreadSafeCache
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TreeNode
import org.jetbrains.kotlin.konan.blackboxtest.support.util.buildTree
import org.jetbrains.kotlin.konan.blackboxtest.support.util.startsWith
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.extension.ExtensionContext
import java.io.File
internal class TestRunProvider(
private val settings: Settings,
@@ -35,10 +34,6 @@ internal class TestRunProvider(
private val cachedCompilations = ThreadSafeCache<TestCompilationCacheKey, TestCompilation>()
private val cachedTestNames = ThreadSafeCache<TestCompilationCacheKey, Collection<TestName>>()
fun setProcessors(testDataFile: File, sourceTransformers: List<(String) -> String>) {
testCaseGroupProvider.setPreprocessors(testDataFile, sourceTransformers)
}
/**
* Produces a single [TestRun] per [TestCase]. So-called "one test case/one test run" mode.
*
@@ -41,9 +41,9 @@ import org.jetbrains.kotlin.utils.addToStdlib.cast
import java.io.File
internal class ExtTestCaseGroupProvider(
private val settings: Settings
private val settings: Settings,
private val sourceTransformersProvider: ExternalSourceTransformersProvider
) : TestCaseGroupProvider, TestDisposable(parentDisposable = settings) {
private val sourceTransformers: MutableMap<String, List<(String) -> String>> = mutableMapOf()
private val structureFactory = ExtTestDataFileStructureFactory(parentDisposable = this)
private val sharedModules = ThreadSafeCache<String, TestModule.Shared?>()
@@ -61,9 +61,7 @@ internal class ExtTestCaseGroupProvider(
val testCases = mutableListOf<TestCase>()
testDataFiles.forEach { testDataFile ->
val extTestDataFile = ExtTestDataFile(
settings, structureFactory, testDataFile, sourceTransformers[testDataFile.canonicalPath] ?: listOf()
)
val extTestDataFile = ExtTestDataFile(settings, structureFactory, sourceTransformersProvider, testDataFile)
if (extTestDataFile.isRelevant)
testCases += extTestDataFile.createTestCase(
@@ -77,13 +75,6 @@ internal class ExtTestCaseGroupProvider(
TestCaseGroup.Default(disabledTestCaseIds, testCases)
}
override fun setPreprocessors(testDataDir: File, preprocessors: List<(String) -> String>) {
if (preprocessors.isNotEmpty())
sourceTransformers[testDataDir.canonicalPath] = preprocessors
else
sourceTransformers.remove(testDataDir.canonicalPath)
}
override fun getTestCaseGroup(testCaseGroupId: TestCaseGroupId): TestCaseGroup? {
assertNotDisposed()
assertTrue(testCaseGroupId is TestCaseGroupId.TestDataDir)
@@ -133,15 +124,17 @@ internal class ExtTestCaseGroupProvider(
private class ExtTestDataFile(
private val settings: Settings,
structureFactory: ExtTestDataFileStructureFactory,
private val testDataFile: File,
sourceTransformers: List<(String) -> String>
sourceTransformersProvider: ExternalSourceTransformersProvider,
private val testDataFile: File
) {
private val structure by lazy {
structureFactory.ExtTestDataFileStructure(testDataFile, sourceTransformers) { line ->
// Remove all diagnostic parameters from the text. Examples:
// <!NO_TAIL_CALLS_FOUND!>, <!NON_TAIL_RECURSIVE_CALL!>, <!>.
line.replace(DIAGNOSTIC_REGEX) { match -> match.groupValues[1] }
}
val customTransformers: ExternalSourceTransformers? = sourceTransformersProvider.getSourceTransformers(testDataFile)
val allTransformers: ExternalSourceTransformers = if (customTransformers.isNullOrEmpty())
MANDATORY_SOURCE_TRANSFORMERS
else
MANDATORY_SOURCE_TRANSFORMERS + customTransformers
structureFactory.ExtTestDataFileStructure(testDataFile, allTransformers)
}
private val testDataFileSettings by lazy {
@@ -633,14 +626,14 @@ private class ExtTestDataFile(
private fun Directives.multiValues(key: String, predicate: (String) -> Boolean = { true }): Set<String> =
listValues(key)?.flatMap { it.split(' ') }?.filter(predicate)?.toSet().orEmpty()
private val DIAGNOSTIC_REGEX = Regex("<!.*?!>(.*?)<!>")
private const val THREAD_LOCAL_ANNOTATION = "@kotlin.native.ThreadLocal"
private val BOX_FUNCTION_NAME = Name.identifier("box")
private val OPT_IN_ANNOTATION_NAME = Name.identifier("OptIn")
private val HELPERS_PACKAGE_NAME = Name.identifier("helpers")
private val TYPE_OF_NAME = Name.identifier("typeOf")
private val MANDATORY_SOURCE_TRANSFORMERS: ExternalSourceTransformers = listOf(DiagnosticsRemovingSourceTransformer)
}
}
@@ -659,16 +652,12 @@ private typealias SharedModuleCache = (moduleName: String, generator: SharedModu
private class ExtTestDataFileStructureFactory(parentDisposable: Disposable) : TestDisposable(parentDisposable) {
private val psiFactory = createPsiFactory(parentDisposable = this)
inner class ExtTestDataFileStructure(
originalTestDataFile: File,
sourceTransformers: List<(String) -> String>,
initialCleanUpTransformation: (String) -> String
) {
inner class ExtTestDataFileStructure(originalTestDataFile: File, sourceTransformers: ExternalSourceTransformers) {
init {
assertNotDisposed()
}
private val filesAndModules = FilesAndModules(originalTestDataFile, initialCleanUpTransformation, sourceTransformers)
private val filesAndModules = FilesAndModules(originalTestDataFile, sourceTransformers)
val directives: Directives get() = filesAndModules.directives
@@ -824,16 +813,12 @@ private class ExtTestDataFileStructureFactory(parentDisposable: Disposable) : Te
ExtTestModule(name, dependencies, friends)
}
private inner class FilesAndModules(
originalTestDataFile: File,
initialCleanUpTransformation: (String) -> String,
sourceTransformers: List<(String) -> String>
) {
private inner class FilesAndModules(originalTestDataFile: File, sourceTransformers: ExternalSourceTransformers) {
private val testFileFactory = ExtTestFileFactory()
private val generatedFiles = TestFiles.createTestFiles(
/* testFileName = */ DEFAULT_FILE_NAME,
/* expectedText = */ originalTestDataFile.applySourceTransformers(sourceTransformers),
/* expectedText = */ originalTestDataFile.readText(),
/* factory = */ testFileFactory,
/* preserveLocations = */ true
)
@@ -842,7 +827,7 @@ private class ExtTestDataFileStructureFactory(parentDisposable: Disposable) : Te
// Clean up contents of every individual test file. Important: This should be done only after parsing testData file,
// because parsing of testData file relies on certain directives which could be removed by the transformation.
generatedFiles.forEach { file ->
file.text = file.text.lineSequence().joinToString("\n", transform = initialCleanUpTransformation)
file.text = sourceTransformers.fold(file.text) { source, transformer -> transformer(source) }
}
val modules = generatedFiles.map { it.module }.associateBy { it.name }
@@ -65,10 +65,6 @@ internal class PredefinedTestCaseGroupProvider(private val settings: Settings, a
TestCaseGroup.Default(disabledTestCaseIds = emptySet(), testCases = listOf(testCase))
}
override fun setPreprocessors(testDataDir: File, preprocessors: List<(String) -> String>) {
// do nothing
}
override fun getTestCaseGroup(testCaseGroupId: TestCaseGroupId): TestCaseGroup? {
assertTrue(testCaseGroupId is TestCaseGroupId.Named)
return lazyTestCaseGroups[testCaseGroupId.cast()]
@@ -12,10 +12,6 @@ import org.jetbrains.kotlin.konan.blackboxtest.support.settings.GeneratedSources
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.Settings
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.TestRoots
import org.jetbrains.kotlin.konan.blackboxtest.support.util.*
import org.jetbrains.kotlin.konan.blackboxtest.support.util.DEFAULT_FILE_NAME
import org.jetbrains.kotlin.konan.blackboxtest.support.util.ThreadSafeFactory
import org.jetbrains.kotlin.konan.blackboxtest.support.util.computeGeneratedSourcesDir
import org.jetbrains.kotlin.konan.blackboxtest.support.util.computePackageName
import org.jetbrains.kotlin.test.directives.model.Directive
import org.jetbrains.kotlin.test.services.JUnit5Assertions
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertEquals
@@ -45,13 +41,6 @@ internal class StandardTestCaseGroupProvider(private val settings: Settings) : T
TestCaseGroup.Default(disabledTestCaseIds = emptySet(), testCases = testCases)
}
override fun setPreprocessors(testDataDir: File, preprocessors: List<(String) -> String>) {
if (preprocessors.isNotEmpty())
sourceTransformers[testDataDir.canonicalPath] = preprocessors
else
sourceTransformers.remove(testDataDir.canonicalPath)
}
override fun getTestCaseGroup(testCaseGroupId: TestCaseGroupId): TestCaseGroup? {
assertTrue(testCaseGroupId is TestCaseGroupId.TestDataDir)
return lazyTestCaseGroups[testCaseGroupId.cast()]
@@ -101,7 +90,7 @@ internal class StandardTestCaseGroupProvider(private val settings: Settings) : T
fun finishTestFile(forceFinish: Boolean, location: Location) {
val needToFinish = forceFinish
|| currentTestFileName != null
|| (currentTestFileName == null /*&& testFiles.isEmpty()*/ && currentTestFileText.hasAnythingButComments())
|| (/*currentTestFileName == null && testFiles.isEmpty() &&*/ currentTestFileText.hasAnythingButComments())
if (needToFinish) {
val fileName = currentTestFileName ?: DEFAULT_FILE_NAME
@@ -119,8 +108,7 @@ internal class StandardTestCaseGroupProvider(private val settings: Settings) : T
}
}
val text = testDataFile.applySourceTransformers(sourceTransformers[testDataFile.canonicalPath] ?: listOf())
text.lines().forEachIndexed { lineNumber, line ->
testDataFile.readLines().forEachIndexed { lineNumber, line ->
val location = Location(testDataFile, lineNumber)
val expectFileDirectiveAfterModuleDirective =
lastParsedDirective == TestDirectives.MODULE // Only FILE directive may follow MODULE directive.
@@ -7,15 +7,7 @@ package org.jetbrains.kotlin.konan.blackboxtest.support.group
import org.jetbrains.kotlin.konan.blackboxtest.support.TestCaseGroup
import org.jetbrains.kotlin.konan.blackboxtest.support.TestCaseGroupId
import java.io.File
internal interface TestCaseGroupProvider {
fun setPreprocessors(testDataDir: File, preprocessors: List<(String) -> String>)
fun getTestCaseGroup(testCaseGroupId: TestCaseGroupId): TestCaseGroup?
}
internal fun String.applySourceTransformers(sourceTransformers: List<(String) -> String>) =
sourceTransformers.fold(this) { source, transformer -> transformer(source) }
internal fun File.applySourceTransformers(sourceTransformers: List<(String) -> String>) =
readText().applySourceTransformers(sourceTransformers)
}
@@ -0,0 +1,30 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.konan.blackboxtest.support.util
import org.jetbrains.kotlin.konan.blackboxtest.AbstractExternalNativeBlackBoxTest
import java.io.File
/**
* This relates only to external codegen tests (see [AbstractExternalNativeBlackBoxTest]) that may have their own source transformers.
*/
internal typealias ExternalSourceTransformer = (/* file contents */ String) -> /* patched file contents */ String
internal typealias ExternalSourceTransformers = List<ExternalSourceTransformer>
internal interface ExternalSourceTransformersProvider {
fun getSourceTransformers(testDataFile: File): ExternalSourceTransformers?
}
internal object DiagnosticsRemovingSourceTransformer : ExternalSourceTransformer {
override fun invoke(source: String) = source.lineSequence().joinToString("\n") { line ->
// Remove all diagnostic parameters from the text. Examples:
// <!NO_TAIL_CALLS_FOUND!>, <!NON_TAIL_RECURSIVE_CALL!>, <!>.
line.replace(DIAGNOSTIC_REGEX) { match -> match.groupValues[1] }
}
private val DIAGNOSTIC_REGEX = Regex("<!.*?!>(.*?)<!>")
}