[mpp, tests] Introduce KpmCoreCases infrastructure

This commit is contained in:
Dmitry Savvinov
2022-08-10 14:42:29 +02:00
parent 3bb70f55d7
commit a2c9864adc
17 changed files with 902 additions and 1 deletions
@@ -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)
}
@@ -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() }
@@ -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
}
@@ -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)
@@ -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()
}
@@ -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\")));")
}
}
@@ -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
}
@@ -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
}
}
@@ -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\")"}")
}
}
}
}
@@ -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()
@@ -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
@@ -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)
}
}
}
@@ -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) }
}
@@ -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
}
@@ -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")
@@ -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)
@@ -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]
}