diff options
| author | Ignat Beresnev <ignat.beresnev@jetbrains.com> | 2023-10-27 13:11:41 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-10-27 13:11:41 +0200 |
| commit | edcd1fb24d01e11b5a8185328255f2005aadf037 (patch) | |
| tree | 8156df7d2d29d8fd9d0fdaccad0fbb92b26e895f /subprojects/analysis-kotlin-api/src/testFixtures | |
| parent | b1ccc2b346ea858762653933f9dd304b91c18505 (diff) | |
| download | dokka-edcd1fb24d01e11b5a8185328255f2005aadf037.tar.gz dokka-edcd1fb24d01e11b5a8185328255f2005aadf037.tar.bz2 dokka-edcd1fb24d01e11b5a8185328255f2005aadf037.zip | |
Implement analysis test API (#3184)
Diffstat (limited to 'subprojects/analysis-kotlin-api/src/testFixtures')
33 files changed, 1973 insertions, 0 deletions
diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestData.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestData.kt new file mode 100644 index 00000000..64bfd7a3 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestData.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api + +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker + +/** + * Represents some sort of data of a [TestProject], which normally consists of a number of [TestDataFile]. + * + * This can be anything that can usually be found in a user-defined project: + * programming language source code, markdown files with documentation, samples, etc. + * + * This virtual test data will be materialized and created physically before running Dokka, + * and then passed as input files into it. + */ +@AnalysisTestDslMarker +interface TestData { + fun getFiles(): List<TestDataFile> +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestDataFile.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestDataFile.kt new file mode 100644 index 00000000..5b2233ba --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestDataFile.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api + +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker + +/** + * Represents a single file of a project's [TestData]. + * + * This file will be materialized and created physically before running Dokka, + * and then passed as one of the input files into it. + * + * @property pathFromProjectRoot this file's path from the root of the project. Must begin + * with `/` to not confuse it with relative paths. + */ +@AnalysisTestDslMarker +abstract class TestDataFile(val pathFromProjectRoot: String) { + + init { + require(pathFromProjectRoot.startsWith("/")) { + "File path going from the project's root must begin with \"/\" to not confuse it with relative paths." + } + } + + /** + * Returns the string contents of this file. + * + * The contents must be complete, as if the user themselves wrote it. For Kotlin files, + * it should return Kotlin source code (including the package and all import statements). + * For `.md` files, it should return valid Markdown documentation. + * + * These contents will be used to populate the real input file to be used by Dokka. + */ + abstract fun getContents(): String +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProject.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProject.kt new file mode 100644 index 00000000..9c0fa936 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProject.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.analysis.TestAnalysisContext +import org.jetbrains.dokka.analysis.test.api.analysis.TestAnalysisServices +import org.jetbrains.dokka.analysis.test.api.analysis.TestProjectAnalyzer +import org.jetbrains.dokka.analysis.test.api.configuration.BaseTestDokkaConfigurationBuilder +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.util.withTempDirectory +import org.jetbrains.dokka.model.DModule + +/** + * Represents a virtual test project (as if it's user-defined) that will be used to run Dokka. + * + * A test project consists of some Dokka configuration (represented as [TestDokkaConfiguration]) + * and some project-specific data like source code and markdown files (represented as [TestData]). + * + * See [kotlinJvmTestProject], [javaTestProject] and [mixedJvmTestProject] for convenient ways + * of bootstrapping test projects. + * + * See [parse] and [useServices] functions to learn how to run Dokka with this project as input. + */ +interface TestProject { + + /** + * Verifies that this project is valid from the user's and Dokka's perspectives. + * Exists to save time with debugging difficult to catch mistakes, such as copy-pasted + * test data that is not applicable to this project. + * + * Must throw an exception if there's misconfiguration, incorrect / corrupted test data + * or API misuse. + * + * Verification is performed before running Dokka on this project. + */ + fun verify() + + /** + * Returns the configuration of this project, which will then be mapped to [DokkaConfiguration]. + * + * This is typically constructed using [BaseTestDokkaConfigurationBuilder]. + */ + fun getConfiguration(): TestDokkaConfiguration + + /** + * Returns this project's test data - a collection of source code files, markdown files + * and whatever else that can be usually found in a user-defined project. + */ + fun getTestData(): TestData +} + +/** + * Runs Dokka on the given [TestProject] and returns the generated documentable model. + * + * Can be used to verify the resulting documentable model, to check that + * everything was parsed and converted correctly. + * + * Usage example: + * ```kotlin + * val testProject = kotlinJvmTestProject { + * ... + * } + * + * val module: DModule = testProject.parse() + * ``` + */ +fun TestProject.parse(): DModule = TestProjectAnalyzer.parse(this) + +/** + * Runs Dokka on the given [TestProject] and provides not only the resulting documentable model, + * but analysis context and configuration as well, which gives you the ability to call public + * analysis services. + * + * Usage example: + * + * ```kotlin + * val testProject = kotlinJvmTestProject { + * ... + * } + * + * testProject.useServices { context -> + * val pckg: DPackage = context.module.packages.single() + * + * // use `moduleAndPackageDocumentationReader` service to get documentation of a package + * val allPackageDocs: SourceSetDependent<DocumentationNode> = moduleAndPackageDocumentationReader.read(pckg) + * } + * ``` + */ +fun TestProject.useServices(block: TestAnalysisServices.(context: TestAnalysisContext) -> Unit) { + withTempDirectory { tempDirectory -> + val (services, context) = TestProjectAnalyzer.analyze(this, tempDirectory) + services.block(context) + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProjectFactory.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProjectFactory.kt new file mode 100644 index 00000000..81a20243 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProjectFactory.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api + +import org.jetbrains.dokka.analysis.test.api.jvm.java.JavaTestProject +import org.jetbrains.dokka.analysis.test.api.jvm.kotlin.KotlinJvmTestProject +import org.jetbrains.dokka.analysis.test.api.jvm.mixed.MixedJvmTestProject +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker + +/** + * Creates a single-target Kotlin/JVM test project that only has Kotlin source code. + * + * See [javaTestProject] and [mixedJvmTestProject] if you want to check interoperability + * with other JVM languages. + * + * By default, the sources are put in `/src/main/kotlin`, and the JVM version of Kotlin's + * standard library is available on classpath. + * + * See [parse] and [useServices] functions to learn how to run Dokka with this project as input. + * + * @sample org.jetbrains.dokka.analysis.test.jvm.kotlin.SampleKotlinJvmAnalysisTest.sample + */ +fun kotlinJvmTestProject(init: (@AnalysisTestDslMarker KotlinJvmTestProject).() -> Unit): TestProject { + val testData = KotlinJvmTestProject() + testData.init() + return testData +} + +/** + * Creates a Java-only test project. + * + * This can be used to test Dokka's Java support or specific + * corner cases related to parsing Java sources. + * + * By default, the sources are put in `/src/main/java`. No Kotlin source code is allowed. + * + * See [parse] and [useServices] functions to learn how to run Dokka with this project as input. + * + * @sample org.jetbrains.dokka.analysis.test.jvm.java.SampleJavaAnalysisTest.sample + */ +fun javaTestProject(init: (@AnalysisTestDslMarker JavaTestProject).() -> Unit): TestProject { + val testData = JavaTestProject() + testData.init() + return testData +} + +/** + * Creates a project where a number of JVM language sources are allowed, + * like Java and Kotlin sources co-existing in the same source directory. + * + * This can be used to test interoperability between JVM languages. + * + * By default, this project consists of a single "jvm" source set, which has two source root directories: + * * `/src/main/kotlin` + * * `/src/main/java` + * + * See [parse] and [useServices] functions to learn how to run Dokka with this project as input. + * + * @sample org.jetbrains.dokka.analysis.test.jvm.mixed.SampleMixedJvmAnalysisTest.sample + */ +fun mixedJvmTestProject(init: (@AnalysisTestDslMarker MixedJvmTestProject).() -> Unit): TestProject { + val testProject = MixedJvmTestProject() + testProject.init() + return testProject +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisContext.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisContext.kt new file mode 100644 index 00000000..de6efb1b --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisContext.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.analysis + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.TestProject +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaConfiguration +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.plugability.DokkaContext + +/** + * Context and data gathered during the analysis of a [TestProject]. + */ +class TestAnalysisContext( + + /** + * The actual [DokkaContext] that was used to run Dokka. + * + * Includes all plugins and classes available on classpath during the analysis. + */ + val context: DokkaContext, + + /** + * The actual [DokkaConfiguration] that was used to run Dokka. + * + * It was initially mapped from [TestDokkaConfiguration], and then added to by Dokka itself. + */ + val configuration: DokkaConfiguration, + + /** + * The entry point to the documentable model of the analyzed [TestProject]. + */ + val module: DModule +) diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisServices.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisServices.kt new file mode 100644 index 00000000..ab70bbd4 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisServices.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.analysis + +import org.jetbrains.dokka.analysis.kotlin.KotlinAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.internal.ModuleAndPackageDocumentationReader +import org.jetbrains.dokka.analysis.kotlin.internal.SampleProviderFactory + +/** + * Services exposed in [KotlinAnalysisPlugin] that are ready to be used. + * + * This class exists purely for convenience and to reduce boilerplate in tests. + * It is analogous to calling `context.plugin<KotlinAnalysisPlugin>().querySingle { serviceName }`. + */ +class TestAnalysisServices( + val sampleProviderFactory: SampleProviderFactory, + val moduleAndPackageDocumentationReader: ModuleAndPackageDocumentationReader +) diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestProjectAnalyzer.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestProjectAnalyzer.kt new file mode 100644 index 00000000..1668b53f --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestProjectAnalyzer.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.analysis + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin +import org.jetbrains.dokka.analysis.test.api.TestDataFile +import org.jetbrains.dokka.analysis.test.api.TestProject +import org.jetbrains.dokka.analysis.test.api.configuration.toDokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.parse +import org.jetbrains.dokka.analysis.test.api.useServices +import org.jetbrains.dokka.analysis.test.api.util.withTempDirectory +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.transformers.documentation.DefaultDocumentableMerger +import org.jetbrains.dokka.transformers.documentation.DocumentableMerger +import org.jetbrains.dokka.transformers.sources.SourceToDocumentableTranslator +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import org.jetbrains.dokka.utilities.LoggingLevel +import java.io.File + +/** + * The main logger used for running Dokka and analyzing projects. + * + * Changing the level to [LoggingLevel.DEBUG] can help with debugging faulty tests + * or tricky corner cases. + */ +val analysisLogger = DokkaConsoleLogger(minLevel = LoggingLevel.INFO) + +/** + * Analyzer of the test projects, it is essentially a very simple Dokka runner. + * + * Takes all virtual files of the given [TestProject], creates the real files for + * them in a temporary directory, and then runs Dokka with this temporary directory + * as the input user project. This allows us to simulate Dokka's behavior and results + * on a made-up project as if it were real and run via the CLI runner. + * + * Executes only a limited number of steps and uses a small subset of [CoreExtensions] + * that are necessary to test the analysis logic. + * + * Works only with single-module projects, where the source code of this project + * resides in the root `src` directory. Works with multiple source sets and targets, + * so both simple Kotlin/JVM and more complicated Kotlin Multiplatform project must work. + */ +object TestProjectAnalyzer { + + /** + * A quick way to analyze a [TestProject], for cases when only the documentable + * model is needed to verify the result. + * + * Creates the input test files, runs Dokka and then deletes them right after the documentable + * model has been created, leaving no trailing files or any other garbage behind. + * + * @see [TestProject.parse] for a user-friendly way to call it + */ + fun parse(testProject: TestProject): DModule { + // since we only need documentables, we can delete the input test files right away + return withTempDirectory(analysisLogger) { tempDirectory -> + val (_, context) = testProject.initialize(outputDirectory = tempDirectory) + generateDocumentableModel(context) + } + } + + /** + * Works in the same way as [parse], but it returns the context and configuration used for + * running Dokka, and does not delete the input test files at the end of the execution - it + * must be taken care of on call site. + * + * @param persistentDirectory a directory that will be used to generate the input test files into. + * It must be available during the test run, especially if services are used, + * otherwise parts of Dokka might not work as expected. Can be safely deleted + * at the end of the test after all asserts have been run. + * + * @see [TestProject.useServices] for a user-friendly way to call it + */ + fun analyze( + testProject: TestProject, + persistentDirectory: File + ): Pair<TestAnalysisServices, TestAnalysisContext> { + val (dokkaConfiguration, dokkaContext) = testProject.initialize(outputDirectory = persistentDirectory) + val analysisServices = createTestAnalysisServices(dokkaContext) + val testAnalysisContext = TestAnalysisContext( + context = dokkaContext, + configuration = dokkaConfiguration, + module = generateDocumentableModel(dokkaContext) + ) + return analysisServices to testAnalysisContext + } + + /** + * Prepares this [TestProject] for analysis by creating + * the test files, setting up context and configuration. + */ + private fun TestProject.initialize(outputDirectory: File): Pair<DokkaConfiguration, DokkaContext> { + analysisLogger.progress("Initializing and verifying project $this") + this.verify() + require(outputDirectory.isDirectory) { + "outputDirectory has to exist and be a directory: $outputDirectory" + } + this.initializeTestFiles(relativeToDir = outputDirectory) + + analysisLogger.progress("Creating configuration and context") + val testDokkaConfiguration = this.getConfiguration() + val dokkaConfiguration = testDokkaConfiguration.toDokkaConfiguration(projectDir = outputDirectory).also { + it.verify() + } + return dokkaConfiguration to createContext(dokkaConfiguration) + } + + /** + * Takes the virtual [TestDataFile] of this [TestProject] and creates + * the real files relative to the [relativeToDir] param. + */ + private fun TestProject.initializeTestFiles(relativeToDir: File) { + analysisLogger.progress("Initializing test files relative to the \"$relativeToDir\" directory") + + this.getTestData().getFiles().forEach { + val testDataFile = relativeToDir.resolve(it.pathFromProjectRoot.removePrefix("/")) + try { + testDataFile.parentFile.mkdirs() + } catch (e: Exception) { + // the IOException thrown from `mkdirs()` has no details and thus is more difficult to debug. + throw IllegalStateException("Unable to create dirs \"${testDataFile.parentFile}\"", e) + } + + analysisLogger.debug("Creating \"${testDataFile.absolutePath}\"") + check(testDataFile.createNewFile()) { + "Unable to create a test file: ${testDataFile.absolutePath}" + } + testDataFile.writeText(it.getContents(), Charsets.UTF_8) + } + } + + /** + * Verifies this [DokkaConfiguration] to make sure there are no unexpected + * parameter option values, such as non-existing classpath entries. + * + * If this method fails, it's likely there's a configuration error in the test, + * or an exception must be made in one of the checks. + */ + private fun DokkaConfiguration.verify() { + this.includes.forEach { verifyFileExists(it) } + this.sourceSets.forEach { sourceSet -> + sourceSet.classpath.forEach { verifyFileExists(it) } + sourceSet.includes.forEach { verifyFileExists(it) } + sourceSet.samples.forEach { verifyFileExists(it) } + // we do not verify sourceRoots since the source directory + // is not guaranteed to exist even if it was configured. + } + } + + private fun verifyFileExists(file: File) { + if (!file.exists() && !file.absolutePath.contains("non-existing")) { + throw IllegalArgumentException( + "The provided file does not exist. Bad test data or configuration? " + + "If it is done intentionally, add \"non-existing\" to the path or the name. File: \"$file\"" + ) + } + } + + private fun createContext(dokkaConfiguration: DokkaConfiguration): DokkaContext { + analysisLogger.progress("Creating DokkaContext from test configuration") + return DokkaContext.create( + configuration = dokkaConfiguration, + logger = analysisLogger, + pluginOverrides = listOf() + ) + } + + /** + * Generates the documentable model by using all available [SourceToDocumentableTranslator] extensions, + * and then merging all the results into a single [DModule] by calling [DocumentableMerger]. + */ + private fun generateDocumentableModel(context: DokkaContext): DModule { + analysisLogger.progress("Generating the documentable model") + val sourceSetModules = context + .configuration + .sourceSets + .map { sourceSet -> translateSources(sourceSet, context) } + .flatten() + + if (sourceSetModules.isEmpty()) { + throw IllegalStateException("Got no modules after translating sources. Is the test data set up?") + } + + return DefaultDocumentableMerger(context).invoke(sourceSetModules) + ?: error("Unable to merge documentables for some reason") + } + + /** + * Translates input source files to the documentable model by using + * all registered [SourceToDocumentableTranslator] core extensions. + */ + private fun translateSources(sourceSet: DokkaConfiguration.DokkaSourceSet, context: DokkaContext): List<DModule> { + val translators = context[CoreExtensions.sourceToDocumentableTranslator] + require(translators.isNotEmpty()) { + "Need at least one source to documentable translator to run tests, otherwise no data will be generated." + } + analysisLogger.debug("Translating sources for ${sourceSet.sourceSetID}") + return translators.map { it.invoke(sourceSet, context) } + } + + /** + * A helper function to query analysis services, to avoid + * boilerplate and misconfiguration in the tests. + * + * The idea is to provide the users with ready-to-use services, + * without them having to know how to query or configure them. + */ + private fun createTestAnalysisServices(context: DokkaContext): TestAnalysisServices { + analysisLogger.progress("Creating analysis services") + val internalPlugin = context.plugin<InternalKotlinAnalysisPlugin>() + return TestAnalysisServices( + sampleProviderFactory = internalPlugin.querySingle { sampleProviderFactory }, + moduleAndPackageDocumentationReader = internalPlugin.querySingle { moduleAndPackageDocumentationReader } + ) + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfiguration.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfiguration.kt new file mode 100644 index 00000000..5c5a0daf --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfiguration.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2014-2023 JetBrai |
