[Testing] Introduce ProjectResolveModel and parsing it from txt
This commit is contained in:
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user