Implement feature interaction tests generator based on templates

This commit is contained in:
victor.petukhov
2018-09-25 12:51:43 +03:00
parent 6f95e4ae3b
commit 45c5a168a3
10 changed files with 422 additions and 15 deletions
+5 -9
View File
@@ -16,14 +16,10 @@ projectTest {
workingDir = rootDir
}
val generateTests by generator("org.jetbrains.kotlin.generators.tests.GenerateCompilerSpecTestsKt")
val generateSpecTests by generator("org.jetbrains.kotlin.spec.tasks.GenerateSpecTestsKt")
val printSpecTestsStatistic by smartJavaExec {
classpath = javaPluginConvention().sourceSets.getByName("test").runtimeClasspath
main = "org.jetbrains.kotlin.spec.tasks.PrintSpecTestsStatisticKt"
}
val generateFeatureInteractionSpecTestData by generator("org.jetbrains.kotlin.spec.tasks.GenerateFeatureInteractionSpecTestDataKt")
val generateJsonTestsMap by smartJavaExec {
classpath = javaPluginConvention().sourceSets.getByName("test").runtimeClasspath
main = "org.jetbrains.kotlin.spec.tasks.GenerateJsonTestsMapKt"
}
val printSpecTestsStatistic by generator("org.jetbrains.kotlin.spec.tasks.PrintSpecTestsStatisticKt")
val generateJsonTestsMap by generator("org.jetbrains.kotlin.spec.tasks.GenerateJsonTestsMapKt")
@@ -0,0 +1,28 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.spec.generators.featureinteraction
enum class Feature(val config: FeatureTemplatesConfig) {
IDENTIFIERS(
FeatureTemplatesConfig(
FeatureTemplatesType.AS_FILE,
templatesPath = "identifiers"
)
),
BOOLEAN_LITERALS(
FeatureTemplatesConfig(
FeatureTemplatesType.AS_STRING,
templates = mapOf("true" to "true", "false" to "false")
)
),
BOOLEAN_LITERALS_IN_BACKTICKS(
FeatureTemplatesConfig(
FeatureTemplatesType.AS_STRING,
templates = mapOf("trueWithBacktick" to "`true`", "falseWithBacktick" to "`false`"),
validationTransformer = TemplateValidationTransformerType.TRIM_BACKTICKS
)
)
}
@@ -0,0 +1,98 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.spec.generators.featureinteraction
import org.jetbrains.kotlin.spec.utils.GeneralConfiguration.TESTDATA_PATH
import java.io.File
import java.util.regex.Pattern
enum class SubstitutionPassType { FIRST, SECOND }
data class SubstitutionRule(
val tag: SubstitutionTag,
val filename: String,
val testNumber: Int,
val varNumber: Int? = null
)
class FeatureInteractionTestDataGenerator(private val configuration: GenerationSpecTestDataConfig) {
companion object {
const val TEMPLATES_PATH = "templates"
private const val PARAMETER_REGEXP = """(?:".*?"|.*?)"""
private fun getVariablePattern(varRegex: String = ".*?", afterContent: String = "") =
Pattern.compile("""<!(?<varName>$varRegex)(?:\((?<parameters>$PARAMETER_REGEXP(?:,\s*$PARAMETER_REGEXP)*)\))?!>$afterContent""")
private fun String.extractDirectives(): Pair<String, String> {
val matcher = getVariablePattern(
varRegex = SubstitutionTag.DIRECTIVES.name,
afterContent = System.lineSeparator().repeat(2)
).matcher(this)
if (!matcher.find())
return Pair("", this)
val parameters = parseParameters(matcher.group("parameters"))
val directives = parameters.joinToString { "// $it${System.lineSeparator()}" } + System.lineSeparator()
val template = StringBuffer(this.length).let {
matcher.appendReplacement(it, "").appendTail(it).toString()
}
return Pair(directives, template)
}
private fun parseParameters(rawParameters: String) =
rawParameters.split(Regex(""",\s*""")).map { it.trim('"') }
}
private fun String.substitute(
filename: String,
testNumber: Int,
passType: SubstitutionPassType = SubstitutionPassType.FIRST
): String {
val buffer = StringBuffer(this.length)
val matcher = getVariablePattern().matcher(this)
while (matcher.find()) {
val varName = matcher.group("varName")
val rawParameters = matcher.group("parameters")
val varNumber = if (rawParameters != null) parseParameters(rawParameters)[0].toInt() else null
val tag = SubstitutionTag.valueOf(varName)
if (tag.passType != passType)
continue
matcher.appendReplacement(
buffer,
configuration.substitutions[tag]?.invoke(
SubstitutionRule(tag, filename, testNumber, varNumber)
)
)
}
return matcher.appendTail(buffer).toString()
}
fun generate() {
var testNumber = 1
val testsPartPath = "$TESTDATA_PATH/${configuration.getTestsPartPath()}"
val layoutTemplate = File("$TESTDATA_PATH/${configuration.getLayoutPath()}").readText()
File(testsPartPath).parentFile.mkdirs()
for ((filename, template) in configuration.prepareAndGetFirstFeatureTemplates()) {
val (directives, templateWithoutDirectives) = template.extractDirectives()
val code = templateWithoutDirectives
.substitute(filename, testNumber, SubstitutionPassType.FIRST)
.substitute(filename, testNumber, SubstitutionPassType.SECOND)
val layout = layoutTemplate.substitute(filename, testNumber)
val testFilePath = "$testsPartPath$testNumber.kt"
File(testFilePath).writeText(directives + layout + System.lineSeparator().repeat(2) + code)
testNumber++
}
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.spec.generators.featureinteraction
import org.jetbrains.kotlin.spec.utils.GeneralConfiguration.TESTDATA_PATH
import org.jetbrains.kotlin.spec.validators.TestArea
import java.io.File
enum class FeatureTemplatesType {
AS_FILE,
AS_STRING
}
class FeatureTemplatesConfig(
private val featureTemplatesType: FeatureTemplatesType,
private val templatesPath: String? = null,
private val templates: Map<String, String>? = null,
val validationTransformer: TemplateValidationTransformerType? = null
) {
lateinit var testArea: TestArea
var currentTemplatesIterator = getTemplatesIterator()
private fun getTemplatesPath(testArea: TestArea) = buildString {
append(TESTDATA_PATH)
append("/${testArea.testDataPath}")
append("/${FeatureInteractionTestDataGenerator.TEMPLATES_PATH}")
append("/$templatesPath")
}
private fun getTemplateFiles(testArea: TestArea) =
File(getTemplatesPath(testArea)).walkTopDown().filter { it.extension == "kt" }
.associate { Pair(it.nameWithoutExtension, it.readText()) }
private fun getTemplates(testArea: TestArea) = lazy {
when (featureTemplatesType) {
FeatureTemplatesType.AS_STRING -> templates!!
FeatureTemplatesType.AS_FILE -> getTemplateFiles(testArea)
}
}
private fun getTemplatesIterator() = lazy { getTemplates(testArea).value.iterator() }
fun resetTemplatesIterator() { currentTemplatesIterator = getTemplatesIterator() }
fun getNextWithRepeat() = let {
takeUnless { it.currentTemplatesIterator.value.hasNext() }?.resetTemplatesIterator()
currentTemplatesIterator.value.next().value
}
}
@@ -0,0 +1,129 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.spec.generators.featureinteraction
import org.jetbrains.kotlin.spec.validators.SpecTestLinkedType
import org.jetbrains.kotlin.spec.validators.TestArea
import org.jetbrains.kotlin.spec.validators.TestType
enum class SubstitutionTag(val passType: SubstitutionPassType = SubstitutionPassType.FIRST) {
DIRECTIVES,
// Test meta info tags
TEST_TYPE,
SECTIONS,
CATEGORIES,
PARAGRAPH_NUMBER,
SENTENCE_NUMBER,
SENTENCE,
TEST_NUMBER,
TEST_DESCRIPTION,
// Test data tags
ELEMENT,
ELEMENT_VALIDATION(SubstitutionPassType.SECOND),
CLASS_OF_FILE
}
typealias TemplatesIterator = Iterator<Map.Entry<String, String>>
abstract class GenerationSpecTestDataConfig {
private val repeatableElements = mutableMapOf<Int, String>()
lateinit var testType: TestType
lateinit var testDescription: String
lateinit var firstFeature: Feature
lateinit var secondFeature: Feature
lateinit var testArea: TestArea
protected val baseSubstitutions = mapOf<SubstitutionTag, (SubstitutionRule) -> String>(
SubstitutionTag.TEST_TYPE to { _ -> testType.toString() },
SubstitutionTag.TEST_NUMBER to { rule -> rule.testNumber.toString() },
SubstitutionTag.TEST_DESCRIPTION to { rule -> testDescription.format(rule.filename) },
SubstitutionTag.ELEMENT to { rule ->
val isRepeatableVar = rule.varNumber != null
when {
isRepeatableVar && repeatableElements.contains(rule.varNumber) ->
repeatableElements[rule.varNumber]!!
else -> {
val element = secondFeature.config.getNextWithRepeat()
if (isRepeatableVar)
repeatableElements[rule.varNumber!!] = element
element
}
}
},
SubstitutionTag.ELEMENT_VALIDATION to { rule ->
val validationFunction = secondFeature.config.validationTransformer
val element = repeatableElements[rule.varNumber]!!
when (validationFunction) {
null -> element
else -> templateValidationTransformers[validationFunction]!!(element)
}
}
)
private fun buildTemplatesIterator(originalIterator: TemplatesIterator) =
object : TemplatesIterator {
override fun next() = run {
repeatableElements.clear()
secondFeature.config.resetTemplatesIterator()
originalIterator.next()
}
override fun hasNext() = originalIterator.hasNext()
}
fun getLayoutPath() = "${testArea.testDataPath}/templates/_layout/$layoutFilename"
fun prepareAndGetFirstFeatureTemplates(): TemplatesIterator {
secondFeature.config.testArea = testArea
firstFeature.config.testArea = testArea
return firstFeature.config.run {
resetTemplatesIterator()
buildTemplatesIterator(currentTemplatesIterator.value)
}
}
abstract val layoutFilename: String
abstract val substitutions: MutableMap<SubstitutionTag, (SubstitutionRule) -> String>
abstract fun getTestsPartPath(): String
}
class GenerationLinkedSpecTestDataConfig : GenerationSpecTestDataConfig() {
var paragraphNumber: Int = 0
var sentenceNumber: Int = 0
lateinit var sentence: String
lateinit var sections: List<String>
override val layoutFilename = "linkedTestsLayout.kt"
override val substitutions = mutableMapOf<SubstitutionTag, (SubstitutionRule) -> String>(
SubstitutionTag.SECTIONS to { _ -> sections.joinToString(", ") },
SubstitutionTag.PARAGRAPH_NUMBER to { _ -> paragraphNumber.toString() },
SubstitutionTag.SENTENCE_NUMBER to { _ -> sentenceNumber.toString() },
SubstitutionTag.SENTENCE to { _ -> sentence },
SubstitutionTag.CLASS_OF_FILE to { rule -> "_${sentenceNumber}_${rule.testNumber}Kt" }
).apply { putAll(baseSubstitutions) }
override fun getTestsPartPath() =
"${testArea.testDataPath}/${SpecTestLinkedType.LINKED.testDataPath}/${sections.joinToString("/")}/p-$paragraphNumber/${testType.type}/$sentenceNumber."
}
class GenerationNotLinkedSpecTestDataConfig : GenerationSpecTestDataConfig() {
lateinit var categories: List<String>
override val layoutFilename = "notLinkedTestsLayout.kt"
override val substitutions = mutableMapOf<SubstitutionTag, (SubstitutionRule) -> String>(
SubstitutionTag.CATEGORIES to { _ -> categories.joinToString(", ") },
SubstitutionTag.CLASS_OF_FILE to { rule -> "_${rule.testNumber}Kt" }
).apply { putAll(baseSubstitutions) }
override fun getTestsPartPath() =
"${testArea.testDataPath}/${SpecTestLinkedType.NOT_LINKED.testDataPath}/${categories.joinToString("/")}/${testType.type}/"
}
@@ -0,0 +1,25 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.spec.generators.featureinteraction
import org.jetbrains.kotlin.spec.tasks.generateTests
fun generationSpecTestDataConfigGroup(regenerateTests: Boolean = false, body: () -> Unit) {
body()
if (regenerateTests) generateTests()
}
fun generationLinkedSpecTestDataConfig(body: GenerationLinkedSpecTestDataConfig.() -> Unit) =
GenerationLinkedSpecTestDataConfig().also {
body(it)
FeatureInteractionTestDataGenerator(it).generate()
}
fun generationNotLinkedSpecTestDataConfig(body: GenerationNotLinkedSpecTestDataConfig.() -> Unit) =
GenerationNotLinkedSpecTestDataConfig().also {
body(it)
FeatureInteractionTestDataGenerator(it).generate()
}
@@ -0,0 +1,14 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.spec.generators.featureinteraction
enum class TemplateValidationTransformerType {
TRIM_BACKTICKS
}
val templateValidationTransformers = mapOf<TemplateValidationTransformerType, (String) -> String>(
TemplateValidationTransformerType.TRIM_BACKTICKS to { element -> element.trim('`') }
)
@@ -0,0 +1,50 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.spec.tasks
import org.jetbrains.kotlin.spec.generators.featureinteraction.Feature
import org.jetbrains.kotlin.spec.generators.featureinteraction.generationLinkedSpecTestDataConfig
import org.jetbrains.kotlin.spec.generators.featureinteraction.generationSpecTestDataConfigGroup
import org.jetbrains.kotlin.spec.validators.TestArea
import org.jetbrains.kotlin.spec.validators.TestType
fun main(args: Array<String>) {
generationSpecTestDataConfigGroup(regenerateTests = true) {
generationLinkedSpecTestDataConfig {
testArea = TestArea.PSI
testType = TestType.NEGATIVE
sections = listOf("constant-literals", "boolean-literals")
paragraphNumber = 1
sentenceNumber = 2
sentence = "These are strong keywords which cannot be used as identifiers unless escaped."
testDescription = "The use of Boolean literals as the identifier (without backtick) in the %s."
firstFeature = Feature.IDENTIFIERS
secondFeature = Feature.BOOLEAN_LITERALS
}
generationLinkedSpecTestDataConfig {
testArea = TestArea.PSI
testType = TestType.POSITIVE
sections = listOf("constant-literals", "boolean-literals")
paragraphNumber = 1
sentenceNumber = 2
sentence = "These are strong keywords which cannot be used as identifiers unless escaped."
testDescription = "The use of Boolean literals as the identifier (with backtick) in the %s."
firstFeature = Feature.IDENTIFIERS
secondFeature = Feature.BOOLEAN_LITERALS_IN_BACKTICKS
}
generationLinkedSpecTestDataConfig {
testArea = TestArea.CODEGEN_BOX
testType = TestType.POSITIVE
sections = listOf("constant-literals", "boolean-literals")
paragraphNumber = 1
sentenceNumber = 2
sentence = "These are strong keywords which cannot be used as identifiers unless escaped."
testDescription = "The use of Boolean literals as the identifier (with backtick) in the %s."
firstFeature = Feature.IDENTIFIERS
secondFeature = Feature.BOOLEAN_LITERALS_IN_BACKTICKS
}
}
}
@@ -6,19 +6,20 @@
package org.jetbrains.kotlin.spec.tasks
import com.google.gson.JsonObject
import org.jetbrains.kotlin.spec.TestsJsonMapBuilder
import org.jetbrains.kotlin.spec.utils.GeneralConfiguration.MODULE_PATH
import org.jetbrains.kotlin.spec.utils.GeneralConfiguration.TESTDATA_PATH
import org.jetbrains.kotlin.spec.utils.TestsJsonMapBuilder
import org.jetbrains.kotlin.spec.validators.LinkedSpecTestValidator
import org.jetbrains.kotlin.spec.validators.SpecTestValidationException
import java.io.File
private const val TEST_DATA_DIR = "./testData"
private const val OUT_DIR = "./out"
private const val OUT_DIR = "out"
private const val OUT_FILENAME = "testsMap.json"
fun main(args: Array<String>) {
val testsMap = JsonObject()
File(TEST_DATA_DIR).walkTopDown().forEach {
File(TESTDATA_PATH).walkTopDown().forEach {
val specTestValidator = LinkedSpecTestValidator(it)
try {
@@ -30,6 +31,8 @@ fun main(args: Array<String>) {
TestsJsonMapBuilder.buildJsonElement(specTestValidator.testInfo, testsMap)
}
File(OUT_DIR).mkdir()
File("$OUT_DIR/$OUT_FILENAME").writeText(testsMap.toString())
val outDir = "$MODULE_PATH/$OUT_DIR"
File(outDir).mkdir()
File("$outDir/$OUT_FILENAME").writeText(testsMap.toString())
}
@@ -0,0 +1,12 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.spec.utils
object GeneralConfiguration {
const val MODULE_PATH = "compiler/tests-spec"
const val TESTDATA_PATH = "$MODULE_PATH/testData"
const val TEST_PATH = "$MODULE_PATH/tests"
}