From f011c21c94345883c370b10907069747dda55656 Mon Sep 17 00:00:00 2001 From: Dmitry Savvinov Date: Fri, 26 Apr 2019 15:52:52 +0300 Subject: [PATCH] [Testing] Introduce ProjectResolveModel and parsing it from txt --- .../idea/multiplatform/multiPlatformSetup.kt | 55 ++++- .../jetbrains/kotlin/projectModel/Model.kt | 173 +++++++++++++++ .../jetbrains/kotlin/projectModel/Parser.kt | 202 ++++++++++++++++++ 3 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 idea/tests/org/jetbrains/kotlin/projectModel/Model.kt create mode 100644 idea/tests/org/jetbrains/kotlin/projectModel/Parser.kt diff --git a/idea/tests/org/jetbrains/kotlin/idea/multiplatform/multiPlatformSetup.kt b/idea/tests/org/jetbrains/kotlin/idea/multiplatform/multiPlatformSetup.kt index 348368fa939..54be4d209f9 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/multiplatform/multiPlatformSetup.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/multiplatform/multiPlatformSetup.kt @@ -10,8 +10,10 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.module.StdModuleTypes import com.intellij.openapi.roots.CompilerModuleExtension import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vfs.LocalFileSystem import junit.framework.TestCase +import org.jetbrains.kotlin.checkers.utils.clearFileFromDiagnosticMarkup import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime import org.jetbrains.kotlin.idea.framework.CommonLibraryKind import org.jetbrains.kotlin.idea.framework.JSLibraryKind @@ -25,6 +27,7 @@ import org.jetbrains.kotlin.platform.js.JsPlatforms import org.jetbrains.kotlin.platform.jvm.isJvm import org.jetbrains.kotlin.platform.js.isJs import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.jetbrains.kotlin.projectModel.* import java.io.File // allows to configure a test mpp project @@ -34,6 +37,56 @@ fun AbstractMultiModuleTest.setupMppProjectFromDirStructure(testRoot: File) { assert(testRoot.isDirectory) { testRoot.absolutePath + " must be a directory" } val dirs = testRoot.listFiles().filter { it.isDirectory } val rootInfos = dirs.map { parseDirName(it) } + doSetupProject(rootInfos) +} + +fun AbstractMultiModuleTest.setupMppProjectFromTextFile(testRoot: File) { + assert(testRoot.isDirectory) { testRoot.absolutePath + " must be a directory" } + val dependencies = File(testRoot, "dependencies.txt") + val projectModel = ProjectStructureParser(testRoot).parse(FileUtil.loadFile(dependencies)) + + check(projectModel.modules.isNotEmpty()) { "No modules were parsed from dependencies.txt" } + + doSetup(projectModel) +} + +fun AbstractMultiModuleTest.doSetup(projectModel: ProjectResolveModel) { + val resolveModulesToIdeaModules = projectModel.modules.map { resolveModule -> + val ideaModule = createModule(resolveModule.name) + addRoot( + ideaModule, + resolveModule.root, + isTestRoot = false, + transformContainedFiles = { if (it.extension == "kt") clearFileFromDiagnosticMarkup(it) } + ) + resolveModule to ideaModule + }.toMap() + + for ((resolveModule, ideaModule) in resolveModulesToIdeaModules.entries) { + resolveModule.dependencies.forEach { + when (val to = it.to) { + FullJdk -> ConfigLibraryUtil.configureSdk(module, PluginTestCaseBase.addJdk(testRootDisposable) { + PluginTestCaseBase.jdk(TestJdkKind.FULL_JDK) + }) + + is ResolveLibrary -> ideaModule.addLibrary(to.root, to.name, to.kind) + + else -> ideaModule.addDependency(resolveModulesToIdeaModules[to]!!) + } + } + } + + for ((resolveModule, ideaModule) in resolveModulesToIdeaModules.entries) { + val platform = resolveModule.platform + ideaModule.createFacet( + platform, + implementedModuleNames = resolveModule.dependencies.filter { it.kind == ResolveDependency.Kind.DEPENDS_ON }.map { it.to.name } + ) + ideaModule.enableMultiPlatform() + } +} + +private fun AbstractMultiModuleTest.doSetupProject(rootInfos: List) { val infosByModuleId = rootInfos.groupBy { it.moduleId } val modulesById = infosByModuleId.mapValues { (moduleId, infos) -> createModuleWithRoots(moduleId, infos) @@ -76,7 +129,7 @@ fun AbstractMultiModuleTest.setupMppProjectFromDirStructure(testRoot: File) { else -> { val commonModuleId = ModuleId(name, CommonPlatforms.defaultCommonPlatform) - module.createFacet(platform, implementedModuleName = commonModuleId.ideaModuleName()) + module.createFacet(platform, implementedModuleNames = listOf(commonModuleId.ideaModuleName())) module.enableMultiPlatform() modulesById[commonModuleId]?.let { commonModule -> diff --git a/idea/tests/org/jetbrains/kotlin/projectModel/Model.kt b/idea/tests/org/jetbrains/kotlin/projectModel/Model.kt new file mode 100644 index 00000000000..7d3ac5f8db7 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/projectModel/Model.kt @@ -0,0 +1,173 @@ +/* + * Copyright 2010-2019 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.projectModel + +import com.intellij.openapi.roots.libraries.PersistentLibraryKind +import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime +import org.jetbrains.kotlin.idea.framework.CommonLibraryKind +import org.jetbrains.kotlin.idea.framework.JSLibraryKind +import org.jetbrains.kotlin.platform.CommonPlatforms +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.platform.js.JsPlatforms +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.jetbrains.kotlin.utils.Printer +import java.io.File + +open class ProjectResolveModel(val modules: List) { + open class Builder { + val modules: MutableList = mutableListOf() + + open fun build(): ProjectResolveModel = ProjectResolveModel(modules.map { it.build() }) + } + +} + +open class ResolveModule( + val name: String, + val root: File, + val platform: TargetPlatform, + val dependencies: List +) { + final override fun toString(): String { + return buildString { renderDescription(Printer(this)) } + } + + open fun renderDescription(printer: Printer) { + printer.println("Module $name") + printer.pushIndent() + printer.println("platform=$platform") + printer.println("root=${root.absolutePath}") + printer.println("dependencies=${dependencies.joinToString { it.to.name }}") + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ResolveModule + + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + return name.hashCode() + } + + open class Builder { + private var state: State = State.NOT_BUILT + private var cachedResult: ResolveModule? = null + + var name: String? = null + var root: File? = null + var platform: TargetPlatform? = null + val dependencies: MutableList = mutableListOf() + + open fun build(): ResolveModule { + if (state == State.BUILT) return cachedResult!! + + require(state == State.NOT_BUILT) { "Re-building module $this with name $name (root at $root)" } + state = State.BUILDING + + val builtDependencies = dependencies.map { it.build() } + cachedResult = ResolveModule(name!!, root!!, platform!!, builtDependencies) + state = State.BUILT + + return cachedResult!! + } + + enum class State { + NOT_BUILT, + BUILDING, + BUILT + } + } +} + +sealed class ResolveLibrary( + name: String, + root: File, + platform: TargetPlatform, + val kind: PersistentLibraryKind<*>? +) : ResolveModule(name, root, platform, emptyList()) { + class Builder(val target: ResolveLibrary) : ResolveModule.Builder() { + override fun build(): ResolveModule = target + } +} + +sealed class Stdlib( + name: String, + root: File, + platform: TargetPlatform, + kind: PersistentLibraryKind<*>? +) : ResolveLibrary(name, root, platform, kind) { + + object CommonStdlib : Stdlib( + "stdlib-common", + ForTestCompileRuntime.stdlibCommonForTests(), + CommonPlatforms.defaultCommonPlatform, + CommonLibraryKind + ) + + object JvmStdlib : Stdlib( + "stdlib-jvm", + ForTestCompileRuntime.runtimeJarForTests(), + JvmPlatforms.defaultJvmPlatform, + null + ) + + object JsStdlib : Stdlib( + "stdlib-js", + ForTestCompileRuntime.stdlibJsForTests(), + JsPlatforms.defaultJsPlatform, + JSLibraryKind + ) +} + +sealed class KotlinTest( + name: String, + root: File, + platform: TargetPlatform, + kind: PersistentLibraryKind<*>? +) : ResolveLibrary(name, root, platform, kind) { + + object JsKotlinTest : KotlinTest( + "kotlin-test-js", + ForTestCompileRuntime.kotlinTestJsJarForTests(), + JsPlatforms.defaultJsPlatform, + JSLibraryKind + ) + + object JvmKotlinTest : KotlinTest( + "kotlin-test-jvm", + ForTestCompileRuntime.kotlinTestJUnitJarForTests(), + JvmPlatforms.defaultJvmPlatform, + null + ) +} + + +object FullJdk : ResolveLibrary( + "full-jdk", + File("fake file for full jdk"), + JvmPlatforms.defaultJvmPlatform, + null +) + +open class ResolveDependency(val to: ResolveModule, val kind: Kind) { + open class Builder { + var to: ResolveModule.Builder = ResolveModule.Builder() + var kind: Kind? = null + + open fun build(): ResolveDependency = ResolveDependency(to.build(), kind!!) + } + + enum class Kind { + DEPENDS_ON, + DEPENDENCY, + } +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/projectModel/Parser.kt b/idea/tests/org/jetbrains/kotlin/projectModel/Parser.kt new file mode 100644 index 00000000000..610f49c6d45 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/projectModel/Parser.kt @@ -0,0 +1,202 @@ +/* + * Copyright 2010-2019 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.projectModel + +import com.intellij.util.text.nullize +import org.jetbrains.kotlin.platform.CommonPlatforms +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.jetbrains.kotlin.platform.TargetPlatform +import java.io.File +import java.io.InputStreamReader +import java.io.Reader + +open class ProjectStructureParser(private val projectRoot: File) { + private val builderByName: MutableMap = hashMapOf() + private val predefinedBuilders: Map = hashMapOf( + "STDLIB_JS" to ResolveLibrary.Builder(Stdlib.JsStdlib), + "STDLIB_JVM" to ResolveLibrary.Builder(Stdlib.JvmStdlib), + "STDLIB_COMMON" to ResolveLibrary.Builder(Stdlib.CommonStdlib), + "FULL_JDK" to ResolveLibrary.Builder(FullJdk), + "KOTLIN_TEST_JS" to ResolveLibrary.Builder(KotlinTest.JsKotlinTest), + "KOTLIN_TEST_JVM" to ResolveLibrary.Builder(KotlinTest.JvmKotlinTest) + ) + + fun parse(text: String): ProjectResolveModel { + val reader = InputStreamReader(text.byteInputStream()) + while (reader.parseNextDeclaration()) { + } + + val projectBuilder = ProjectResolveModel.Builder() + projectBuilder.modules.addAll(builderByName.values) + + return projectBuilder.build() + } + + private fun Reader.parseNextDeclaration(): Boolean { + val firstWord = nextWord() ?: return false + when (firstWord) { + DEFINE_MODULE_KEYWORD -> parseModuleDefinition() + LINE_COMMENT_TOKEN -> consumeUntilFirst { it == '\n' } + else -> parseDependenciesDefinition(firstWord) + } + return true + } + + private fun Reader.parseModuleDefinition() { + val name = nextWord()!! + + // skip until attributes list begins + consumeUntilFirst { it.toString() == ATTRIBUTES_OPENING_BRACKET } + + // read whole attributes list + val attributesMap = readAttributes() + + require(builderByName[name] == null) { "Redefinition of module $name" } + builderByName[name] = ResolveModule.Builder().also { + it.name = name + initializeModuleByAttributes(it, attributesMap) + } + } + + protected open fun initializeModuleByAttributes(builder: ResolveModule.Builder, attributes: Map) { + val platformAttribute = attributes["platform"] + requireNotNull(platformAttribute) { "Missing required attribute 'platform' for module ${builder.name}" } + builder.platform = parsePlatform(platformAttribute) + + val root = attributes["root"] ?: builder.name!! + builder.root = File(projectRoot, root) + } + + private fun Reader.parseDependenciesDefinition(fromName: String) { + fun getDeclaredBuilder(name: String): ResolveModule.Builder = + requireNotNull(builderByName[name] ?: predefinedBuilders[name]) { + "Module $name wasn't declared. All modules should be declared explicitly" + } + + val fromBuilder = getDeclaredBuilder(fromName) + + val arrow = nextWord() + require(arrow == DEPENDENCIES_ARROW) { + "Malformed declaration: '$fromName $arrow ...' \n$HELP_TEXT" + } + + val dependencies = consumeUntilFirst { it == '{' }.split(",").map { + val toBuilder = getDeclaredBuilder(it.trim()) + ResolveDependency.Builder().apply { + to = toBuilder + } + } + + fromBuilder.dependencies.addAll(dependencies) + + val attributes = readAttributes() + initializeDependenciesByAttributes(dependencies, attributes) + } + + protected open fun initializeDependenciesByAttributes(dependencies: List, attributes: Map) { + fun applyForEach(action: (ResolveDependency.Builder).() -> Unit) { + dependencies.forEach(action) + } + + val kindAttribute = attributes["kind"] + requireNotNull(kindAttribute) { "Missing required attribute 'kind' for dependencies ${dependencies.joinToString()}" } + applyForEach { kind = ResolveDependency.Kind.valueOf(kindAttribute) } + + } + + private fun Reader.readAttributes(): Map { + val attributesString = consumeUntilFirst { it.toString() == ATTRIBUTES_CLOSING_BRACKET } + + return attributesString + .split(ATTRIBUTES_SEPARATOR) + .map { it.splitIntoExactlyTwoParts(ATTRIBUTE_VALUE_SEPARATOR) } + .toMap() + } + + private fun parsePlatform(platformString: String): TargetPlatform { + val platformsByPlatformName = CommonPlatforms.allSimplePlatforms + .map { it.single().toString() to it.single() } + .toMap() + + val platforms = parseRepeatableAttribute(platformString).map { + if (it == "JVM") + JvmPlatforms.defaultJvmPlatform.single() + else + platformsByPlatformName[it] ?: error("Unknown platform $it. Available platforms: ${platformsByPlatformName.keys.joinToString()}") + }.toSet() + + return TargetPlatform(platforms) + } + + protected fun parseRepeatableAttribute(value: String): List { + require(value.startsWith(REPEATABLE_ATTRIBUTE_OPENING_BRACKET) && value.endsWith(REPEATABLE_ATTRIBUTE_CLOSING_BRACKET)) { + "Value of repeatable attribute should be declared in square brackets: [foo, bar, baz]" + } + return value.removePrefix(REPEATABLE_ATTRIBUTE_OPENING_BRACKET) + .removeSuffix(REPEATABLE_ATTRIBUTE_CLOSING_BRACKET) + .split(REPEATABLE_ATTRIBUTE_VALUES_SEPARATOR) + .map { it.trim() } + } + + + companion object { + const val DEFINE_MODULE_KEYWORD = "MODULE" + const val DEPENDENCIES_ARROW = "->" + + const val ATTRIBUTES_OPENING_BRACKET = "{" + const val ATTRIBUTES_CLOSING_BRACKET = "}" + const val ATTRIBUTES_SEPARATOR = ";" + const val ATTRIBUTE_VALUE_SEPARATOR = "=" + + const val REPEATABLE_ATTRIBUTE_OPENING_BRACKET = "[" + const val REPEATABLE_ATTRIBUTE_CLOSING_BRACKET = "]" + const val REPEATABLE_ATTRIBUTE_VALUES_SEPARATOR = "," + + const val LINE_COMMENT_TOKEN = "//" + + val HELP_TEXT = "Possible declarations:\n" + + "- Module declaration: 'MODULE myModuleName { ...attributes... }\n" + + "- Module dependencies: myModuleName -> otherModule1, otherModule2, ..." + + "Note that each module should be explicitly declared before referring to it in dependencies" + } +} + +fun Reader.consumeUntilFirst(shouldStop: (Char) -> Boolean): String { + var char = nextChar() + return buildString { + while (char != null && !shouldStop(char!!)) { + append(char!!) + char = nextChar() + } + }.trim() +} + +private fun Reader.nextChar(): Char? = + read().takeUnless { it == -1 }?.toChar() + +private fun Reader.nextWord(): String? { + var char = nextChar() + return buildString { + // Skip all separators + while (char != null && char!!.isSeparator()) { + char = nextChar() + } + + // Read the word + while (char != null && !char!!.isSeparator()) { + append(char!!) + char = nextChar() + } + }.nullize() +} + +private fun Char.isSeparator() = isWhitespace() || this == '\n' + +private fun String.splitIntoExactlyTwoParts(separator: String): Pair { + val result = split(separator) + require(result.size == 2) { "$this can not be split into exactly two parts with separator $separator" } + return result[0].trim() to result[1].trim() +} \ No newline at end of file