diff --git a/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/test/framework/AbstractKtIdeaTest.kt b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/test/framework/AbstractKtIdeaTest.kt new file mode 100644 index 00000000000..6b2d0c2325e --- /dev/null +++ b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/test/framework/AbstractKtIdeaTest.kt @@ -0,0 +1,88 @@ +/* + * 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.idea.test.framework + +import com.intellij.openapi.util.io.FileUtil +import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase +import java.io.File + +abstract class AbstractKtIdeaTest : KotlinLightCodeInsightFixtureTestCase() { + protected open val allowedDirectives: List> = emptyList() + + protected fun doTest(path: String) { + val testDataFile = File(path) + val text = FileUtil.loadFile(testDataFile) + val testFileStructure = createTestFileStructure(text, testDataFile) + doTestByFileStructure(testFileStructure) + } + + private fun createTestFileStructure(text: String, testDataFile: File): TestFileStructure { + val files = FileSplitter.splitIntoFiles(text, testDataFile.name) + val mainFile = createTestFile(files.first(), isMainFile = true) + val testFiles = files.drop(1).map { file -> + createTestFile(file, isMainFile = false) + } + val directives = parseDirectives(text) + return TestFileStructure( + filePath = testDataFile.toPath(), + caretPosition = getCaretPosition(text), + directives = directives, + mainFile = mainFile as TestFile.KtTestFile, + otherFiles = testFiles + ) + } + + private fun getCaretPosition(text: String) = text.indexOfOrNull(KtTest.CARET_SYMBOL) + + private fun parseDirectives(text: String): TestFileDirectives { + val directives = text.lineSequence().mapNotNull(::extractDirectiveIfAny) + return TestFileDirectives(directives.toMap()) + } + + private fun extractDirectiveIfAny(line: String): Pair? { + val directive = allowedDirectives.firstOrNull { directive -> line.startsWith(directive.name) } ?: return null + val value = line.substringAfter(directive.name).trim() + val parsedValue = directive.parse(value) + ?: error("Invalid ${directive.name} value `$value`") + return directive.name to parsedValue + } + + private fun createTestFile(file: FileSplitter.FileNameWithText, isMainFile: Boolean): TestFile { + val psiFile = if (isMainFile) { + myFixture.configureByText(file.name, file.text) + } else { + myFixture.addFileToProject(file.name, file.text) + } + return TestFile.createByPsiFile(psiFile) + } + + abstract fun doTestByFileStructure(fileStructure: TestFileStructure) +} + +private object FileSplitter { + data class FileNameWithText(val name: String, val text: String) + + fun splitIntoFiles(text: String, defaultName: String): List { + val result = mutableListOf() + val stopAt = text.indexOfOrNull(KtTest.RESULT_DIRECTIVE) ?: text.length + var index = text.indexOfOrNull(KtTest.FILE_DIRECTIVE) ?: return listOf(FileNameWithText(defaultName, text)) + while (index < stopAt) { + val eolIndex = text.indexOfOrNull("\n", index) ?: text.length + val fileName = text.substring(index + KtTest.FILE_DIRECTIVE.length, eolIndex).trim() + val nextFileIndex = text.indexOfOrNull(KtTest.FILE_DIRECTIVE, index + 1) ?: stopAt + val fileText = text.substring(eolIndex, nextFileIndex).trim() + index = nextFileIndex + result += FileNameWithText(fileName, fileText) + } + return result + } +} + +private fun String.indexOfOrNull(substring: String) = + indexOf(substring).takeIf { it >= 0 } + +private fun String.indexOfOrNull(substring: String, startingIndex: Int) = + indexOf(substring, startingIndex).takeIf { it >= 0 } \ No newline at end of file diff --git a/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/test/framework/KtTest.kt b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/test/framework/KtTest.kt new file mode 100644 index 00000000000..353f4866d45 --- /dev/null +++ b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/test/framework/KtTest.kt @@ -0,0 +1,12 @@ +/* + * 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.idea.test.framework + +object KtTest { + const val CARET_SYMBOL = "" + const val FILE_DIRECTIVE = "// FILE:" + const val RESULT_DIRECTIVE = "// RESULT" +} \ No newline at end of file diff --git a/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/test/framework/TestFileStructure.kt b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/test/framework/TestFileStructure.kt new file mode 100644 index 00000000000..336f9cc5287 --- /dev/null +++ b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/test/framework/TestFileStructure.kt @@ -0,0 +1,57 @@ +/* + * 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.idea.test.framework + +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiJavaFile +import org.jetbrains.kotlin.psi.KtFile +import java.nio.file.Path + +class TestFileStructure( + val filePath: Path, + val caretPosition: Int?, + val directives: TestFileDirectives, + val mainFile: TestFile.KtTestFile, + val otherFiles: List, +) { + val mainKtFile: KtFile + get() = mainFile.psiFile + + val allFiles: List = listOf(mainFile) + otherFiles +} + +data class TestStructureExpectedDataBlock(val name: String, val values: List) + +class TestFileDirectives( + private val directives: Map +) { + fun > getDirectiveValueIfPresent(directive: DIRECTIVE): VALUE? { + val value = directives[directive.name] ?: return null + @Suppress("UNCHECKED_CAST") + return value as VALUE + } +} + +abstract class TestFileDirective { + abstract val name: String + abstract fun parse(value: String): VALUE? +} + +sealed class TestFile { + abstract val psiFile: PsiFile + + data class KtTestFile(override val psiFile: KtFile) : TestFile() + data class JavaTestFile(override val psiFile: PsiJavaFile) : TestFile() + + companion object { + fun createByPsiFile(psiFile: PsiFile) = when (psiFile) { + is KtFile -> KtTestFile(psiFile) + is PsiJavaFile -> JavaTestFile(psiFile) + else -> error("Unknown file type ${psiFile::class}") + } + } +} + diff --git a/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/test/framework/TestStructureRenderer.kt b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/test/framework/TestStructureRenderer.kt new file mode 100644 index 00000000000..97b7f6680ec --- /dev/null +++ b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/test/framework/TestStructureRenderer.kt @@ -0,0 +1,54 @@ +/* + * 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.idea.test.framework + +object TestStructureRenderer { + fun render(testStructure: TestFileStructure, expectedData: List): String = buildString { + renderFiles(testStructure) + renderExpectedData(expectedData) + renderCaretSymbol(testStructure) + } + + private fun StringBuilder.renderCaretSymbol(testStructure: TestFileStructure) { + testStructure.caretPosition?.let { position -> + insert(position, KtTest.CARET_SYMBOL) + } + } + + private fun StringBuilder.renderFiles(testStructure: TestFileStructure) { + if (testStructure.otherFiles.isEmpty()) { + appendLine(testStructure.mainFile.psiFile.text) + } else { + testStructure.allFiles.forEach { file -> + renderFile(file) + } + } + } + + private fun StringBuilder.renderExpectedData(expectedData: List) { + if (expectedData.isNotEmpty()) { + appendLine(KtTest.RESULT_DIRECTIVE) + appendLine() + expectedData.forEach { block -> + renderExpectedDataBlock(block) + appendLine() + } + } + } + + private fun StringBuilder.renderExpectedDataBlock(block: TestStructureExpectedDataBlock) { + appendLine("// ${block.name}") + block.values.forEach { value -> + appendLine("// $value") + } + } + + private fun StringBuilder.renderFile(file: TestFile) { + appendLine("${KtTest.FILE_DIRECTIVE} ${file.psiFile.name}") + appendLine(file.psiFile.text) + appendLine() + } +} \ No newline at end of file