[mpp, tests] Introduce KpmCoreCases infrastructure
This commit is contained in:
@@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
id("jps-compatible")
|
||||
id("java-test-fixtures")
|
||||
}
|
||||
|
||||
publish()
|
||||
@@ -12,7 +13,11 @@ standardPublicJars()
|
||||
dependencies {
|
||||
implementation(kotlinStdlib())
|
||||
implementation(project(":kotlin-tooling-core"))
|
||||
testImplementation(kotlin("test-junit"))
|
||||
testFixturesImplementation(kotlin("test-junit"))
|
||||
testFixturesImplementation(project(":kotlin-tooling-core"))
|
||||
testFixturesImplementation(project(":core:util.runtime"))
|
||||
testFixturesImplementation(projectTests(":generators:test-generator"))
|
||||
testFixturesImplementation(project(":kotlin-reflect"))
|
||||
}
|
||||
|
||||
tasks.withType<KotlinJvmCompile>().configureEach {
|
||||
@@ -23,6 +28,16 @@ tasks.withType<KotlinJvmCompile>().configureEach {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<KotlinJvmCompile>("compileTestFixturesKotlin") {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += listOf(
|
||||
"-XXLanguage:+AllowSealedInheritorsInDifferentFilesOfSamePackage",
|
||||
"-XXLanguage:+SealedInterfaces",
|
||||
"-Xjvm-default=all"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<Jar>("jar") {
|
||||
callGroovy("manifestAttributes", manifest, project)
|
||||
}
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.project.model.coreCases
|
||||
|
||||
import org.jetbrains.kotlin.project.model.infra.KpmTestCase
|
||||
|
||||
/**
|
||||
* A representation of a Core Test Case.
|
||||
*
|
||||
* # Main idea:
|
||||
* - provides unified format for declaring project structure of core cases
|
||||
* - sync tested core cases across all subsystems
|
||||
*
|
||||
* # How it works:
|
||||
* - Core cases are defined inside [org.jetbrains.kotlin.project.model.coreCases]
|
||||
* (see "Conventions" below for exact format to follow) with the help of a DSL in
|
||||
* [org.jetbrains.kotlin.project.model.testDsl] package
|
||||
*
|
||||
* - Inherit [org.jetbrains.kotlin.project.model.infra.KpmCoreCasesTestRunner] in your
|
||||
* test runner. It will:
|
||||
* a) ensure that all core cases are covered and warn you when new one is added
|
||||
* b) inject core cases instances for you automatically
|
||||
*
|
||||
* - You can also use [org.jetbrains.kotlin.kpm.GenerateKpmTests]
|
||||
* if you don't need custom per-test-method assertions; it will generate test
|
||||
* cases which just call `doTest` on a passed [KpmTestCaseDescriptor] in a manner, similar
|
||||
* to other `*TestsGenerated`-runners
|
||||
*
|
||||
* # Conventions
|
||||
* 1. All Core Cases should inherit [KpmTestCaseDescriptor] marker
|
||||
* 2. All Core cases must reside in the [org.jetbrains.kotlin.project.model.coreCases]-package
|
||||
* 3. Each Core Case should be declared in a separate .kt-file, with the name equal
|
||||
* to the name of the case itself (`MyTestCase` -> `MyTestCase.kt`)
|
||||
* 4. Test Runner-class should consist of methods, which are named in the following
|
||||
* pattern: `test$caseName`
|
||||
* 5. (Optional) Test methods can declare a parameter of a [KpmTestCase]-type. Testing
|
||||
* infrastructure will inject an instance of corresponding [KpmTestCase]
|
||||
* (correspondence is determined based on test method naming convention from pt. 4)
|
||||
*
|
||||
* # Custom data, assertions, etc.
|
||||
* It is an explicit non-goal of this infrastructure to provide a DSL capable of
|
||||
* expressing assertions needed by an arbitrary subsystem. Instead, two extensibility
|
||||
* mechanisms are provided:
|
||||
* - `extras`, as in [KpmTestCase.extras]: essentially a way to attach custom userdata
|
||||
* to a given entity
|
||||
* - if needed, one can declare a new Core Case based on the previous one by calling
|
||||
* `describeCase` from "super-case" and then adding additional configuration
|
||||
*/
|
||||
sealed interface KpmTestCaseDescriptor {
|
||||
val name: String
|
||||
get() = this::class.simpleName ?: error("Can't get simpleName of a KpmTestCaseDescriptor ${this::class}. Is it an anonymous class?")
|
||||
|
||||
fun KpmTestCase.describeCase()
|
||||
|
||||
companion object {
|
||||
val allCaseDescriptorsByNames: Map<String, KpmTestCaseDescriptor> by lazy {
|
||||
doGetAllCases()
|
||||
}
|
||||
|
||||
val allCasesNames: Set<String>
|
||||
get() = allCaseDescriptorsByNames.keys
|
||||
|
||||
private fun doGetAllCases(): Map<String, KpmTestCaseDescriptor> = KpmTestCaseDescriptor::class.sealedSubclasses
|
||||
.map {
|
||||
requireNotNull(it.objectInstance) {
|
||||
"Can't get object instance for $it. Check that it is declared as an `object` "
|
||||
}
|
||||
}
|
||||
.associateBy { it.name }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun KpmTestCaseDescriptor.instantiateCase(): KpmTestCase = KpmTestCase(name).apply { describeCase() }
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.project.model.infra
|
||||
|
||||
import org.jetbrains.kotlin.project.model.coreCases.KpmTestCaseDescriptor
|
||||
import org.jetbrains.kotlin.project.model.coreCases.instantiateCase
|
||||
import org.junit.jupiter.api.extension.*
|
||||
import java.lang.reflect.Method
|
||||
|
||||
class KpmCoreCasesJunitParameterResolver : ParameterResolver {
|
||||
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean =
|
||||
parameterContext.parameter.type == KpmTestCaseDescriptor::class.java
|
||||
|
||||
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any {
|
||||
require(parameterContext.parameter.type == KpmTestCaseDescriptor::class.java)
|
||||
val kpmCaseName = extensionContext.requiredTestMethod.kpmCaseName
|
||||
val caseDescriptor = KpmTestCaseDescriptor.allCaseDescriptorsByNames[kpmCaseName]
|
||||
requireNotNull(caseDescriptor) {
|
||||
"Can't find KpmCoreCase for name $kpmCaseName while " +
|
||||
"\n injecting parameter ${parameterContext.parameter} into \n" +
|
||||
"${extensionContext.requiredTestMethod}"
|
||||
}
|
||||
return caseDescriptor.instantiateCase()
|
||||
}
|
||||
}
|
||||
|
||||
private val Method.kpmCaseName: String
|
||||
get() {
|
||||
val testCaseName = this.name.substringAfter("test")
|
||||
require(testCaseName in KpmTestCaseDescriptor.allCasesNames) {
|
||||
"Can't find matching core case for name ${testCaseName}.\n" +
|
||||
"Please check that the test method follow pattern 'test\$caseName', \n" +
|
||||
"where '\$caseName' is a name as declared in 'o.j.k.project.model.coreCases'-package\n" +
|
||||
"\n" +
|
||||
"Known cases:\n" +
|
||||
"${KpmTestCaseDescriptor.allCasesNames}"
|
||||
}
|
||||
|
||||
return testCaseName
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.project.model.infra
|
||||
|
||||
import org.jetbrains.kotlin.project.model.coreCases.KpmTestCaseDescriptor
|
||||
import org.jetbrains.kotlin.project.model.infra.generate.generateTestMethodsTemplateForCases
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
|
||||
/**
|
||||
* Base class for testing KPM Core cases
|
||||
*
|
||||
* All core cases are listed here as abstract tests and will be required,
|
||||
* to be overridden introducing a compile-time check that as soon as new
|
||||
* case is added, it will be covered in all inheritors.
|
||||
*
|
||||
* For situation when a new Core Case is added, but is mistakenly not added
|
||||
* to this interface, there's a [checkAllCoreCasesCovered], which will enforce
|
||||
* that all Core Cases have a respective method in this interface
|
||||
*
|
||||
* Additionally, this interface uses [KpmCoreCasesJunitParameterResolver], which
|
||||
* will inject a corresponding [KpmTestCaseDescriptor] into a test-method based on the
|
||||
* method's name
|
||||
*/
|
||||
@ExtendWith(KpmCoreCasesJunitParameterResolver::class)
|
||||
interface KpmCoreCasesTestRunner {
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testSimpleProjectToProject(case: KpmTestCase)
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testSimpleTwoLevel(case: KpmTestCase)
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun checkAllCoreCasesCovered() {
|
||||
val testRunnerClass = this::class.java
|
||||
|
||||
val testCasesNames = testRunnerClass.methods.asSequence()
|
||||
.map { it.name }
|
||||
.filter { it.startsWith("test") }
|
||||
.map { it.substringAfter("test") }
|
||||
.toSet()
|
||||
|
||||
val uncoveredCases = KpmTestCaseDescriptor.allCasesNames - testCasesNames
|
||||
if (uncoveredCases.isNotEmpty()) {
|
||||
Assertions.fail<Nothing>(
|
||||
"""
|
||||
Test runner '${testRunnerClass.canonicalName}'
|
||||
has some KPM Core Test Cases uncovered:
|
||||
|
||||
${uncoveredCases.joinToString()}
|
||||
""".trimIndent() + "\n\n" + fixSuggestion(uncoveredCases)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixSuggestion(missingCases: Set<String>): String =
|
||||
"Please re-run \"Generate KPM Tests\"-task to generate missing methods,\n" +
|
||||
"or insert the following scaffold if the test runner uses custom assertions:\n\n" +
|
||||
generateTestMethodsTemplateForCases(missingCases)
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.project.model.infra
|
||||
|
||||
import org.jetbrains.kotlin.project.model.*
|
||||
import org.jetbrains.kotlin.project.model.testDsl.*
|
||||
import org.jetbrains.kotlin.project.model.utils.ObservableIndexedCollection
|
||||
import org.jetbrains.kotlin.tooling.core.MutableExtras
|
||||
import org.jetbrains.kotlin.tooling.core.mutableExtrasOf
|
||||
import java.io.File
|
||||
|
||||
interface KpmTestEntity {
|
||||
val name: String
|
||||
}
|
||||
|
||||
class KpmTestCase(
|
||||
override val name: String,
|
||||
) : KpmTestEntity {
|
||||
val projects: ObservableIndexedCollection<TestKpmModuleContainer> = ObservableIndexedCollection()
|
||||
val extras: MutableExtras = mutableExtrasOf()
|
||||
|
||||
override fun toString(): String = "Case $name"
|
||||
}
|
||||
|
||||
class TestKpmModuleContainer(
|
||||
val containingCase: KpmTestCase,
|
||||
override val name: String,
|
||||
) : KpmTestEntity {
|
||||
val modules: ObservableIndexedCollection<TestKpmModule> = ObservableIndexedCollection()
|
||||
val extras: MutableExtras = mutableExtrasOf()
|
||||
|
||||
fun applyDefaults() {
|
||||
module("main")
|
||||
}
|
||||
|
||||
override fun toString(): String = ":$name"
|
||||
}
|
||||
|
||||
class TestKpmModule(
|
||||
val containingProject: TestKpmModuleContainer,
|
||||
override val moduleIdentifier: KpmModuleIdentifier,
|
||||
) : KpmTestEntity, KpmModule {
|
||||
override val fragments: ObservableIndexedCollection<TestKpmFragment> = ObservableIndexedCollection()
|
||||
override val plugins: MutableSet<KpmCompilerPlugin> = mutableSetOf()
|
||||
val extras: MutableExtras = mutableExtrasOf()
|
||||
|
||||
override val name: String
|
||||
get() = moduleIdentifier.moduleClassifier ?: "main"
|
||||
|
||||
fun applyDefaults() {
|
||||
fragment("common")
|
||||
}
|
||||
}
|
||||
|
||||
open class TestKpmFragment(
|
||||
override val containingModule: TestKpmModule,
|
||||
override val fragmentName: String,
|
||||
) : KpmTestEntity, KpmFragment {
|
||||
override var languageSettings: LanguageSettings? = null
|
||||
val extras: MutableExtras = mutableExtrasOf()
|
||||
override val kotlinSourceRoots: MutableList<File> = mutableListOf()
|
||||
override val declaredModuleDependencies: MutableList<KpmModuleDependency> = mutableListOf()
|
||||
override val declaredRefinesDependencies: MutableList<TestKpmFragment> = mutableListOf()
|
||||
override val name: String get() = fragmentName
|
||||
|
||||
fun applyDefaults() {
|
||||
refines(containingModule.common)
|
||||
}
|
||||
}
|
||||
|
||||
class TestKpmVariant(
|
||||
containingModule: TestKpmModule,
|
||||
fragmentName: String,
|
||||
) : TestKpmFragment(containingModule, fragmentName), KpmTestEntity, KpmVariant {
|
||||
override val variantAttributes: MutableMap<KotlinAttributeKey, String> = mutableMapOf()
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.project.model.infra.generate
|
||||
|
||||
import org.jetbrains.kotlin.generators.MethodGenerator
|
||||
import org.jetbrains.kotlin.generators.model.MethodModel
|
||||
import org.jetbrains.kotlin.test.util.KtTestUtil
|
||||
import org.jetbrains.kotlin.utils.Printer
|
||||
|
||||
object KpmCoreCaseTestMethodGenerator : MethodGenerator<KpmCoreCaseTestMethodModel>() {
|
||||
override val kind: MethodModel.Kind
|
||||
get() = KpmCoreCaseTestMethodModel.Kind
|
||||
|
||||
override fun generateSignature(method: KpmCoreCaseTestMethodModel, p: Printer) {
|
||||
p.print("public void test${method.name}(KpmTestCase kpmCase) throws Exception")
|
||||
}
|
||||
|
||||
override fun generateBody(method: KpmCoreCaseTestMethodModel, p: Printer) {
|
||||
val filePath = KtTestUtil.getFilePath(method.pathToTestCase)
|
||||
p.println("runTest(SourcesKt.addSourcesFromCanonicalFileStructure(kpmCase, new File(\"$filePath\")));")
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.project.model.infra.generate
|
||||
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import org.jetbrains.kotlin.generators.model.MethodModel
|
||||
import org.jetbrains.kotlin.test.util.KtTestUtil
|
||||
import java.io.File
|
||||
|
||||
class KpmCoreCaseTestMethodModel(
|
||||
override val name: String, // equals to name of corresponding KpmCoreCase
|
||||
internal val pathToTestSourcesRootDir: File,
|
||||
internal val pathToTestCase: File,
|
||||
) : MethodModel {
|
||||
object Kind : MethodModel.Kind()
|
||||
|
||||
override val dataString: String
|
||||
get() {
|
||||
val path = FileUtil.getRelativePath(pathToTestSourcesRootDir, pathToTestCase)!!
|
||||
return KtTestUtil.getFilePath(File(path))
|
||||
}
|
||||
override val tags: List<String>
|
||||
get() = emptyList()
|
||||
|
||||
override val kind: MethodModel.Kind
|
||||
get() = Kind
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.project.model.infra.generate
|
||||
|
||||
import org.jetbrains.kotlin.generators.model.AnnotationModel
|
||||
import org.jetbrains.kotlin.generators.model.MethodModel
|
||||
import org.jetbrains.kotlin.generators.model.TestClassModel
|
||||
import org.jetbrains.kotlin.project.model.coreCases.KpmCoreCase
|
||||
import org.jetbrains.kotlin.project.model.infra.KpmTestCase
|
||||
import org.jetbrains.kotlin.project.model.infra.generateTemplateCanonicalFileStructure
|
||||
import org.jetbrains.kotlin.test.util.KtTestUtil
|
||||
import java.io.File
|
||||
|
||||
class KpmCoreCasesTestClassModel(
|
||||
// Root of testdata folder, inside it expected to find one folder per testcase
|
||||
// E.g. [additionalTestDataRoot] = "foo/bar", and we have KpmCoreCases "A" and "B"
|
||||
// Then folders "foo/bar/A" and "foo/bar/B" expected to exist and contain sources
|
||||
// for cases A, B. If they do not exist, they will be auto-generated with template
|
||||
// sources.
|
||||
private val additionalTestDataRoot: File,
|
||||
) : TestClassModel() {
|
||||
// Metadata-annotations enable IDE support for tests, like "Navigate to test data" action
|
||||
override val dataString: String // Will be used in @TestMetadata, needs to be path to folder with test data
|
||||
get() = KtTestUtil.getFilePath(additionalTestDataRoot)
|
||||
|
||||
override val dataPathRoot: String? // Will be used in @com.intellij.testFramework.TestDataPath
|
||||
get() = null // $PROJECT_ROOT will be used instead
|
||||
|
||||
override val name: String
|
||||
get() = error("Unused by infra, shouldn't be called")
|
||||
|
||||
override val tags: List<String>
|
||||
get() = emptyList()
|
||||
|
||||
override val innerTestClasses: Collection<TestClassModel>
|
||||
get() = emptyList()
|
||||
|
||||
override val methods: Collection<MethodModel> by lazy {
|
||||
doCollectMethods()
|
||||
}
|
||||
|
||||
override val isEmpty: Boolean
|
||||
get() = methods.isEmpty()
|
||||
|
||||
override val imports: Set<Class<*>>
|
||||
get() = super.imports + setOf(
|
||||
KpmTestCase::class.java,
|
||||
this::class.java.classLoader.loadClass("org.jetbrains.kotlin.project.model.infra.SourcesKt") // file-facade :(
|
||||
)
|
||||
|
||||
override val annotations: Collection<AnnotationModel>
|
||||
get() = emptyList() // Don't need to annotate with `@ExtendWith`, because we inherit [KpmCoreCasesTestRunner]
|
||||
|
||||
private fun doCollectMethods(): Collection<MethodModel> {
|
||||
val allCoreCasesNames: Set<String> = KpmCoreCase.allCasesNames
|
||||
val allAdditionalTestdata: Set<File> = additionalTestDataRoot.listFiles().orEmpty().toSet()
|
||||
|
||||
val methodModelsForCoreCasesWithExistingTestData = allAdditionalTestdata.map { testDataForCase ->
|
||||
check(!testDataForCase.isFile) {
|
||||
"Expected to find only folders in testdata root ${additionalTestDataRoot.absolutePath}\n," +
|
||||
"but found a file ${testDataForCase.absolutePath}"
|
||||
}
|
||||
|
||||
check(testDataForCase.name in allCoreCasesNames) {
|
||||
"Each folder name in test data should correspond to some KPM Core Case.\n" +
|
||||
" Found folder ${testDataForCase.name}\n" +
|
||||
" All core cases: $allCoreCasesNames"
|
||||
}
|
||||
|
||||
KpmCoreCaseTestMethodModel(
|
||||
testDataForCase.name,
|
||||
additionalTestDataRoot,
|
||||
testDataForCase
|
||||
)
|
||||
}
|
||||
|
||||
val coreCasesNamesWithoutTestData = allCoreCasesNames - methodModelsForCoreCasesWithExistingTestData.mapTo(mutableSetOf()) {
|
||||
it.name
|
||||
}
|
||||
|
||||
val methodModelsForCoreCasesWithoutTestData = coreCasesNamesWithoutTestData.map {
|
||||
val expectedTestDataPath = additionalTestDataRoot.resolve(it)
|
||||
val kpmCoreCase = KpmCoreCase.allCasesByNames[it]!!
|
||||
println("Generating template sources testdata for uncovered KPM Core Case $it at ${additionalTestDataRoot.path}")
|
||||
kpmCoreCase.case.generateTemplateCanonicalFileStructure(expectedTestDataPath)
|
||||
|
||||
KpmCoreCaseTestMethodModel(it, additionalTestDataRoot, expectedTestDataPath)
|
||||
}
|
||||
|
||||
return methodModelsForCoreCasesWithExistingTestData + methodModelsForCoreCasesWithoutTestData
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.project.model.infra
|
||||
|
||||
import org.jetbrains.kotlin.project.model.KpmLocalModuleIdentifier
|
||||
import org.jetbrains.kotlin.project.model.KpmMavenModuleIdentifier
|
||||
import org.jetbrains.kotlin.project.model.nativeTarget
|
||||
import org.jetbrains.kotlin.project.model.platform
|
||||
import org.jetbrains.kotlin.utils.Printer
|
||||
|
||||
@Suppress("unused") // useful for debugging
|
||||
fun KpmTestCase.renderDeclarationDsl(): String {
|
||||
val p = Printer(StringBuilder())
|
||||
p.render(this)
|
||||
return p.toString()
|
||||
}
|
||||
|
||||
private fun Printer.render(case: KpmTestCase) {
|
||||
println("val ${case.name} = describeCase(\"${case.name}\") {")
|
||||
pushIndent()
|
||||
val projectsSorted = case.projects.sortedBy { it.name }
|
||||
for (project in projectsSorted) {
|
||||
render(project)
|
||||
if (project !== projectsSorted.last()) println()
|
||||
}
|
||||
popIndent()
|
||||
println("}")
|
||||
|
||||
}
|
||||
|
||||
private fun Printer.render(project: TestKpmModuleContainer) {
|
||||
println("project(\"${project.name}\") {")
|
||||
pushIndent()
|
||||
val modulesSorted = project.modules.sortedBy { it.name }
|
||||
for (module in modulesSorted) {
|
||||
render(module)
|
||||
if (module !== modulesSorted.last()) println()
|
||||
}
|
||||
popIndent()
|
||||
println("}")
|
||||
}
|
||||
|
||||
private fun Printer.render(module: TestKpmModule) {
|
||||
println("module(\"${module.name}\") {")
|
||||
pushIndent()
|
||||
val fragmentsSorted = module.fragments.sortedBy { it.name }
|
||||
|
||||
// printedFragmentDeclaration is needed for pretty-printing separating new line between fragments
|
||||
// declarations and their dependencies only in case both are present (i.e. no trailing newlines)
|
||||
var printedFragmentDeclaration = false
|
||||
for (fragment in fragmentsSorted) {
|
||||
printedFragmentDeclaration = printedFragmentDeclaration or renderFragmentDeclaration(fragment)
|
||||
}
|
||||
|
||||
for (fragment in fragmentsSorted) {
|
||||
renderFragmentDependencies(fragment, printedFragmentDeclaration)
|
||||
}
|
||||
|
||||
popIndent()
|
||||
println("}")
|
||||
}
|
||||
|
||||
private fun Printer.renderFragmentDeclaration(fragment: TestKpmFragment): Boolean {
|
||||
if (fragment.name == "common") return false
|
||||
|
||||
when {
|
||||
fragment !is TestKpmVariant -> println("fragment(\"${fragment.name}\")")
|
||||
fragment.platform == "jvm" -> println("jvm()")
|
||||
fragment.platform == "js" -> println("js()")
|
||||
fragment.nativeTarget == "linux" -> println("linux()")
|
||||
fragment.nativeTarget == "macosX64" -> println("macosX64()")
|
||||
fragment.nativeTarget == "android" -> println("android()")
|
||||
else -> error("Unknown platform: ${fragment.platform}, nativeTarget=${fragment.nativeTarget}")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun Printer.renderFragmentDependencies(fragment: TestKpmFragment, prependWithEmptyLine: Boolean) {
|
||||
var printEmptyLineOnce: Boolean = prependWithEmptyLine
|
||||
fun println(text: String) {
|
||||
if (printEmptyLineOnce) {
|
||||
this.println()
|
||||
printEmptyLineOnce = false
|
||||
}
|
||||
this.println(text)
|
||||
}
|
||||
|
||||
for (refinedFragment in fragment.declaredRefinesDependencies.sortedBy { it.name }) {
|
||||
if (refinedFragment.name == "common") continue
|
||||
println("${fragment.name} refines ${refinedFragment.name}")
|
||||
}
|
||||
|
||||
val moduleDependenciesSorted = fragment.declaredModuleDependencies.sortedBy { it.moduleIdentifier.toString() }
|
||||
for (dependencyModule in moduleDependenciesSorted) {
|
||||
when (val id = dependencyModule.moduleIdentifier) {
|
||||
is KpmLocalModuleIdentifier -> {
|
||||
val projectId = id.projectId
|
||||
println("${fragment.name} depends ${"project(\"$projectId\")"}")
|
||||
}
|
||||
|
||||
is KpmMavenModuleIdentifier -> {
|
||||
val group = id.group
|
||||
val name = id.name
|
||||
println("${fragment.name} depends ${"maven(\"$group\", \"$name\")"}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.project.model.infra
|
||||
|
||||
import org.jetbrains.kotlin.project.model.coreCases.KpmTestCaseDescriptor
|
||||
import org.jetbrains.kotlin.project.model.coreCases.instantiateCase
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* In this file you can find support for decorating an existing [KpmTestCase] with sources
|
||||
*
|
||||
* The low-level API is [addSources]
|
||||
*
|
||||
* For the convenience, the concept of "canonical sources file structure" is introduced.
|
||||
* You can ask to generate template of this structure in designated place by calling [generateTemplateCanonicalFileStructure],
|
||||
* which is useful when you have a [KpmTestCase] and want to add some manually-written sources (test data, usually) to it.
|
||||
*
|
||||
* After that, you can use [addSourcesFromCanonicalFileStructure] to automatically decorate an existing
|
||||
* [KpmTestCase] with sources pulled from the specified folder
|
||||
*
|
||||
* See [generateTemplateCanonicalFileStructure] KDoc for details on the canonical sources file structure
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Warning! Mutates passed test case
|
||||
*/
|
||||
fun KpmTestCase.addSourcesFromCanonicalFileStructure(root: File): KpmTestCase {
|
||||
return addSources { fragment ->
|
||||
val canonicalFragmentTestdataFolder = fragment.canonicalSourceFolderAbsolute(root)
|
||||
require(canonicalFragmentTestdataFolder.exists()) {
|
||||
"Can't find testdata for fragment $fragment at ${canonicalFragmentTestdataFolder.absolutePath}"
|
||||
}
|
||||
|
||||
canonicalFragmentTestdataFolder.listFiles()?.toList().orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning! Mutates passed test case
|
||||
*/
|
||||
fun KpmTestCase.addSources(sourcesForFragment: (TestKpmFragment) -> Iterable<File>): KpmTestCase {
|
||||
val allFragments = projects.flatMap { it.modules.flatMap { it.fragments } }
|
||||
for (fragment in allFragments) {
|
||||
val sources = sourcesForFragment(fragment)
|
||||
fragment.kotlinSourceRoots.addAll(sources)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a canonical source-files structure at designated [root] for a given [KpmTestCaseDescriptor]
|
||||
*
|
||||
* In general, each [TestKpmFragment] will have a folder formed as following:
|
||||
* `root/$fragmentProject.name/$fragmentModule.name/$fragment.name`
|
||||
*
|
||||
* However, in order to reduce nesting for simple common cases, two amendments are applied:
|
||||
* - if a given [KpmTestCaseDescriptor] has only one single [TestKpmModuleContainer], then the project folder is omitted,
|
||||
* and modules of that project are embedded straight into the [root]
|
||||
* - similarly, if a given [TestKpmModuleContainer] has only one single [TestKpmModule], then the module folder is omitted,
|
||||
* and fragments of that module are embedded straight into the parent-directory.
|
||||
* This rule is applied for each project separately.
|
||||
*
|
||||
* Both amendments can be applied simultaneously, so for a test case with a single project and single module, fragments will live
|
||||
* directly inside [root]
|
||||
*/
|
||||
fun KpmTestCaseDescriptor.generateTemplateCanonicalFileStructure(root: File) {
|
||||
val case = instantiateCase()
|
||||
val allFragments = case.projects.flatMap { it.modules.flatMap { it.fragments } }
|
||||
allFragments.forEach { fragment ->
|
||||
val canonicalFragmentTestdataFolder = fragment.canonicalSourceFolderAbsolute(root)
|
||||
canonicalFragmentTestdataFolder.mkdirs()
|
||||
|
||||
val templateSources = canonicalFragmentTestdataFolder.resolve(fragment.fragmentName + ".kt")
|
||||
templateSources.writeText(PLACEHOLDER_SOURCES_TEXT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestKpmFragment.canonicalSourceFolderAbsolute(root: File): File {
|
||||
val pathRelativeToRoot = canonicalSourceFolderRelative()
|
||||
return root.resolve(pathRelativeToRoot)
|
||||
}
|
||||
|
||||
private fun TestKpmFragment.canonicalSourceFolderRelative(): File {
|
||||
val module = containingModule
|
||||
val project = module.containingProject
|
||||
val case = project.containingCase
|
||||
|
||||
val isSingleModule = project.modules.size == 1
|
||||
val isSingleProject = case.projects.size == 1
|
||||
|
||||
val pathParts = listOfNotNull(
|
||||
project.name.takeIf { !isSingleProject },
|
||||
module.name.takeIf { !isSingleModule },
|
||||
fragmentName
|
||||
)
|
||||
|
||||
return File(pathParts.joinToString(separator = "/"))
|
||||
}
|
||||
|
||||
private val PLACEHOLDER_SOURCES_TEXT = """
|
||||
/**
|
||||
* Generated by [org.jetbrains.kotlin.project.model.infra.${KpmTestCaseDescriptor::generateTemplateCanonicalFileStructure.name}]
|
||||
*
|
||||
* Write your testdata sources here, or remove the file, if it is not needed
|
||||
*/
|
||||
""".trimIndent()
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("UnusedReceiverParameter") // receivers are convenient for DSL scoping
|
||||
|
||||
package org.jetbrains.kotlin.project.model.testDsl
|
||||
|
||||
import org.jetbrains.kotlin.project.model.KpmLocalModuleIdentifier
|
||||
import org.jetbrains.kotlin.project.model.KpmMavenModuleIdentifier
|
||||
import org.jetbrains.kotlin.project.model.infra.KpmTestEntity
|
||||
|
||||
fun KpmTestEntity.project(projectId: String): KpmLocalModuleIdentifier = KpmLocalModuleIdentifier("", projectId, null)
|
||||
|
||||
val KpmLocalModuleIdentifier.test: KpmLocalModuleIdentifier
|
||||
get() = KpmLocalModuleIdentifier(buildId, projectId, "test")
|
||||
|
||||
// TODO: custom aux modules
|
||||
|
||||
// TODO: scopes
|
||||
fun KpmTestEntity.maven(group: String, name: String): KpmMavenModuleIdentifier = KpmMavenModuleIdentifier(group, name, null)
|
||||
|
||||
// TODO: published aux modules
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.project.model.testDsl
|
||||
|
||||
import org.jetbrains.kotlin.project.model.infra.KpmTestCase
|
||||
import org.jetbrains.kotlin.project.model.infra.TestKpmFragment
|
||||
import org.jetbrains.kotlin.project.model.infra.TestKpmModule
|
||||
import org.jetbrains.kotlin.project.model.infra.TestKpmModuleContainer
|
||||
|
||||
fun KpmTestCase.project(
|
||||
name: String,
|
||||
applyDefaults: Boolean = true,
|
||||
configure: TestKpmModuleContainer.() -> Unit = { }
|
||||
): TestKpmModuleContainer {
|
||||
val project = projects.getOrPut(name) { TestKpmModuleContainer(this, name) }
|
||||
if (applyDefaults) project.applyDefaults()
|
||||
project.configure()
|
||||
return project
|
||||
}
|
||||
|
||||
fun KpmTestCase.projectNamed(name: String) = projects[name]
|
||||
?: error("Project with name $name doesn't exist. Existing projects: ${projects.joinToString { it.name }}")
|
||||
|
||||
fun KpmTestCase.allModules(configure: TestKpmModule.() -> Unit) {
|
||||
projects.withAll {
|
||||
modules.withAll(configure)
|
||||
}
|
||||
}
|
||||
|
||||
fun KpmTestCase.allFragments(configure: TestKpmFragment.() -> Unit) {
|
||||
projects.withAll {
|
||||
modules.withAll {
|
||||
fragments.withAll(configure)
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package org.jetbrains.kotlin.project.model.testDsl
|
||||
|
||||
import org.jetbrains.kotlin.project.model.KpmModuleDependency
|
||||
import org.jetbrains.kotlin.project.model.infra.TestKpmFragment
|
||||
import org.jetbrains.kotlin.project.model.infra.TestKpmModule
|
||||
|
||||
fun TestKpmFragment.depends(module: TestKpmModule): TestKpmFragment {
|
||||
declaredModuleDependencies += KpmModuleDependency(module.moduleIdentifier)
|
||||
return this
|
||||
}
|
||||
|
||||
fun TestKpmFragment.refines(fragment: TestKpmFragment): TestKpmFragment {
|
||||
declaredRefinesDependencies += fragment
|
||||
return this
|
||||
}
|
||||
|
||||
fun TestKpmFragment.fragment(name: String, applyDefaults: Boolean = true, configure: TestKpmFragment.() -> Unit = { }): TestKpmFragment {
|
||||
return containingModule.fragment(name, applyDefaults, configure).also { subFragment -> subFragment.refines(this) }
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package org.jetbrains.kotlin.project.model.testDsl
|
||||
|
||||
import org.jetbrains.kotlin.project.model.KpmLocalModuleIdentifier
|
||||
import org.jetbrains.kotlin.project.model.infra.TestKpmModuleContainer
|
||||
import org.jetbrains.kotlin.project.model.infra.TestKpmModule
|
||||
|
||||
fun TestKpmModuleContainer.allModules(action: TestKpmModule.() -> Unit) {
|
||||
modules.withAll(action)
|
||||
}
|
||||
|
||||
fun TestKpmModuleContainer.module(name: String, applyDefaults: Boolean = true, configure: TestKpmModule.() -> Unit = { }): TestKpmModule {
|
||||
val module = modules.getOrPut(name) {
|
||||
val id = KpmLocalModuleIdentifier(
|
||||
buildId = "",
|
||||
projectId = this.name,
|
||||
moduleClassifier = name.takeIf { it != "main" }
|
||||
)
|
||||
val module = TestKpmModule(this, id)
|
||||
if (applyDefaults) module.applyDefaults()
|
||||
|
||||
module
|
||||
}
|
||||
configure(module)
|
||||
return module
|
||||
}
|
||||
|
||||
fun TestKpmModuleContainer.moduleNamed(name: String): TestKpmModule =
|
||||
modules[name] ?: error("Module with name $name doesn't exist. Existing modules: ${modules.joinToString { it.name }}")
|
||||
|
||||
val TestKpmModuleContainer.main get() = moduleNamed("main")
|
||||
val TestKpmModuleContainer.test get() = moduleNamed("test")
|
||||
|
||||
fun TestKpmModuleContainer.depends(other: TestKpmModuleContainer): TestKpmModuleContainer {
|
||||
main.depends(other)
|
||||
return this
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.project.model.testDsl
|
||||
|
||||
import org.jetbrains.kotlin.project.model.KotlinPlatformTypeAttribute
|
||||
import org.jetbrains.kotlin.project.model.infra.TestKpmFragment
|
||||
import org.jetbrains.kotlin.project.model.infra.TestKpmModuleContainer
|
||||
import org.jetbrains.kotlin.project.model.infra.TestKpmModule
|
||||
import org.jetbrains.kotlin.project.model.infra.TestKpmVariant
|
||||
|
||||
fun TestKpmModule.fragment(name: String, applyDefaults: Boolean = true, configure: TestKpmFragment.() -> Unit = { }): TestKpmFragment {
|
||||
val result = fragments.getOrPut(name) {
|
||||
val fragment = TestKpmFragment(this, name)
|
||||
fragment
|
||||
}
|
||||
if (applyDefaults) result.applyDefaults()
|
||||
return result.also(configure)
|
||||
}
|
||||
|
||||
fun TestKpmModule.fragmentNamed(name: String): TestKpmFragment =
|
||||
fragments[name] ?: error("Fragment with name $name doesn't exist. Existing fragments ${fragments.joinToString { it.name }}")
|
||||
|
||||
fun TestKpmModule.variant(
|
||||
name: String,
|
||||
platform: String, // from KotlinPlatformTypeAttribute
|
||||
applyDefaults: Boolean = true,
|
||||
configure: TestKpmVariant.() -> Unit = { }
|
||||
): TestKpmVariant {
|
||||
val result = fragments.getOrPut(name) {
|
||||
val variant = TestKpmVariant(this, name)
|
||||
variant.variantAttributes[KotlinPlatformTypeAttribute] = platform
|
||||
variant
|
||||
} as TestKpmVariant
|
||||
if (applyDefaults) result.applyDefaults()
|
||||
result.configure()
|
||||
return result
|
||||
}
|
||||
|
||||
fun TestKpmModule.depends(otherModule: TestKpmModule): TestKpmModule {
|
||||
common.depends(otherModule)
|
||||
return this
|
||||
}
|
||||
|
||||
fun TestKpmModule.depends(otherProject: TestKpmModuleContainer): TestKpmModule {
|
||||
common.depends(otherProject.main)
|
||||
return this
|
||||
}
|
||||
|
||||
val TestKpmModule.common: TestKpmFragment get() = fragmentNamed("common")
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package org.jetbrains.kotlin.project.model.testDsl
|
||||
|
||||
import org.jetbrains.kotlin.project.model.KotlinNativeTargetAttribute
|
||||
import org.jetbrains.kotlin.project.model.KotlinPlatformTypeAttribute
|
||||
import org.jetbrains.kotlin.project.model.infra.TestKpmModule
|
||||
import org.jetbrains.kotlin.project.model.infra.TestKpmVariant
|
||||
|
||||
fun TestKpmModule.jvm(configure: TestKpmVariant.() -> Unit = { }) =
|
||||
variant("jvm", KotlinPlatformTypeAttribute.JVM, configure = configure)
|
||||
|
||||
fun TestKpmModule.js(configure: TestKpmVariant.() -> Unit = { }) =
|
||||
variant("jvm", KotlinPlatformTypeAttribute.JS, configure = configure)
|
||||
|
||||
fun TestKpmModule.android(configure: TestKpmVariant.() -> Unit = { }) =
|
||||
variant("android", KotlinPlatformTypeAttribute.ANDROID_JVM, configure = configure)
|
||||
|
||||
fun TestKpmModule.nativeVariant(name: String, nativeTarget: String, configure: TestKpmVariant.() -> Unit = { }): TestKpmVariant {
|
||||
val variant = variant(name, KotlinPlatformTypeAttribute.NATIVE)
|
||||
variant.variantAttributes[KotlinNativeTargetAttribute] = nativeTarget
|
||||
variant.configure()
|
||||
return variant
|
||||
}
|
||||
|
||||
fun TestKpmModule.linux(configure: TestKpmVariant.() -> Unit = { }) = nativeVariant("linux", "linux", configure)
|
||||
fun TestKpmModule.macosX64(configure: TestKpmVariant.() -> Unit = { }) = nativeVariant("macosX64", "macosX64", configure)
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.project.model.utils
|
||||
|
||||
import org.jetbrains.kotlin.project.model.infra.KpmTestEntity
|
||||
|
||||
class ObservableIndexedCollection<T : KpmTestEntity> private constructor(
|
||||
private val _items: MutableMap<String, T>
|
||||
) : Collection<T> by _items.values {
|
||||
constructor() : this(mutableMapOf())
|
||||
|
||||
private val allItemsActions = mutableListOf<T.() -> Unit>()
|
||||
|
||||
fun add(item: T) {
|
||||
_items[item.name] = item
|
||||
allItemsActions.forEach { action -> action(item) }
|
||||
}
|
||||
|
||||
fun withAll(action: T.() -> Unit) {
|
||||
_items.values.forEach(action)
|
||||
allItemsActions.add(action)
|
||||
}
|
||||
|
||||
fun getOrPut(name: String, defaultValue: () -> T): T =
|
||||
if (!_items.contains(name)) defaultValue().also { add(it) } else _items[name]!!
|
||||
|
||||
operator fun get(name: String): T? = _items[name]
|
||||
}
|
||||
Reference in New Issue
Block a user