[Testing] Introduce ProjectResolveModel and parsing it from txt

This commit is contained in:
Dmitry Savvinov
2019-04-26 15:52:52 +03:00
parent a59daafbb4
commit f011c21c94
3 changed files with 429 additions and 1 deletions
@@ -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<RootInfo>) {
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 ->
@@ -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<ResolveModule>) {
open class Builder {
val modules: MutableList<ResolveModule.Builder> = 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<ResolveDependency>
) {
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<ResolveDependency.Builder> = 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,
}
}
@@ -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<String, ResolveModule.Builder> = hashMapOf()
private val predefinedBuilders: Map<String, ResolveLibrary.Builder> = 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<String, String>) {
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<ResolveDependency.Builder>, attributes: Map<String, String>) {
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<String, String> {
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<String> {
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<String, String> {
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()
}