diff --git a/compiler/tests-common/tests/org/jetbrains/kotlin/checkers/BaseDiagnosticsTest.kt b/compiler/tests-common/tests/org/jetbrains/kotlin/checkers/BaseDiagnosticsTest.kt index 2a441eebe35..2dc37705cf5 100644 --- a/compiler/tests-common/tests/org/jetbrains/kotlin/checkers/BaseDiagnosticsTest.kt +++ b/compiler/tests-common/tests/org/jetbrains/kotlin/checkers/BaseDiagnosticsTest.kt @@ -85,7 +85,7 @@ abstract class BaseDiagnosticsTest : KotlinMultiFileTestWithJava) - protected fun getKtFiles(testFiles: List, includeExtras: Boolean): List { + protected open fun getKtFiles(testFiles: List, includeExtras: Boolean): List { var declareFlexibleType = false var declareCheckType = false val ktFiles = arrayListOf() @@ -123,9 +123,10 @@ abstract class BaseDiagnosticsTest : KotlinMultiFileTestWithJava + val directives: Map ) { private val diagnosedRanges: List = ArrayList() + val actualDiagnostics: MutableList = ArrayList() val expectedText: String private val clearText: String private val createKtFile: Lazy @@ -240,6 +241,8 @@ abstract class BaseDiagnosticsTest : KotlinMultiFileTestWithJava() val inferenceCompatibilityOfTest = asInferenceCompatibility(withNewInference) val invertedInferenceCompatibilityOfTest = asInferenceCompatibility(!withNewInference) diff --git a/compiler/tests-spec/build.gradle.kts b/compiler/tests-spec/build.gradle.kts index bc5882eb511..d73103b40dc 100644 --- a/compiler/tests-spec/build.gradle.kts +++ b/compiler/tests-spec/build.gradle.kts @@ -15,3 +15,5 @@ sourceSets { projectTest { workingDir = rootDir } + +val generateTests by generator("org.jetbrains.kotlin.generators.tests.GenerateCompilerSpecTestsKt") diff --git a/compiler/tests-spec/testData/helpers/diagnostics/basicTypes.kt b/compiler/tests-spec/testData/helpers/diagnostics/basicTypes.kt new file mode 100644 index 00000000000..1c7488c4ca3 --- /dev/null +++ b/compiler/tests-spec/testData/helpers/diagnostics/basicTypes.kt @@ -0,0 +1,29 @@ +fun getInt(arg: Any = Any()) = arg.hashCode() +fun getShort(arg: Any = Any()) = arg.hashCode().toShort() +fun getLong(arg: Any = Any()) = arg.hashCode().toLong() +fun getFloat(arg: Any = Any()) = arg.hashCode().toFloat() +fun getDouble(arg: Any = Any()) = arg.hashCode().toDouble() +fun getByte(arg: Any = Any()) = arg.hashCode().toByte() +fun getChar(arg: Any = Any()) = arg.hashCode().toChar() +fun getString(arg: Any = Any()) = arg.hashCode().toString() +fun getBoolean(arg: Any = Any()) = arg.hashCode() % 2 == 0 +fun getNothing(): Nothing = throw Exception() +fun getUnit() = {} +fun getAny() = Any() +fun getList() = mutableListOf() + +class _BasicTypesProvider { + fun getInt(arg: Any = Any()) = arg.hashCode() + fun getShort(arg: Any = Any()) = arg.hashCode().toShort() + fun getLong(arg: Any = Any()) = arg.hashCode().toLong() + fun getFloat(arg: Any = Any()) = arg.hashCode().toFloat() + fun getDouble(arg: Any = Any()) = arg.hashCode().toDouble() + fun getByte(arg: Any = Any()) = arg.hashCode().toByte() + fun getChar(arg: Any = Any()) = arg.hashCode().toChar() + fun getString(arg: Any = Any()) = arg.hashCode().toString() + fun getBoolean(arg: Any = Any()) = arg.hashCode() % 2 == 0 + fun getNothing(): Nothing = throw Exception() + fun getUnit() = {} + fun getAny() = Any() + fun getList() = mutableListOf() +} \ No newline at end of file diff --git a/compiler/tests-spec/testData/helpers/diagnostics/classes.kt b/compiler/tests-spec/testData/helpers/diagnostics/classes.kt new file mode 100644 index 00000000000..be53fd9b2a0 --- /dev/null +++ b/compiler/tests-spec/testData/helpers/diagnostics/classes.kt @@ -0,0 +1,35 @@ +class _Class { + val prop_1 = 1 + val prop_2 = 2 + val prop_3 = 3 + + fun fun_1(): (Int) -> (Int) -> Int = {number: Int -> { number * 5 }} + fun fun_2(value: Int): Int = value * 2 + fun fun_3(value1: Int): (Int) -> Int = fun(value2: Int): Int = value1 * value2 * 2 + + operator fun contains(a: Int): Boolean = a > 30 + operator fun contains(a: Long): Boolean = a > 30L + operator fun contains(a: Char): Boolean = a > 30.toChar() + + fun getIntArray(value: Int): IntArray = intArrayOf(1, 2, 3, value, 91923, 14, 123124) + fun getLongArray(value: Long): LongArray = longArrayOf(1L, 2L, 3L, value, 9192323244L, 14L, 123124L) + fun getCharArray(value: Char): CharArray = charArrayOf(1.toChar(), 2.toChar(), 3.toChar(), value) + + class _NestedClass { + val prop_4 = 4 + val prop_5 = 5 + } +} + +class _EmptyClass {} + +class _ClassWithCompanionObject { + companion object {} +} + +open class _ClassLevel1 {} +open class _ClassLevel2: _ClassLevel1() {} +open class _ClassLevel3: _ClassLevel2() {} +open class _ClassLevel4: _ClassLevel3() {} +open class _ClassLevel5: _ClassLevel4() {} +class _ClassLevel6: _ClassLevel5() {} diff --git a/compiler/tests-spec/testData/helpers/diagnostics/enumClasses.kt b/compiler/tests-spec/testData/helpers/diagnostics/enumClasses.kt new file mode 100644 index 00000000000..e404053ec2f --- /dev/null +++ b/compiler/tests-spec/testData/helpers/diagnostics/enumClasses.kt @@ -0,0 +1,9 @@ +enum class _EnumClass { + NORTH, SOUTH, WEST, EAST +} + +enum class _EnumClassSingle { + EVERYTHING +} + +enum class _EnumClassEmpty diff --git a/compiler/tests-spec/testData/helpers/diagnostics/funs.kt b/compiler/tests-spec/testData/helpers/diagnostics/funs.kt new file mode 100644 index 00000000000..6685358da7a --- /dev/null +++ b/compiler/tests-spec/testData/helpers/diagnostics/funs.kt @@ -0,0 +1,3 @@ +fun _funWithoutArgs(): Int { + return Any().hashCode().toInt() +} diff --git a/compiler/tests-spec/testData/helpers/diagnostics/objects.kt b/compiler/tests-spec/testData/helpers/diagnostics/objects.kt new file mode 100644 index 00000000000..ea795bd73e4 --- /dev/null +++ b/compiler/tests-spec/testData/helpers/diagnostics/objects.kt @@ -0,0 +1 @@ +object _EmptyObject {} \ No newline at end of file diff --git a/compiler/tests-spec/testData/helpers/diagnostics/sealedClasses.kt b/compiler/tests-spec/testData/helpers/diagnostics/sealedClasses.kt new file mode 100644 index 00000000000..9309c664286 --- /dev/null +++ b/compiler/tests-spec/testData/helpers/diagnostics/sealedClasses.kt @@ -0,0 +1,36 @@ +sealed class _SealedClass +data class _SealedChild1(val number: Int) : _SealedClass() +data class _SealedChild2(val e1: Int, val e2: Int) : _SealedClass() +data class _SealedChild3(val m1: Int, val m2: Int) : _SealedClass() + +sealed class _SealedClassWithObjects +object _SealedWithObjectsChild1 : _SealedClassWithObjects() +object _SealedWithObjectsChild2 : _SealedClassWithObjects() +object _SealedWithObjectsChild3 : _SealedClassWithObjects() + +sealed class _SealedClassSingle +data class _SealedSingleChild1(val number: Int) : _SealedClassSingle() + +sealed class _SealedClassSingleWithObject +object _SealedSingleWithObjectChild1: Expr3() {} + +sealed class _SealedClassEmpty + +sealed class _SealedClassWithMethods +class _SealedWithMethodsChild1() : _SealedClassWithMethods() { + fun m1() = this.hashCode().toString() +} +class _SealedWithMethodsChild2() : _SealedClassWithMethods() { + fun m2() = this.hashCode().toString() +} +class _SealedWithMethodsChild3() : _SealedClassWithMethods() { + fun m3() = this.hashCode().toString() +} + +sealed class _SealedClassMixed +data class _SealedMixedChild1(val number: Int) : _SealedClassMixed() +data class _SealedMixedChild2(val e1: Int, val e2: Int) : _SealedClassMixed() +data class _SealedMixedChild3(val m1: Int, val m2: Int) : _SealedClassMixed() +object _SealedMixedChildObject1 : _SealedClassMixed() +object _SealedMixedChildObject2 : _SealedClassMixed() +object _SealedMixedChildObject3 : _SealedClassMixed() \ No newline at end of file diff --git a/compiler/tests-spec/testData/helpers/diagnostics/typeAliases.kt b/compiler/tests-spec/testData/helpers/diagnostics/typeAliases.kt new file mode 100644 index 00000000000..4fca9796c09 --- /dev/null +++ b/compiler/tests-spec/testData/helpers/diagnostics/typeAliases.kt @@ -0,0 +1,4 @@ +typealias _TypeAliasAny = Any +typealias _TypeAliasUnit = Unit +typealias _TypeAliasNothing = Nothing +typealias _TypeAliasInt = Int diff --git a/compiler/tests-spec/tests/org/jetbrains/kotlin/checkers/AbstractDiagnosticsTestSpec.kt b/compiler/tests-spec/tests/org/jetbrains/kotlin/checkers/AbstractDiagnosticsTestSpec.kt new file mode 100644 index 00000000000..5eb0ed09c5d --- /dev/null +++ b/compiler/tests-spec/tests/org/jetbrains/kotlin/checkers/AbstractDiagnosticsTestSpec.kt @@ -0,0 +1,91 @@ +/* + * 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.checkers + +import org.jetbrains.kotlin.config.LanguageVersionSettings +import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.spec.DiagnosticSpecTestValidator +import org.jetbrains.kotlin.spec.SpecTestValidationException +import org.jetbrains.kotlin.test.* +import org.junit.Assert +import java.io.File + +abstract class AbstractDiagnosticsTestSpec : AbstractDiagnosticsTest() { + companion object { + // map of pairs: source helper filename - target helper filename + private val directives = mapOf( + "WITH_BASIC_TYPES" to Pair("basicTypes.kt", "BASIC_TYPES.kt"), + "WITH_CLASSES" to Pair("classes.kt", "CLASSES.kt"), + "WITH_ENUM_CLASSES" to Pair("enumClasses.kt", "ENUM_CLASSES.kt"), + "WITH_SEALED_CLASSES" to Pair("sealedClasses.kt", "SEALED_CLASSES.kt"), + "WITH_FUNS" to Pair("funs.kt", "FUNS.kt"), + "WITH_OBJECTS" to Pair("objects.kt", "OBJECTS.kt"), + "WITH_TYPEALIASES" to Pair("typeAliases.kt", "TYPE_ALIASES.kt") + ) + + private const val MODULE_PATH = "./compiler/tests-spec" + private const val HELPERS_PATH = "$MODULE_PATH/testData/helpers/diagnostics" + } + + private lateinit var testValidator: DiagnosticSpecTestValidator + + private fun checkDirective(directive: String, testFiles: List) = + testFiles.any { it.directives.contains(directive) } + + override fun getConfigurationKind() = ConfigurationKind.ALL + + override fun skipDescriptorsValidation(): Boolean = true + + override fun getKtFiles(testFiles: List, includeExtras: Boolean): List { + val ktFiles = super.getKtFiles(testFiles, includeExtras) as ArrayList + + if (includeExtras) { + for ((name, filenames) in directives) { + if (checkDirective(name, testFiles)) { + val (sourceFilename, targetFilename) = filenames + val declarations = File("$HELPERS_PATH/$sourceFilename").readText() + + ktFiles.add(KotlinTestUtils.createFile(targetFilename, declarations, project)) + } + } + } + + return ktFiles + } + + override fun analyzeAndCheck(testDataFile: File, files: List) { + testValidator = DiagnosticSpecTestValidator(testDataFile) + + try { + testValidator.parseTestInfo() + } catch (e: SpecTestValidationException) { + Assert.fail(e.description) + } + + testValidator.printTestInfo() + + super.analyzeAndCheck(testDataFile, files) + } + + override fun performAdditionalChecksAfterDiagnostics( + testDataFile: File, + testFiles: List, + moduleFiles: Map>, + moduleDescriptors: Map, + moduleBindings: Map, + languageVersionSettingsByModule: Map + ) { + try { + testValidator.validateTestType(testFiles) + } catch (e: SpecTestValidationException) { + Assert.fail(e.description) + } finally { + testValidator.printDiagnosticStatistic() + } + } +} diff --git a/compiler/tests-spec/tests/org/jetbrains/kotlin/generators/tests/GenerateCompilerSpecTests.kt b/compiler/tests-spec/tests/org/jetbrains/kotlin/generators/tests/GenerateCompilerSpecTests.kt new file mode 100644 index 00000000000..8d6578495ff --- /dev/null +++ b/compiler/tests-spec/tests/org/jetbrains/kotlin/generators/tests/GenerateCompilerSpecTests.kt @@ -0,0 +1,17 @@ +/* + * 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.generators.tests + +import org.jetbrains.kotlin.generators.tests.generator.testGroup +import org.jetbrains.kotlin.checkers.AbstractDiagnosticsTestSpec + +fun main(args: Array) { + testGroup("compiler/tests-spec/tests", "compiler/tests-spec/testData") { + testClass { + model("diagnostics") + } + } +} \ No newline at end of file diff --git a/compiler/tests-spec/tests/org/jetbrains/kotlin/spec/DiagnosticSpecTestValidator.kt b/compiler/tests-spec/tests/org/jetbrains/kotlin/spec/DiagnosticSpecTestValidator.kt new file mode 100644 index 00000000000..dc3553a5a16 --- /dev/null +++ b/compiler/tests-spec/tests/org/jetbrains/kotlin/spec/DiagnosticSpecTestValidator.kt @@ -0,0 +1,68 @@ +/* + * 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 + +import org.jetbrains.kotlin.checkers.BaseDiagnosticsTest +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.diagnostics.Severity +import java.io.File + +class DiagnosticSpecTestValidator(testDataFile: File) : SpecTestValidator(testDataFile, TestArea.DIAGNOSTICS) { + private lateinit var diagnostics: MutableList + private lateinit var diagnosticStats: MutableMap + private lateinit var diagnosticSeverityStats: MutableMap + + private fun collectDiagnosticStatistic() { + diagnosticSeverityStats = mutableMapOf() + + diagnostics.forEach { + val severity = it.factory.severity + + if (diagnosticSeverityStats.contains(severity)) { + diagnosticSeverityStats[severity] = diagnosticSeverityStats[severity]!! + 1 + } else { + diagnosticSeverityStats[severity] = 1 + } + } + } + + private fun computeTestType(): TestType { + return if (Severity.ERROR in diagnosticSeverityStats) TestType.NEGATIVE else TestType.POSITIVE + } + + private fun collectDiagnostics(files: List) { + diagnostics = mutableListOf() + diagnosticStats = mutableMapOf() + + files.forEach { + it.actualDiagnostics.forEach { + val diagnosticName = it.diagnostic.factory.name + + if (diagnosticStats.contains(diagnosticName)) { + diagnosticStats[diagnosticName] = diagnosticStats[diagnosticName]!! + 1 + } else { + diagnosticStats[diagnosticName] = 1 + } + + diagnostics.add(it.diagnostic) + } + } + + collectDiagnosticStatistic() + } + + fun validateTestType(files: List) { + if (!this::diagnostics.isInitialized) this.collectDiagnostics(files) + + validateTestType(computeTestType()) + } + + fun printDiagnosticStatistic() { + val diagnostics = if (diagnosticStats.isNotEmpty()) "$diagnosticSeverityStats | $diagnosticStats" else "does not contain" + + println("DIAGNOSTICS: $diagnostics") + } +} \ No newline at end of file diff --git a/compiler/tests-spec/tests/org/jetbrains/kotlin/spec/SpecTestValidator.kt b/compiler/tests-spec/tests/org/jetbrains/kotlin/spec/SpecTestValidator.kt new file mode 100644 index 00000000000..ab60ca069fe --- /dev/null +++ b/compiler/tests-spec/tests/org/jetbrains/kotlin/spec/SpecTestValidator.kt @@ -0,0 +1,323 @@ +/* + * 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 + +import java.io.File +import java.util.regex.Matcher +import java.util.regex.Pattern + +enum class TestType(val type: String) { + POSITIVE("pos"), + NEGATIVE("neg"); + + companion object { + private val map = TestType.values().associateBy(TestType::type) + fun fromValue(type: String) = map[type] + } +} + +enum class TestArea { + PSI, + DIAGNOSTICS, + CODEGEN +} + +interface SpecTestInfoElementType { + val valuePattern: Pattern? + val required: Boolean +} + +enum class SpecTestFileInfoElementType( + override val valuePattern: Pattern? = null, + override val required: Boolean = false +) : SpecTestInfoElementType { + SECTION( + valuePattern = Pattern.compile("""(?(?:${SpecTestValidator.INTEGER_REGEX})(?:\.${SpecTestValidator.INTEGER_REGEX})*)(?.*?)"""), + required = true + ), + PARAGRAPH(required = true), + SENTENCE( + valuePattern = Pattern.compile("""\[(?${SpecTestValidator.INTEGER_REGEX})\](?.*?)"""), + required = true + ), + NUMBER(required = true), + DESCRIPTION(required = true), + ISSUES(valuePattern = Pattern.compile("""(KT-[1-9]\d*)(,\s*KT-[1-9]\d*)*""")), + UNEXPECTED_BEHAVIOUR, + DISCUSSION, + NOTE +} + +enum class SpecTestCaseInfoElementType( + override val valuePattern: Pattern? = null, + override val required: Boolean = false +) : SpecTestInfoElementType { + CASE_DESCRIPTION(required = true), + ISSUES(valuePattern = SpecTestFileInfoElementType.ISSUES.valuePattern), + UNEXPECTED_BEHAVIOUR, + DISCUSSION, + NOTE +} + +data class SpecTestInfoElementContent( + val content: String, + val additionalMatcher: Matcher? = null +) + +data class SpecTestCase( + val number: Int, + val description: String, + val unexpectedBehavior: Boolean, + val issues: List? +) + +class SpecTest( + val testArea: TestArea, + val testType: TestType, + val sectionNumber: String, + val sectionName: String, + val paragraphNumber: Int, + val sentenceNumber: Int, + val sentence: String? = null, + val testNumber: Int, + val description: String? = null, + val cases: List? = null, + val unexpectedBehavior: Boolean? = null, + val issues: Set? = null +) { + fun checkConsistency(other: SpecTest): Boolean { + return this.testArea == other.testArea + && this.testType == other.testType + && this.sectionNumber == other.sectionNumber + && this.testNumber == other.testNumber + && this.paragraphNumber == other.paragraphNumber + && this.sentenceNumber == other.sentenceNumber + } +} + +enum class SpecTestValidationFailedReason(val description: String) { + FILENAME_NOT_VALID( + "Incorrect test filename or folder name.${SpecTestValidator.lineSeparator}" + + "It must match the following path pattern: " + + "testsData//s_/p-//_.kt " + + "(example: testsData/diagnostic/s-16.30_when-expression/p-3/pos/1.3.kt)" + ), + TESTINFO_NOT_VALID("Test info is incorrect."), + FILEPATH_AND_TESTINFO_IN_FILE_NOT_CONSISTENCY("Test info from filepath and file content is not consistency"), + TEST_IS_NOT_POSITIVE("Test is not positive because it contains error elements (PsiErrorElement or diagnostic with error severity)."), + TEST_IS_NOT_NEGATIVE("Test is not negative because it not contains error type elements (PsiErrorElement or diagnostic with error severity)."), + UNKNOWN("Unknown validation error.") +} + +class SpecTestValidationException(reason: SpecTestValidationFailedReason, details: String = "") : Exception() { + val description = "${reason.description} $details" +} + +typealias SpecTestInfoElements = Map + +abstract class SpecTestValidator(private val testDataFile: File, private val testArea: TestArea) { + private lateinit var testInfoByFilename: SpecTest + private lateinit var testInfoByContent: SpecTest + private val testInfo by lazy { testInfoByContent } + + companion object { + const val INTEGER_REGEX = """[1-9]\d*""" + private const val SINGLELINE_COMMENT_REGEX = """\/\/\s*%s""" + private const val MULTILINE_COMMENT_REGEX = """\/\*\s*%s\s+\*\/""" + + val lineSeparator: String = System.lineSeparator() + private val testCaseInfoRegex = """(?CASE DESCRIPTION:[\s\S]*?$lineSeparator)""" + private val testAreaRegex = """(?${TestArea.values().joinToString("|")})""" + private val testTypeRegex = """(?${TestType.values().joinToString("|")})""" + private val testInfoElementPattern: Pattern = Pattern.compile("""\s*(?[A-Z ]*?):\s*(?.*?)$lineSeparator""") + private val pathSeparator = Pattern.quote(File.separator) + + val testPathPattern: Pattern = + Pattern.compile("""^.*?$pathSeparator(?diagnostics|psi|codegen)${pathSeparator}s-(?(?:$INTEGER_REGEX)(?:\.$INTEGER_REGEX)*)_(?[\w-]+)${pathSeparator}p-(?$INTEGER_REGEX)$pathSeparator(?pos|neg)$pathSeparator(?$INTEGER_REGEX)\.(?$INTEGER_REGEX)\.kt$""") + val testInfoPattern: Pattern = + Pattern.compile(MULTILINE_COMMENT_REGEX.format("""KOTLIN $testAreaRegex SPEC TEST \($testTypeRegex\)$lineSeparator(?[\s\S]*?$lineSeparator)""")) + val testCaseInfoSinglelinePattern: Pattern = Pattern.compile(SINGLELINE_COMMENT_REGEX.format(testCaseInfoRegex)) + val testCaseInfoMultilinePattern: Pattern = Pattern.compile(MULTILINE_COMMENT_REGEX.format(testCaseInfoRegex)) + + private fun getTestInfo( + testInfoMatcher: Matcher, + testInfoElements: SpecTestInfoElements, + testCases: List, + unexpectedBehavior: Boolean = false, + issues: Set? = null + ): SpecTest { + val sectionMatcher = testInfoElements[SpecTestFileInfoElementType.SECTION]!!.additionalMatcher!! + val sentenceMatcher = testInfoElements[SpecTestFileInfoElementType.SENTENCE]!!.additionalMatcher!! + + return SpecTest( + TestArea.valueOf(testInfoMatcher.group("testArea").toUpperCase()), + TestType.valueOf(testInfoMatcher.group("testType")), + sectionMatcher.group("number"), + sectionMatcher.group("name"), + testInfoElements[SpecTestFileInfoElementType.PARAGRAPH]!!.content.toInt(), + sentenceMatcher.group("number").toInt(), + sentenceMatcher.group("text"), + testInfoElements[SpecTestFileInfoElementType.NUMBER]!!.content.toInt(), + testInfoElements[SpecTestFileInfoElementType.DESCRIPTION]!!.content, + testCases, + unexpectedBehavior, + issues + ) + } + + private fun getTestInfo(testInfoMatcher: Matcher): SpecTest { + return SpecTest( + TestArea.valueOf(testInfoMatcher.group("testArea").toUpperCase()), + TestType.fromValue(testInfoMatcher.group("testType"))!!, + testInfoMatcher.group("sectionNumber"), + testInfoMatcher.group("sectionName"), + testInfoMatcher.group("paragraphNumber").toInt(), + testInfoMatcher.group("sentenceNumber").toInt(), + testNumber = testInfoMatcher.group("testNumber").toInt() + ) + } + + private fun parseIssues(issues: SpecTestInfoElementContent?) = issues?.content?.split(",") + + private fun getSingleTestCase(testInfoElements: SpecTestInfoElements) = SpecTestCase( + 1, + description = testInfoElements[SpecTestFileInfoElementType.DESCRIPTION]!!.content, + unexpectedBehavior = testInfoElements.contains(SpecTestFileInfoElementType.UNEXPECTED_BEHAVIOUR), + issues = parseIssues(testInfoElements[SpecTestFileInfoElementType.ISSUES]) + ) + + private fun getTestCasesInfo(testCaseInfoMatcher: Matcher, infoElements: SpecTestInfoElements): List { + val testCases = mutableListOf() + var testCasesCounter = 1 + + while (testCaseInfoMatcher.find()) { + val caseInfoElements = getTestInfoElements(testCaseInfoMatcher.group("infoElements")) + + testCases.add( + SpecTestCase( + testCasesCounter++, + caseInfoElements[SpecTestCaseInfoElementType.CASE_DESCRIPTION]!!.content, + caseInfoElements.contains(SpecTestCaseInfoElementType.UNEXPECTED_BEHAVIOUR), + parseIssues(caseInfoElements[SpecTestCaseInfoElementType.ISSUES]) + ) + ) + } + + if (testCases.isEmpty()) testCases.add(getSingleTestCase(infoElements)) + + return testCases + } + + private inline fun > getTestInfoElements(testInfoElements: String): SpecTestInfoElements { + val testInfoElementsMap = mutableMapOf() + val testInfoElementMatcher = testInfoElementPattern.matcher(testInfoElements) + + while (testInfoElementMatcher.find()) { + val testInfoOriginalElementName = testInfoElementMatcher.group("name") + val testInfoElementName: SpecTestInfoElementType + try { + testInfoElementName = enumValueOf(testInfoOriginalElementName.replace(" ", "_")) as SpecTestInfoElementType + } catch (e: IllegalArgumentException) { + throw SpecTestValidationException( + SpecTestValidationFailedReason.TESTINFO_NOT_VALID, + "Unknown '$testInfoOriginalElementName' test info element name." + ) + } + val testInfoElementValue = testInfoElementMatcher.group("value") + val testInfoElementValueMatcher = testInfoElementName.valuePattern?.matcher(testInfoElementValue) + + if (testInfoElementValueMatcher != null && !testInfoElementValueMatcher.find()) + throw SpecTestValidationException( + SpecTestValidationFailedReason.TESTINFO_NOT_VALID, + "'$testInfoElementValue' in '$testInfoElementName' is not parsed." + ) + + testInfoElementsMap[testInfoElementName] = SpecTestInfoElementContent(testInfoElementValue, testInfoElementValueMatcher) + } + + enumValues().forEach { + it as SpecTestInfoElementType + if (it.required && !testInfoElementsMap.contains(it)) { + throw SpecTestValidationException( + SpecTestValidationFailedReason.TESTINFO_NOT_VALID, + "$it in case or test info is required." + ) + } + } + + return testInfoElementsMap + } + + private fun getIssues(testCases: List, testIssues: List?): Set { + val issues = mutableSetOf() + + testCases.forEach { + if (it.issues != null) issues.addAll(it.issues) + } + + if (testIssues != null) issues.addAll(testIssues) + + return issues + } + } + + fun parseTestInfo() { + val testInfoByFilenameMatcher = testPathPattern.matcher(testDataFile.path) + + if (!testInfoByFilenameMatcher.find()) + throw SpecTestValidationException(SpecTestValidationFailedReason.FILENAME_NOT_VALID) + + val fileContent = testDataFile.readText() + val testInfoByContentMatcher = testInfoPattern.matcher(fileContent) + + if (!testInfoByContentMatcher.find()) + throw SpecTestValidationException(SpecTestValidationFailedReason.TESTINFO_NOT_VALID) + + val testInfoElements = getTestInfoElements(testInfoByContentMatcher.group("infoElements")) + val testCases = getTestCasesInfo(testCaseInfoSinglelinePattern.matcher(fileContent), testInfoElements) + + getTestCasesInfo(testCaseInfoMultilinePattern.matcher(fileContent), testInfoElements) + + testInfoByFilename = getTestInfo(testInfoByFilenameMatcher) + testInfoByContent = getTestInfo( + testInfoByContentMatcher, + testInfoElements, + testCases = testCases, + unexpectedBehavior = testInfoElements.contains(SpecTestFileInfoElementType.UNEXPECTED_BEHAVIOUR) || testCases.any { it.unexpectedBehavior }, + issues = getIssues(testCases, parseIssues(testInfoElements[SpecTestFileInfoElementType.ISSUES])) + ) + + if (!testInfoByFilename.checkConsistency(testInfoByContent)) + throw SpecTestValidationException(SpecTestValidationFailedReason.FILEPATH_AND_TESTINFO_IN_FILE_NOT_CONSISTENCY) + } + + protected fun validateTestType(computedTestType: TestType) { + if (computedTestType != testInfo.testType) { + val isNotNegative = computedTestType == TestType.POSITIVE && testInfo.testType == TestType.NEGATIVE + val isNotPositive = computedTestType == TestType.NEGATIVE && testInfo.testType == TestType.POSITIVE + val reason = when { + isNotNegative -> SpecTestValidationFailedReason.TEST_IS_NOT_NEGATIVE + isNotPositive -> SpecTestValidationFailedReason.TEST_IS_NOT_POSITIVE + else -> SpecTestValidationFailedReason.UNKNOWN + } + throw SpecTestValidationException(reason) + } + } + + fun printTestInfo() { + println("--------------------------------------------------") + if (testInfoByContent.unexpectedBehavior!!) + println("(!!!) HAS UNEXPECTED BEHAVIOUR (!!!)") + println("$testArea ${testInfoByFilename.testType} SPEC TEST") + println("SECTION: ${testInfoByFilename.sectionNumber} ${testInfoByContent.sectionName} (paragraph: ${testInfoByFilename.paragraphNumber})") + println("SENTENCE ${testInfoByContent.sentenceNumber}: ${testInfoByContent.sentence}") + println("TEST NUMBER: ${testInfoByContent.testNumber}") + println("NUMBER OF TEST CASES: ${testInfoByContent.cases!!.size}") + println("DESCRIPTION: ${testInfoByContent.description}") + if (testInfoByContent.issues!!.isNotEmpty()) { + println("LINKED ISSUES: ${testInfoByContent.issues!!.joinToString(", ")}") + } + } +} \ No newline at end of file