diff --git a/libraries/tools/kotlin-project-model/build.gradle.kts b/libraries/tools/kotlin-project-model/build.gradle.kts index 6c995874ff2..08e59a976e8 100644 --- a/libraries/tools/kotlin-project-model/build.gradle.kts +++ b/libraries/tools/kotlin-project-model/build.gradle.kts @@ -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().configureEach { @@ -23,6 +28,16 @@ tasks.withType().configureEach { } } +tasks.named("compileTestFixturesKotlin") { + kotlinOptions { + freeCompilerArgs += listOf( + "-XXLanguage:+AllowSealedInheritorsInDifferentFilesOfSamePackage", + "-XXLanguage:+SealedInterfaces", + "-Xjvm-default=all" + ) + } +} + tasks.named("jar") { callGroovy("manifestAttributes", manifest, project) } diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/coreCases/KpmTestCaseDescriptor.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/coreCases/KpmTestCaseDescriptor.kt new file mode 100644 index 00000000000..d1560f7f48a --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/coreCases/KpmTestCaseDescriptor.kt @@ -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 by lazy { + doGetAllCases() + } + + val allCasesNames: Set + get() = allCaseDescriptorsByNames.keys + + private fun doGetAllCases(): Map = 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() } + diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/KpmCoreCasesJunitParameterResolver.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/KpmCoreCasesJunitParameterResolver.kt new file mode 100644 index 00000000000..da1b03d2c04 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/KpmCoreCasesJunitParameterResolver.kt @@ -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 + } diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/KpmCoreCasesTestRunner.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/KpmCoreCasesTestRunner.kt new file mode 100644 index 00000000000..75cc201110d --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/KpmCoreCasesTestRunner.kt @@ -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( + """ + Test runner '${testRunnerClass.canonicalName}' + has some KPM Core Test Cases uncovered: + + ${uncoveredCases.joinToString()} + """.trimIndent() + "\n\n" + fixSuggestion(uncoveredCases) + ) + } + } +} + +private fun fixSuggestion(missingCases: Set): 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) diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/TestModel.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/TestModel.kt new file mode 100644 index 00000000000..12f7621e4e6 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/TestModel.kt @@ -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 = ObservableIndexedCollection() + val extras: MutableExtras = mutableExtrasOf() + + override fun toString(): String = "Case $name" +} + +class TestKpmModuleContainer( + val containingCase: KpmTestCase, + override val name: String, +) : KpmTestEntity { + val modules: ObservableIndexedCollection = 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 = ObservableIndexedCollection() + override val plugins: MutableSet = 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 = mutableListOf() + override val declaredModuleDependencies: MutableList = mutableListOf() + override val declaredRefinesDependencies: MutableList = 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 = mutableMapOf() +} diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/generate/KpmCoreCaseTestMethodGenerator.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/generate/KpmCoreCaseTestMethodGenerator.kt new file mode 100644 index 00000000000..bafd9420199 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/generate/KpmCoreCaseTestMethodGenerator.kt @@ -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() { + 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\")));") + } +} diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/generate/KpmCoreCaseTestMethodModel.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/generate/KpmCoreCaseTestMethodModel.kt new file mode 100644 index 00000000000..4092ae1145f --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/generate/KpmCoreCaseTestMethodModel.kt @@ -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 + get() = emptyList() + + override val kind: MethodModel.Kind + get() = Kind +} diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/generate/KpmCoreCasesTestClassModel.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/generate/KpmCoreCasesTestClassModel.kt new file mode 100644 index 00000000000..dbe74547ab2 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/generate/KpmCoreCasesTestClassModel.kt @@ -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 + get() = emptyList() + + override val innerTestClasses: Collection + get() = emptyList() + + override val methods: Collection by lazy { + doCollectMethods() + } + + override val isEmpty: Boolean + get() = methods.isEmpty() + + override val imports: Set> + 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 + get() = emptyList() // Don't need to annotate with `@ExtendWith`, because we inherit [KpmCoreCasesTestRunner] + + private fun doCollectMethods(): Collection { + val allCoreCasesNames: Set = KpmCoreCase.allCasesNames + val allAdditionalTestdata: Set = 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 + } +} diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/renderTestCase.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/renderTestCase.kt new file mode 100644 index 00000000000..b245eb6fb31 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/renderTestCase.kt @@ -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\")"}") + } + } + } +} diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/sources.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/sources.kt new file mode 100644 index 00000000000..fb3b3d2025a --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/infra/sources.kt @@ -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): 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() diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/dependencies.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/dependencies.kt new file mode 100644 index 00000000000..fad6fd8d8df --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/dependencies.kt @@ -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 diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/kpmTestCaseDsl.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/kpmTestCaseDsl.kt new file mode 100644 index 00000000000..25dd6c29270 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/kpmTestCaseDsl.kt @@ -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) + } + } +} diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/testKpmFragmentDsl.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/testKpmFragmentDsl.kt new file mode 100644 index 00000000000..c0782a6fb0d --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/testKpmFragmentDsl.kt @@ -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) } +} diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/testKpmGradleProjectDsl.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/testKpmGradleProjectDsl.kt new file mode 100644 index 00000000000..f9c9678a054 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/testKpmGradleProjectDsl.kt @@ -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 +} diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/testKpmModuleDsl.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/testKpmModuleDsl.kt new file mode 100644 index 00000000000..0befff5fa72 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/testKpmModuleDsl.kt @@ -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") diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/variantsShortcuts.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/variantsShortcuts.kt new file mode 100644 index 00000000000..fb5c884d34f --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/testDsl/variantsShortcuts.kt @@ -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) diff --git a/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/utils/ObservableIndexedCollection.kt b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/utils/ObservableIndexedCollection.kt new file mode 100644 index 00000000000..edb3445fad0 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/testFixtures/kotlin/org/jetbrains/kotlin/project/model/utils/ObservableIndexedCollection.kt @@ -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 private constructor( + private val _items: MutableMap +) : Collection by _items.values { + constructor() : this(mutableMapOf()) + + private val allItemsActions = mutableListOf 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] +}