[Native][tests] Re-organize TestCaseGroupProvider

This commit is contained in:
Dmitriy Dolovov
2021-11-18 16:16:12 +03:00
parent 5f9bf1611e
commit 6cae6dc5e0
6 changed files with 269 additions and 259 deletions
@@ -7,7 +7,7 @@ package org.jetbrains.kotlin.konan.blackboxtest;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.util.KtTestUtil;
import org.jetbrains.kotlin.konan.blackboxtest.support.NativeBlackBoxTestCaseGroupProvider;
import org.jetbrains.kotlin.konan.blackboxtest.support.group.NativeBlackBoxTestCaseGroupProvider;
import org.jetbrains.kotlin.konan.blackboxtest.support.group.ExtTestCaseGroupProvider;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.jupiter.api.Nested;
@@ -8,7 +8,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.AbstractNativeBlackBoxTest
import org.jetbrains.kotlin.konan.blackboxtest.support.NativeBlackBoxTestCaseGroupProvider
import org.jetbrains.kotlin.konan.blackboxtest.support.group.NativeBlackBoxTestCaseGroupProvider
import org.jetbrains.kotlin.konan.blackboxtest.support.group.ExtTestCaseGroupProvider
fun main() {
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.konan.blackboxtest.support
import org.jetbrains.kotlin.konan.blackboxtest.AbstractNativeBlackBoxTest
import org.jetbrains.kotlin.konan.blackboxtest.support.group.NativeBlackBoxTestCaseGroupProvider
import org.jetbrains.kotlin.konan.blackboxtest.support.group.StandardTestCaseGroupProvider
import org.jetbrains.kotlin.konan.blackboxtest.support.group.TestCaseGroupProvider
import org.jetbrains.kotlin.konan.blackboxtest.support.util.*
@@ -3,7 +3,7 @@
* 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
package org.jetbrains.kotlin.konan.blackboxtest.support.group
import kotlin.reflect.KClass
@@ -0,0 +1,264 @@
/*
* 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.group
import org.jetbrains.kotlin.konan.blackboxtest.support.*
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
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertFalse
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertNotEquals
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.jetbrains.kotlin.test.services.impl.RegisteredDirectivesParser
import java.io.File
internal class StandardTestCaseGroupProvider(private val environment: TestEnvironment) : TestCaseGroupProvider {
// Load test cases in groups on demand.
private val lazyTestCaseGroups = ThreadSafeFactory<File, TestCaseGroup?> { testDataDir ->
val testDataFiles = testDataDir.listFiles()
?: return@ThreadSafeFactory null // `null` means that there is no such testDataDir.
val testCases = testDataFiles.mapNotNull { testDataFile ->
if (!testDataFile.isFile || testDataFile.extension != "kt")
return@mapNotNull null
createTestCase(testDataFile)
}
TestCaseGroup.Default(disabledTestDataFileNames = emptySet(), testCases = testCases)
}
override fun getTestCaseGroup(testDataDir: File) = lazyTestCaseGroups[testDataDir]
private fun createTestCase(testDataFile: File): TestCase {
val generatedSourcesDir = computeGeneratedSourcesDir(
testDataBaseDir = environment.testRoots.baseDir,
testDataFile = testDataFile,
generatedSourcesBaseDir = environment.testSourcesDir
)
val effectivePackageName = computePackageName(
testDataBaseDir = environment.testRoots.baseDir,
testDataFile = testDataFile
)
val testModules = hashMapOf<String, TestModule.Exclusive>()
var currentTestModule: TestModule.Exclusive? = null
var currentTestFileName: String? = null
val currentTestFileText = StringBuilder()
val directivesParser = RegisteredDirectivesParser(TestDirectives, JUnit5Assertions)
var lastParsedDirective: Directive? = null
fun switchTestModule(newTestModule: TestModule.Exclusive, location: Location): TestModule.Exclusive {
// Don't register new test module if there is another one with the same name.
val testModule = testModules.getOrPut(newTestModule.name) { newTestModule }
assertTrue(testModule === newTestModule || testModule.haveSameSymbols(newTestModule)) {
"""
$location: Two declarations of the same module with different dependencies or friends found:
$testModule
$newTestModule
""".trimIndent()
}
currentTestModule = testModule
return testModule
}
fun beginTestFile(fileName: String) {
assertEquals(null, currentTestFileName)
currentTestFileName = fileName
}
fun finishTestFile(forceFinish: Boolean, location: Location) {
val needToFinish = forceFinish
|| currentTestFileName != null
|| (currentTestFileName == null /*&& testFiles.isEmpty()*/ && currentTestFileText.hasAnythingButComments())
if (needToFinish) {
val fileName = currentTestFileName ?: DEFAULT_FILE_NAME
val testModule = currentTestModule ?: switchTestModule(TestModule.newDefaultModule(), location)
testModule.files += TestFile.createUncommitted(
location = generatedSourcesDir.resolve(testModule.name).resolve(fileName),
module = testModule,
text = currentTestFileText
)
currentTestFileText.clear()
repeat(location.lineNumber ?: 0) { currentTestFileText.appendLine() }
currentTestFileName = null
}
}
testDataFile.readLines().forEachIndexed { lineNumber, line ->
val location = Location(testDataFile, lineNumber)
val expectFileDirectiveAfterModuleDirective =
lastParsedDirective == TestDirectives.MODULE // Only FILE directive may follow MODULE directive.
val rawDirective = RegisteredDirectivesParser.parseDirective(line)
if (rawDirective != null) {
val parsedDirective = try {
directivesParser.convertToRegisteredDirective(rawDirective)
} catch (e: AssertionError) {
// Enhance error message with concrete test data file and line number where the error has happened.
throw AssertionError(
"""
$location: Error while parsing directive in test data file.
Cause: ${e.message}
""".trimIndent(),
e
)
}
if (parsedDirective != null) {
when (val directive = parsedDirective.directive) {
TestDirectives.FILE -> {
val newFileName = parseFileName(parsedDirective, location)
finishTestFile(forceFinish = false, location)
beginTestFile(newFileName)
}
else -> {
assertFalse(expectFileDirectiveAfterModuleDirective) {
"$location: Directive $directive encountered after ${TestDirectives.MODULE} directive but was expecting ${TestDirectives.FILE}"
}
when (directive) {
TestDirectives.MODULE -> {
finishTestFile(forceFinish = false, location)
switchTestModule(parseModule(parsedDirective, location), location)
}
else -> {
assertNotEquals(TestDirectives.FILE, lastParsedDirective) {
"$location: Global directive $directive encountered after ${TestDirectives.FILE} directive"
}
assertNotEquals(TestDirectives.MODULE, lastParsedDirective) {
"$location: Global directive $directive encountered after ${TestDirectives.MODULE} directive"
}
directivesParser.addParsedDirective(parsedDirective)
}
}
}
}
currentTestFileText.appendLine()
lastParsedDirective = parsedDirective.directive
return@forEachIndexed
}
}
if (expectFileDirectiveAfterModuleDirective) {
// Was expecting a line with the FILE directive as this is the only possible continuation of a line with
// the MODULE directive, but failed.
fail { "$location: ${TestDirectives.FILE} directive expected after ${TestDirectives.MODULE} directive" }
}
currentTestFileText.appendLine(line)
}
val location = Location(testDataFile)
finishTestFile(forceFinish = true, location)
val registeredDirectives = directivesParser.build()
val freeCompilerArgs = parseFreeCompilerArgs(registeredDirectives, location)
val expectedOutputDataFile = parseOutputDataFile(baseDir = testDataFile.parentFile, registeredDirectives, location)
val testKind = parseTestKind(registeredDirectives, location)
if (testKind == TestKind.REGULAR) {
// Fix package declarations to avoid unintended conflicts between symbols with the same name in different test cases.
testModules.values.forEach { testModule ->
testModule.files.forEach { testFile -> fixPackageDeclaration(testFile, effectivePackageName, testDataFile) }
}
}
val testCase = TestCase(
kind = testKind,
modules = testModules.values.toSet(),
freeCompilerArgs = freeCompilerArgs,
origin = TestOrigin.SingleTestDataFile(testDataFile),
nominalPackageName = effectivePackageName,
expectedOutputDataFile = expectedOutputDataFile,
extras = if (testKind == TestKind.STANDALONE_NO_TR) {
TestCase.StandaloneNoTestRunnerExtras(
entryPoint = parseEntryPoint(registeredDirectives, location),
inputDataFile = parseInputDataFile(baseDir = testDataFile.parentFile, registeredDirectives, location)
)
} else
null
)
testCase.initialize(findSharedModule = null)
return testCase
}
private fun CharSequence.hasAnythingButComments(): Boolean {
var result = false
runForFirstMeaningfulStatement { _, _ -> result = true }
return result
}
private fun fixPackageDeclaration(
testFile: TestFile<TestModule.Exclusive>,
packageName: PackageName,
testDataFile: File
) = testFile.update { text ->
var existingPackageDeclarationLine: String? = null
var existingPackageDeclarationLineNumber: Int? = null
text.runForFirstMeaningfulStatement { lineNumber, line ->
// First meaningful line.
val trimmedLine = line.trim()
if (trimmedLine.startsWith("package ")) {
existingPackageDeclarationLine = trimmedLine
existingPackageDeclarationLineNumber = lineNumber
}
}
if (existingPackageDeclarationLine != null) {
val existingPackageName = existingPackageDeclarationLine!!.substringAfter("package ").trimStart()
assertTrue(
existingPackageName == packageName
|| (existingPackageName.length > packageName.length
&& existingPackageName.startsWith(packageName)
&& existingPackageName[packageName.length] == '.')
) {
val location = Location(testDataFile, existingPackageDeclarationLineNumber)
"""
$location: Invalid package name declaration found: $existingPackageDeclarationLine
Expected: package $packageName
""".trimIndent()
}
text
} else
"package $packageName $text"
}
private inline fun CharSequence.runForFirstMeaningfulStatement(action: (lineNumber: Int, line: String) -> Unit) {
var inMultilineComment = false
for ((lineNumber, line) in lines().withIndex()) {
val trimmedLine = line.trim()
when {
inMultilineComment -> inMultilineComment = !trimmedLine.endsWith("*/")
trimmedLine.startsWith("/*") -> inMultilineComment = true
trimmedLine.isMeaningfulLine() -> {
action(lineNumber, line)
break
}
}
}
}
private fun String.isMeaningfulLine() = isNotEmpty() && !startsWith("//") && !startsWith("@file:")
}
@@ -5,264 +5,9 @@
package org.jetbrains.kotlin.konan.blackboxtest.support.group
import org.jetbrains.kotlin.konan.blackboxtest.support.*
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
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertFalse
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertNotEquals
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.jetbrains.kotlin.test.services.impl.RegisteredDirectivesParser
import org.jetbrains.kotlin.konan.blackboxtest.support.TestCaseGroup
import java.io.File
internal interface TestCaseGroupProvider {
fun getTestCaseGroup(testDataDir: File): TestCaseGroup?
}
internal class StandardTestCaseGroupProvider(private val environment: TestEnvironment) : TestCaseGroupProvider {
// Load test cases in groups on demand.
private val lazyTestCaseGroups = ThreadSafeFactory<File, TestCaseGroup?> { testDataDir ->
val testDataFiles = testDataDir.listFiles()
?: return@ThreadSafeFactory null // `null` means that there is no such testDataDir.
val testCases = testDataFiles.mapNotNull { testDataFile ->
if (!testDataFile.isFile || testDataFile.extension != "kt")
return@mapNotNull null
createTestCase(testDataFile)
}
TestCaseGroup.Default(disabledTestDataFileNames = emptySet(), testCases = testCases)
}
override fun getTestCaseGroup(testDataDir: File) = lazyTestCaseGroups[testDataDir]
private fun createTestCase(testDataFile: File): TestCase {
val generatedSourcesDir = computeGeneratedSourcesDir(
testDataBaseDir = environment.testRoots.baseDir,
testDataFile = testDataFile,
generatedSourcesBaseDir = environment.testSourcesDir
)
val effectivePackageName = computePackageName(
testDataBaseDir = environment.testRoots.baseDir,
testDataFile = testDataFile
)
val testModules = hashMapOf<String, TestModule.Exclusive>()
var currentTestModule: TestModule.Exclusive? = null
var currentTestFileName: String? = null
val currentTestFileText = StringBuilder()
val directivesParser = RegisteredDirectivesParser(TestDirectives, JUnit5Assertions)
var lastParsedDirective: Directive? = null
fun switchTestModule(newTestModule: TestModule.Exclusive, location: Location): TestModule.Exclusive {
// Don't register new test module if there is another one with the same name.
val testModule = testModules.getOrPut(newTestModule.name) { newTestModule }
assertTrue(testModule === newTestModule || testModule.haveSameSymbols(newTestModule)) {
"""
$location: Two declarations of the same module with different dependencies or friends found:
$testModule
$newTestModule
""".trimIndent()
}
currentTestModule = testModule
return testModule
}
fun beginTestFile(fileName: String) {
assertEquals(null, currentTestFileName)
currentTestFileName = fileName
}
fun finishTestFile(forceFinish: Boolean, location: Location) {
val needToFinish = forceFinish
|| currentTestFileName != null
|| (currentTestFileName == null /*&& testFiles.isEmpty()*/ && currentTestFileText.hasAnythingButComments())
if (needToFinish) {
val fileName = currentTestFileName ?: DEFAULT_FILE_NAME
val testModule = currentTestModule ?: switchTestModule(TestModule.newDefaultModule(), location)
testModule.files += TestFile.createUncommitted(
location = generatedSourcesDir.resolve(testModule.name).resolve(fileName),
module = testModule,
text = currentTestFileText
)
currentTestFileText.clear()
repeat(location.lineNumber ?: 0) { currentTestFileText.appendLine() }
currentTestFileName = null
}
}
testDataFile.readLines().forEachIndexed { lineNumber, line ->
val location = Location(testDataFile, lineNumber)
val expectFileDirectiveAfterModuleDirective =
lastParsedDirective == TestDirectives.MODULE // Only FILE directive may follow MODULE directive.
val rawDirective = RegisteredDirectivesParser.parseDirective(line)
if (rawDirective != null) {
val parsedDirective = try {
directivesParser.convertToRegisteredDirective(rawDirective)
} catch (e: AssertionError) {
// Enhance error message with concrete test data file and line number where the error has happened.
throw AssertionError(
"""
$location: Error while parsing directive in test data file.
Cause: ${e.message}
""".trimIndent(),
e
)
}
if (parsedDirective != null) {
when (val directive = parsedDirective.directive) {
TestDirectives.FILE -> {
val newFileName = parseFileName(parsedDirective, location)
finishTestFile(forceFinish = false, location)
beginTestFile(newFileName)
}
else -> {
assertFalse(expectFileDirectiveAfterModuleDirective) {
"$location: Directive $directive encountered after ${TestDirectives.MODULE} directive but was expecting ${TestDirectives.FILE}"
}
when (directive) {
TestDirectives.MODULE -> {
finishTestFile(forceFinish = false, location)
switchTestModule(parseModule(parsedDirective, location), location)
}
else -> {
assertNotEquals(TestDirectives.FILE, lastParsedDirective) {
"$location: Global directive $directive encountered after ${TestDirectives.FILE} directive"
}
assertNotEquals(TestDirectives.MODULE, lastParsedDirective) {
"$location: Global directive $directive encountered after ${TestDirectives.MODULE} directive"
}
directivesParser.addParsedDirective(parsedDirective)
}
}
}
}
currentTestFileText.appendLine()
lastParsedDirective = parsedDirective.directive
return@forEachIndexed
}
}
if (expectFileDirectiveAfterModuleDirective) {
// Was expecting a line with the FILE directive as this is the only possible continuation of a line with
// the MODULE directive, but failed.
fail { "$location: ${TestDirectives.FILE} directive expected after ${TestDirectives.MODULE} directive" }
}
currentTestFileText.appendLine(line)
}
val location = Location(testDataFile)
finishTestFile(forceFinish = true, location)
val registeredDirectives = directivesParser.build()
val freeCompilerArgs = parseFreeCompilerArgs(registeredDirectives, location)
val expectedOutputDataFile = parseOutputDataFile(baseDir = testDataFile.parentFile, registeredDirectives, location)
val testKind = parseTestKind(registeredDirectives, location)
if (testKind == TestKind.REGULAR) {
// Fix package declarations to avoid unintended conflicts between symbols with the same name in different test cases.
testModules.values.forEach { testModule ->
testModule.files.forEach { testFile -> fixPackageDeclaration(testFile, effectivePackageName, testDataFile) }
}
}
val testCase = TestCase(
kind = testKind,
modules = testModules.values.toSet(),
freeCompilerArgs = freeCompilerArgs,
origin = TestOrigin.SingleTestDataFile(testDataFile),
nominalPackageName = effectivePackageName,
expectedOutputDataFile = expectedOutputDataFile,
extras = if (testKind == TestKind.STANDALONE_NO_TR) {
TestCase.StandaloneNoTestRunnerExtras(
entryPoint = parseEntryPoint(registeredDirectives, location),
inputDataFile = parseInputDataFile(baseDir = testDataFile.parentFile, registeredDirectives, location)
)
} else
null
)
testCase.initialize(findSharedModule = null)
return testCase
}
private fun CharSequence.hasAnythingButComments(): Boolean {
var result = false
runForFirstMeaningfulStatement { _, _ -> result = true }
return result
}
private fun fixPackageDeclaration(
testFile: TestFile<TestModule.Exclusive>,
packageName: PackageName,
testDataFile: File
) = testFile.update { text ->
var existingPackageDeclarationLine: String? = null
var existingPackageDeclarationLineNumber: Int? = null
text.runForFirstMeaningfulStatement { lineNumber, line ->
// First meaningful line.
val trimmedLine = line.trim()
if (trimmedLine.startsWith("package ")) {
existingPackageDeclarationLine = trimmedLine
existingPackageDeclarationLineNumber = lineNumber
}
}
if (existingPackageDeclarationLine != null) {
val existingPackageName = existingPackageDeclarationLine!!.substringAfter("package ").trimStart()
assertTrue(
existingPackageName == packageName
|| (existingPackageName.length > packageName.length
&& existingPackageName.startsWith(packageName)
&& existingPackageName[packageName.length] == '.')
) {
val location = Location(testDataFile, existingPackageDeclarationLineNumber)
"""
$location: Invalid package name declaration found: $existingPackageDeclarationLine
Expected: package $packageName
""".trimIndent()
}
text
} else
"package $packageName $text"
}
private inline fun CharSequence.runForFirstMeaningfulStatement(action: (lineNumber: Int, line: String) -> Unit) {
var inMultilineComment = false
for ((lineNumber, line) in lines().withIndex()) {
val trimmedLine = line.trim()
when {
inMultilineComment -> inMultilineComment = !trimmedLine.endsWith("*/")
trimmedLine.startsWith("/*") -> inMultilineComment = true
trimmedLine.isMeaningfulLine() -> {
action(lineNumber, line)
break
}
}
}
}
private fun String.isMeaningfulLine() = isNotEmpty() && !startsWith("//") && !startsWith("@file:")
}