Compare commits

...

4 Commits

Author SHA1 Message Date
Dmitry Savvinov
a42c86bbba [Testing] Support test roots in dependencies.txt 2019-05-28 16:18:53 +03:00
Dmitry Savvinov
d63fa84a7e [Testing] Introduce ProjectResolveModel and parsing it from txt 2019-05-28 16:18:53 +03:00
Dmitry Savvinov
0d582ece93 [Testing] Add more flexiblity to AbstractMultiModuleTest
- Preserve root names
- Provide an ability to transform copied file
- Allow passing several implemented modules in AbstractMultiModuleTest
2019-05-28 16:18:53 +03:00
Dmitry Savvinov
fe55ef2cfc [Testing] Remove duplicate logic, clean-up CheckerTestUtil 2019-05-28 16:18:52 +03:00
8 changed files with 493 additions and 40 deletions

View File

@@ -67,9 +67,9 @@ class TextDiagnostic(
return result
}
fun asString(): String {
fun asString(withNewInference: Boolean = true, renderParameters: Boolean = true): String {
val result = StringBuilder()
if (inferenceCompatibility.abbreviation != null) {
if (withNewInference && inferenceCompatibility.abbreviation != null) {
result.append(inferenceCompatibility.abbreviation)
result.append(";")
}
@@ -78,9 +78,10 @@ class TextDiagnostic(
result.append(":")
}
result.append(name)
if (parameters != null) {
if (renderParameters && parameters != null) {
result.append("(")
result.append(StringUtil.join(parameters, { "\"$it\"" }, ", "))
result.append(StringUtil.join(parameters, { "\"${it.replace('\n', ' ')}\"" }, ", "))
result.append(")")
}
return result.toString()

View File

@@ -44,7 +44,7 @@ object CheckerTestUtil {
private const val IGNORE_DIAGNOSTIC_PARAMETER = "IGNORE"
private const val INDIVIDUAL_DIAGNOSTIC = """(\w+;)?(\w+:)?(\w+)(?:\(((?:".*?")(?:,\s*".*?")*)\))?"""
private val rangeStartOrEndPattern = Pattern.compile("(<!$INDIVIDUAL_DIAGNOSTIC(,\\s*$INDIVIDUAL_DIAGNOSTIC)*!>)|(<!>)")
internal val rangeStartOrEndPattern = Pattern.compile("(<!$INDIVIDUAL_DIAGNOSTIC(,\\s*$INDIVIDUAL_DIAGNOSTIC)*!>)|(<!>)")
val individualDiagnosticPattern: Pattern = Pattern.compile(INDIVIDUAL_DIAGNOSTIC)
fun getDiagnosticsIncludingSyntaxErrors(
@@ -328,6 +328,7 @@ object CheckerTestUtil {
return false
if (expected.parameters == null)
return true
if (actual.parameters == null || expected.parameters.size != actual.parameters.size)
return false
@@ -420,7 +421,7 @@ object CheckerTestUtil {
psiFile,
diagnostics,
emptyMap(),
com.intellij.util.Function { it.text },
{ it.text },
emptyList(),
false,
false
@@ -430,12 +431,12 @@ object CheckerTestUtil {
psiFile: PsiFile,
diagnostics: Collection<ActualDiagnostic>,
diagnosticToExpectedDiagnostic: Map<AbstractTestDiagnostic, TextDiagnostic>,
getFileText: com.intellij.util.Function<PsiFile, String>,
getFileText: (PsiFile) -> String,
uncheckedDiagnostics: Collection<PositionalTextDiagnostic>,
withNewInferenceDirective: Boolean,
renderDiagnosticMessages: Boolean
): StringBuffer {
val text = getFileText.`fun`(psiFile)
val text = getFileText(psiFile)
val result = StringBuffer()
val diagnosticsFiltered = diagnostics.filter { actualDiagnostic -> psiFile == actualDiagnostic.file }
if (diagnosticsFiltered.isEmpty() && uncheckedDiagnostics.isEmpty()) {
@@ -514,33 +515,15 @@ object CheckerTestUtil {
for (diagnostic in diagnostics) {
val expectedDiagnostic = diagnosticToExpectedDiagnostic[diagnostic]
if (expectedDiagnostic != null) {
val actualTextDiagnostic = TextDiagnostic.asTextDiagnostic(diagnostic)
val actualTextDiagnostic = TextDiagnostic.asTextDiagnostic(diagnostic)
if (expectedDiagnostic != null || !hasExplicitDefinitionOnlyOption(diagnostic)) {
val shouldRenderParameters =
renderDiagnosticMessages || expectedDiagnostic?.parameters != null
diagnosticsAsText.add(
if (compareTextDiagnostic(expectedDiagnostic, actualTextDiagnostic))
expectedDiagnostic.asString() else actualTextDiagnostic.asString()
actualTextDiagnostic.asString(withNewInferenceDirective, shouldRenderParameters)
)
} else if (!hasExplicitDefinitionOnlyOption(diagnostic)) {
val diagnosticText = StringBuilder()
if (withNewInferenceDirective && diagnostic.inferenceCompatibility.abbreviation != null) {
diagnosticText.append(diagnostic.inferenceCompatibility.abbreviation)
diagnosticText.append(";")
}
if (diagnostic.platform != null) {
diagnosticText.append(diagnostic.platform)
diagnosticText.append(":")
}
diagnosticText.append(diagnostic.name)
if (renderDiagnosticMessages) {
val textDiagnostic = TextDiagnostic.asTextDiagnostic(diagnostic)
if (textDiagnostic.parameters != null) {
diagnosticText
.append("(")
.append(textDiagnostic.parameters.joinToString(", "))
.append(")")
}
}
diagnosticsAsText.add(diagnosticText.toString())
}
}
}

View File

@@ -0,0 +1,16 @@
/*
* 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.checkers.utils
import java.io.File
fun clearFileFromDiagnosticMarkup(file: File) {
val text = file.readText()
val cleanText = clearTextFromDiagnosticMarkup(text)
file.writeText(cleanText)
}
fun clearTextFromDiagnosticMarkup(text: String): String = CheckerTestUtil.rangeStartOrEndPattern.matcher(text).replaceAll("")

View File

@@ -330,7 +330,7 @@ abstract class BaseDiagnosticsTest : KotlinMultiFileTestWithJava<TestModule, Tes
ktFile,
filteredDiagnostics,
diagnosticToExpectedDiagnostic,
com.intellij.util.Function { file -> file.text },
{ file -> file.text },
uncheckedDiagnostics,
withNewInferenceDirective,
renderDiagnosticMessages

View File

@@ -12,6 +12,7 @@ import com.intellij.openapi.roots.CompilerModuleExtension
import com.intellij.openapi.roots.ModuleRootModificationUtil
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 +26,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 +36,67 @@ 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 dependeciesTxt = File(testRoot, "dependencies.txt")
val projectModel = ProjectStructureParser(testRoot).parse(dependeciesTxt.readText())
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) }
)
if (resolveModule.testRoot != null) {
addRoot(
ideaModule,
resolveModule.testRoot,
isTestRoot = true,
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 +139,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 ->

View File

@@ -71,9 +71,18 @@ abstract class AbstractMultiModuleTest : DaemonAnalyzerTestCase() {
return super.createModule(path, moduleType)
}
fun addRoot(module: Module, sourceDirInTestData: File, isTestRoot: Boolean) {
val tmpRootDir = createTempDirectory()
fun addRoot(module: Module, sourceDirInTestData: File, isTestRoot: Boolean, transformContainedFiles: ((File) -> Unit)? = null) {
val tmpDir = createTempDirectory()
// Preserve original root name. This might be useful for later matching of copied files to original ones
val tmpRootDir = File(tmpDir, sourceDirInTestData.name).also { it.mkdir() }
FileUtil.copyDir(sourceDirInTestData, tmpRootDir)
if (transformContainedFiles != null) {
tmpRootDir.listFiles().forEach(transformContainedFiles)
}
val virtualTempDir = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(tmpRootDir)!!
object : WriteCommandAction.Simple<Unit>(project) {
override fun run() {
@@ -133,7 +142,7 @@ abstract class AbstractMultiModuleTest : DaemonAnalyzerTestCase() {
fun Module.createFacet(
platformKind: TargetPlatform? = null,
useProjectSettings: Boolean = true,
implementedModuleName: String? = null
implementedModuleNames: List<String>? = null
) {
WriteAction.run<Throwable> {
val modelsProvider = IdeModifiableModelsProviderImpl(project)
@@ -143,8 +152,8 @@ fun Module.createFacet(
modelsProvider.getModifiableRootModel(this@createFacet),
platformKind
)
if (implementedModuleName != null) {
this.implementedModuleNames = listOf(implementedModuleName)
if (implementedModuleNames != null) {
this.implementedModuleNames = implementedModuleNames
}
}
modelsProvider.commit()

View File

@@ -0,0 +1,176 @@
/*
* 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>,
val testRoot: File? = null
) {
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}")
if (testRoot != null) printer.println("testRoot=${testRoot.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()
var testRoot: File? = null
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, testRoot)
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,
}
}

View File

@@ -0,0 +1,205 @@
/*
* 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)
val testRoot = attributes["testRoot"]
if (testRoot != null) builder.testRoot = File(projectRoot, testRoot)
}
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()
}