diff options
Diffstat (limited to 'dokka-subprojects/core-test-api/src')
4 files changed, 565 insertions, 0 deletions
diff --git a/dokka-subprojects/core-test-api/src/main/kotlin/org/jetbrains/dokka/testApi/context/MockContext.kt b/dokka-subprojects/core-test-api/src/main/kotlin/org/jetbrains/dokka/testApi/context/MockContext.kt new file mode 100644 index 00000000..06ea2dad --- /dev/null +++ b/dokka-subprojects/core-test-api/src/main/kotlin/org/jetbrains/dokka/testApi/context/MockContext.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.testApi.context + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.plugability.ExtensionPoint +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.dokka.utilities.LoggingLevel +import kotlin.reflect.KClass +import kotlin.reflect.KMutableProperty +import kotlin.reflect.full.memberProperties + +@Suppress("UNCHECKED_CAST") // It is only usable from tests so we do not care about safety +public class MockContext( + vararg extensions: Pair<ExtensionPoint<*>, (DokkaContext) -> Any>, + private val testConfiguration: DokkaConfiguration? = null, + private val unusedExtensionPoints: List<ExtensionPoint<*>>? = null +) : DokkaContext { + private val extensionMap by lazy { + extensions.groupBy(Pair<ExtensionPoint<*>, (DokkaContext) -> Any>::first) { + it.second(this) + } + } + + private val plugins = mutableMapOf<KClass<out DokkaPlugin>, DokkaPlugin>() + + override fun <T : DokkaPlugin> plugin(kclass: KClass<T>): T = plugins.getOrPut(kclass) { + kclass.constructors.single { it.parameters.isEmpty() }.call().also { it.injectContext(this) } + } as T + + override fun <T : Any, E : ExtensionPoint<T>> get(point: E): List<T> = extensionMap[point].orEmpty() as List<T> + + override fun <T : Any, E : ExtensionPoint<T>> single(point: E): T = get(point).single() + + override val logger: DokkaLogger = DokkaConsoleLogger(LoggingLevel.DEBUG) + + override val configuration: DokkaConfiguration + get() = testConfiguration ?: throw IllegalStateException("This mock context doesn't provide configuration") + + override val unusedPoints: Collection<ExtensionPoint<*>> + get() = unusedExtensionPoints + ?: throw IllegalStateException("This mock context doesn't provide unused extension points") +} + +private fun DokkaPlugin.injectContext(context: DokkaContext) { + (DokkaPlugin::class.memberProperties.single { it.name == "context" } as KMutableProperty<*>) + .setter.call(this, context) +} diff --git a/dokka-subprojects/core-test-api/src/main/kotlin/org/jetbrains/dokka/testApi/logger/TestLogger.kt b/dokka-subprojects/core-test-api/src/main/kotlin/org/jetbrains/dokka/testApi/logger/TestLogger.kt new file mode 100644 index 00000000..c285a663 --- /dev/null +++ b/dokka-subprojects/core-test-api/src/main/kotlin/org/jetbrains/dokka/testApi/logger/TestLogger.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.testApi.logger + +import org.jetbrains.dokka.utilities.DokkaLogger +import java.util.* + +/* + * Even in tests it be used in a concurrent environment, so needs to be thread safe + */ +public class TestLogger(private val logger: DokkaLogger) : DokkaLogger { + override var warningsCount: Int by logger::warningsCount + override var errorsCount: Int by logger::errorsCount + + private var _debugMessages = synchronizedMutableListOf<String>() + public val debugMessages: List<String> get() = _debugMessages.toList() + + private var _infoMessages = synchronizedMutableListOf<String>() + public val infoMessages: List<String> get() = _infoMessages.toList() + + private var _progressMessages = synchronizedMutableListOf<String>() + public val progressMessages: List<String> get() = _progressMessages.toList() + + private var _warnMessages = synchronizedMutableListOf<String>() + public val warnMessages: List<String> get() = _warnMessages.toList() + + private var _errorMessages = synchronizedMutableListOf<String>() + public val errorMessages: List<String> get() = _errorMessages.toList() + + override fun debug(message: String) { + _debugMessages.add(message) + logger.debug(message) + } + + override fun info(message: String) { + _infoMessages.add(message) + logger.info(message) + } + + override fun progress(message: String) { + _progressMessages.add(message) + logger.progress(message) + } + + override fun warn(message: String) { + _warnMessages.add(message) + logger.warn(message) + } + + override fun error(message: String) { + _errorMessages.add(message) + logger.error(message) + } + + private fun <T> synchronizedMutableListOf(): MutableList<T> = Collections.synchronizedList(mutableListOf()) +} diff --git a/dokka-subprojects/core-test-api/src/main/kotlin/org/jetbrains/dokka/testApi/testRunner/TestDokkaConfigurationBuilder.kt b/dokka-subprojects/core-test-api/src/main/kotlin/org/jetbrains/dokka/testApi/testRunner/TestDokkaConfigurationBuilder.kt new file mode 100644 index 00000000..4c451fed --- /dev/null +++ b/dokka-subprojects/core-test-api/src/main/kotlin/org/jetbrains/dokka/testApi/testRunner/TestDokkaConfigurationBuilder.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package testApi.testRunner + +import org.jetbrains.dokka.* +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.doc.CustomDocTag +import org.jetbrains.dokka.model.doc.Description +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.model.doc.Text +import org.jetbrains.dokka.model.properties.PropertyContainer +import java.io.File + +public fun dokkaConfiguration(block: TestDokkaConfigurationBuilder.() -> Unit): DokkaConfigurationImpl = + TestDokkaConfigurationBuilder().apply(block).build() + +@DslMarker +public annotation class DokkaConfigurationDsl + +// TODO this class heavily relies on `DokkaSourceSetImpl`, should be refactored to `DokkaSourceSet` +@DokkaConfigurationDsl +public class TestDokkaConfigurationBuilder { + + public var moduleName: String = "root" + set(value) { + check(lazySourceSets.isEmpty()) { "Cannot set moduleName after adding source sets" } + field = value + } + public var moduleVersion: String = "1.0-SNAPSHOT" + public var outputDir: File = File("out") + public var format: String = "html" + public var offlineMode: Boolean = false + public var cacheRoot: String? = null + public var pluginsClasspath: List<File> = emptyList() + public var pluginsConfigurations: MutableList<PluginConfigurationImpl> = mutableListOf() + public var failOnWarning: Boolean = false + public var modules: List<DokkaModuleDescriptionImpl> = emptyList() + public var suppressObviousFunctions: Boolean = DokkaDefaults.suppressObviousFunctions + public var includes: List<File> = emptyList() + public var suppressInheritedMembers: Boolean = DokkaDefaults.suppressInheritedMembers + public var delayTemplateSubstitution: Boolean = DokkaDefaults.delayTemplateSubstitution + private val lazySourceSets = mutableListOf<Lazy<DokkaSourceSetImpl>>() + + public fun build(): DokkaConfigurationImpl = DokkaConfigurationImpl( + moduleName = moduleName, + moduleVersion = moduleVersion, + outputDir = outputDir, + cacheRoot = cacheRoot?.let(::File), + offlineMode = offlineMode, + sourceSets = lazySourceSets.map { it.value }.toList(), + pluginsClasspath = pluginsClasspath, + pluginsConfiguration = pluginsConfigurations, + modules = modules, + failOnWarning = failOnWarning, + suppressObviousFunctions = suppressObviousFunctions, + includes = includes.toSet(), + suppressInheritedMembers = suppressInheritedMembers, + delayTemplateSubstitution = delayTemplateSubstitution, + finalizeCoroutines = false + ) + + public fun sourceSets(block: SourceSetsBuilder.() -> Unit) { + lazySourceSets.addAll(SourceSetsBuilder(moduleName).apply(block)) + } + + public fun sourceSet(block: DokkaSourceSetBuilder.() -> Unit): Lazy<DokkaSourceSetImpl> { + val lazySourceSet = lazy { DokkaSourceSetBuilder(moduleName).apply(block).build() } + lazySourceSets.add(lazySourceSet) + return lazySourceSet + } + + public fun unattachedSourceSet(block: DokkaSourceSetBuilder.() -> Unit): DokkaSourceSetImpl { + return DokkaSourceSetBuilder(moduleName).apply(block).build() + } +} + +@DokkaConfigurationDsl +public class SourceSetsBuilder( + public val moduleName: String +) : ArrayList<Lazy<DokkaSourceSetImpl>>() { + public fun sourceSet(block: DokkaSourceSetBuilder.() -> Unit): Lazy<DokkaConfiguration.DokkaSourceSet> { + return lazy { DokkaSourceSetBuilder(moduleName).apply(block).build() }.apply(::add) + } +} + +@DokkaConfigurationDsl +public class DokkaSourceSetBuilder( + private val moduleName: String, + public var name: String = "main", + public var displayName: String = "JVM", + public var classpath: List<String> = emptyList(), + public var sourceRoots: List<String> = emptyList(), + public var dependentSourceSets: Set<DokkaSourceSetID> = emptySet(), + public var samples: List<String> = emptyList(), + public var includes: List<String> = emptyList(), + @Deprecated(message = "Use [documentedVisibilities] property for a more flexible control over documented visibilities") + public var includeNonPublic: Boolean = false, + public var documentedVisibilities: Set<DokkaConfiguration.Visibility> = DokkaDefaults.documentedVisibilities, + public var reportUndocumented: Boolean = false, + public var skipEmptyPackages: Boolean = false, + public var skipDeprecated: Boolean = false, + public var jdkVersion: Int = 8, + public var languageVersion: String? = null, + public var apiVersion: String? = null, + public var noStdlibLink: Boolean = false, + public var noJdkLink: Boolean = false, + public var suppressedFiles: List<String> = emptyList(), + public var analysisPlatform: String = "jvm", + public var perPackageOptions: List<PackageOptionsImpl> = emptyList(), + public var externalDocumentationLinks: List<ExternalDocumentationLinkImpl> = emptyList(), + public var sourceLinks: List<SourceLinkDefinitionImpl> = emptyList() +) { + @Suppress("DEPRECATION") + public fun build(): DokkaSourceSetImpl { + return DokkaSourceSetImpl( + displayName = displayName, + sourceSetID = DokkaSourceSetID(moduleName, name), + classpath = classpath.map(::File), + sourceRoots = sourceRoots.map(::File).toSet(), + dependentSourceSets = dependentSourceSets, + samples = samples.map(::File).toSet(), + includes = includes.map(::File).toSet(), + includeNonPublic = includeNonPublic, + documentedVisibilities = documentedVisibilities, + reportUndocumented = reportUndocumented, + skipEmptyPackages = skipEmptyPackages, + skipDeprecated = skipDeprecated, + jdkVersion = jdkVersion, + sourceLinks = sourceLinks.toSet(), + perPackageOptions = perPackageOptions.toList(), + externalDocumentationLinks = externalDocumentationLinks.toSet(), + languageVersion = languageVersion, + apiVersion = apiVersion, + noStdlibLink = noStdlibLink, + noJdkLink = noJdkLink, + suppressedFiles = suppressedFiles.map(::File).toSet(), + analysisPlatform = Platform.fromString(analysisPlatform) + ) + } +} + +public val defaultSourceSet: DokkaSourceSetImpl = DokkaSourceSetImpl( + displayName = "DEFAULT", + sourceSetID = DokkaSourceSetID("DEFAULT", "DEFAULT"), + classpath = emptyList(), + sourceRoots = emptySet(), + dependentSourceSets = emptySet(), + samples = emptySet(), + includes = emptySet(), + includeNonPublic = false, + documentedVisibilities = DokkaDefaults.documentedVisibilities, + reportUndocumented = false, + skipEmptyPackages = true, + skipDeprecated = false, + jdkVersion = 8, + sourceLinks = emptySet(), + perPackageOptions = emptyList(), + externalDocumentationLinks = emptySet(), + languageVersion = null, + apiVersion = null, + noStdlibLink = false, + noJdkLink = false, + suppressedFiles = emptySet(), + analysisPlatform = Platform.DEFAULT +) + +public fun sourceSet(name: String): DokkaConfiguration.DokkaSourceSet { + return defaultSourceSet.copy( + displayName = name, + sourceSetID = defaultSourceSet.sourceSetID.copy(sourceSetName = name) + ) +} + +public fun dModule( + name: String, + packages: List<DPackage> = emptyList(), + documentation: SourceSetDependent<DocumentationNode> = emptyMap(), + expectPresentInSet: DokkaConfiguration.DokkaSourceSet? = null, + sourceSets: Set<DokkaConfiguration.DokkaSourceSet> = emptySet(), + extra: PropertyContainer<DModule> = PropertyContainer.empty() +): DModule = DModule( + name = name, + packages = packages, + documentation = documentation, + expectPresentInSet = expectPresentInSet, + sourceSets = sourceSets, + extra = extra +) + +public fun dPackage( + dri: DRI, + functions: List<DFunction> = emptyList(), + properties: List<DProperty> = emptyList(), + classlikes: List<DClasslike> = emptyList(), + typealiases: List<DTypeAlias> = emptyList(), + documentation: SourceSetDependent<DocumentationNode> = emptyMap(), + expectPresentInSet: DokkaConfiguration.DokkaSourceSet? = null, + sourceSets: Set<DokkaConfiguration.DokkaSourceSet> = emptySet(), + extra: PropertyContainer<DPackage> = PropertyContainer.empty() +): DPackage = DPackage( + dri = dri, + functions = functions, + properties = properties, + classlikes = classlikes, + typealiases = typealiases, + documentation = documentation, + expectPresentInSet = expectPresentInSet, + sourceSets = sourceSets, + extra = extra +) + +public fun documentationNode(vararg texts: String): DocumentationNode { + return DocumentationNode( + texts.toList() + .map { Description(CustomDocTag(listOf(Text(it)), name = "MARKDOWN_FILE")) }) +} diff --git a/dokka-subprojects/core-test-api/src/main/kotlin/org/jetbrains/dokka/testApi/testRunner/TestRunner.kt b/dokka-subprojects/core-test-api/src/main/kotlin/org/jetbrains/dokka/testApi/testRunner/TestRunner.kt new file mode 100644 index 00000000..1f7ee060 --- /dev/null +++ b/dokka-subprojects/core-test-api/src/main/kotlin/org/jetbrains/dokka/testApi/testRunner/TestRunner.kt @@ -0,0 +1,235 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.testApi.testRunner + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.ExternalDocumentationLinkImpl +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.testApi.logger.TestLogger +import org.jetbrains.dokka.utilities.DokkaLogger +import testApi.testRunner.TestDokkaConfigurationBuilder +import java.io.File +import java.net.URL +import java.nio.charset.Charset +import java.nio.file.Files +import java.nio.file.InvalidPathException +import java.nio.file.Path +import java.nio.file.Paths + +// TODO: take dokka configuration from file +public abstract class AbstractTest<M : TestMethods, T : TestBuilder<M>, D : DokkaTestGenerator<M>>( + protected val testBuilder: () -> T, + protected val dokkaTestGenerator: (DokkaConfiguration, DokkaLogger, M, List<DokkaPlugin>) -> D, + protected val logger: TestLogger, +) { + protected fun getTestDataDir(name: String): Path { + return File("src/test/resources/$name").takeIf { it.exists() }?.toPath() + ?: throw InvalidPathException(name, "Cannot be found") + } + + /** + * @param cleanupOutput if set to true, any temporary files will be cleaned up after execution. If set to false, + * it will be left to the user or the OS to delete it. Has no effect if [useOutputLocationFromConfig] + * is also set to true. + * @param useOutputLocationFromConfig if set to true, output location specified in [DokkaConfigurationImpl.outputDir] + * will be used. If set to false, a temporary folder will be used instead. + */ + protected fun testFromData( + configuration: DokkaConfigurationImpl, + cleanupOutput: Boolean = true, + useOutputLocationFromConfig: Boolean = false, + pluginOverrides: List<DokkaPlugin> = emptyList(), + block: T.() -> Unit, + ) { + if (useOutputLocationFromConfig) { + runTests( + configuration = configuration, + pluginOverrides = pluginOverrides, + testLogger = logger, + block = block + ) + } else { + withTempDirectory(cleanUpAfterUse = cleanupOutput) { tempDir -> + if (!cleanupOutput) { + logger.info("Output will be generated under: ${tempDir.absolutePath}") + } + runTests( + configuration = configuration.copy(outputDir = tempDir), + pluginOverrides = pluginOverrides, + testLogger = logger, + block = block + ) + } + } + } + + protected fun testInline( + query: String, + configuration: DokkaConfigurationImpl, + cleanupOutput: Boolean = true, + pluginOverrides: List<DokkaPlugin> = emptyList(), + loggerForTest: DokkaLogger = logger, + block: T.() -> Unit, + ) { + withTempDirectory(cleanUpAfterUse = cleanupOutput) { tempDir -> + if (!cleanupOutput) { + loggerForTest.info("Output will be generated under: ${tempDir.absolutePath}") + } + + val fileMap = query.toFileMap() + fileMap.materializeFiles(tempDir.toPath().toAbsolutePath()) + + val newConfiguration = configuration.copy( + outputDir = tempDir, + sourceSets = configuration.sourceSets.map { sourceSet -> + sourceSet.copy( + sourceRoots = sourceSet.sourceRoots.map { file -> tempDir.resolve(file) }.toSet(), + suppressedFiles = sourceSet.suppressedFiles.map { file -> tempDir.resolve(file) }.toSet(), + sourceLinks = sourceSet.sourceLinks.map { + link -> link.copy(localDirectory = tempDir.resolve(link.localDirectory).absolutePath) + }.toSet(), + includes = sourceSet.includes.map { file -> tempDir.resolve(file) }.toSet() + ) + } + ) + runTests( + configuration = newConfiguration, + pluginOverrides = pluginOverrides, + testLogger = loggerForTest, + block = block + ) + } + } + + private fun withTempDirectory(cleanUpAfterUse: Boolean, block: (tempDirectory: File) -> Unit) { + val tempDir = this.createTempDir() + try { + block(tempDir) + } finally { + if (cleanUpAfterUse) { + tempDir.deleteRecursively() + } + } + } + + private fun runTests( + configuration: DokkaConfiguration, + pluginOverrides: List<DokkaPlugin>, + testLogger: DokkaLogger = logger, + block: T.() -> Unit + ) { + val testMethods = testBuilder().apply(block).build() + dokkaTestGenerator( + configuration, + testLogger, + testMethods, + pluginOverrides + ).generate() + } + + private fun String.toFileMap(): Map<String, String> { + return this.trimIndent().trimMargin() + .replace("\r\n", "\n") + .sliceAt(filePathRegex) + .filter { it.isNotEmpty() && it.isNotBlank() && "\n" in it } + .map { fileDeclaration -> fileDeclaration.trim() }.associate { fileDeclaration -> + val filePathAndContent = fileDeclaration.split("\n", limit = 2) + val filePath = filePathAndContent.first().removePrefix("/").trim() + val content = filePathAndContent.last().trim() + filePath to content + } + } + + private fun String.sliceAt(regex: Regex): List<String> { + val matchesStartIndices = regex.findAll(this).toList().map { match -> match.range.first } + return sequence { + yield(0) + yieldAll(matchesStartIndices) + yield(this@sliceAt.length) + } + .zipWithNext { startIndex: Int, endIndex: Int -> substring(startIndex, endIndex) } + .toList() + .also { slices -> + /* Post-condition verifying that no character is lost */ + check(slices.sumBy { it.length } == length) + } + } + + private fun Map<String, String>.materializeFiles( + root: Path = Paths.get("."), + charset: Charset = Charset.forName("utf-8"), + ) = this.map { (path, content) -> + val file = root.resolve(path) + Files.createDirectories(file.parent) + Files.write(file, content.toByteArray(charset)) + } + + @Suppress("DEPRECATION") // TODO migrate to kotlin.io.path.createTempDirectory with languageVersion >= 1.5 + private fun createTempDir(): File = kotlin.io.createTempDir() + + protected fun dokkaConfiguration(block: TestDokkaConfigurationBuilder.() -> Unit): DokkaConfigurationImpl = + testApi.testRunner.dokkaConfiguration(block) + + + protected val jvmStdlibPath: String? by lazy { + ClassLoader.getSystemResource("kotlin/jvm/Strictfp.class") + ?.file + ?.replace("file:", "") + ?.replaceAfter(".jar", "") + } + + protected val jsStdlibPath: String? by lazy { + ClassLoader.getSystemResource("kotlin/jquery") + ?.file + ?.replace("file:", "") + ?.replaceAfter(".jar", "") + } + + protected val commonStdlibPath: String? by lazy { + // `kotlin-stdlib-common` is legacy + // we can use any platform dependency + // since common code should be resolved with all platform + jvmStdlibPath + } + + protected val stdlibExternalDocumentationLink: ExternalDocumentationLinkImpl = ExternalDocumentationLinkImpl( + URL("https://kotlinlang.org/api/latest/jvm/stdlib/"), + URL("https://kotlinlang.org/api/latest/jvm/stdlib/package-list") + ) + + public companion object { + private val filePathRegex = Regex("""[\n^](\/[\w|\-]+)+(\.\w+)?\s*\n""") + } +} + +public interface TestMethods + +public open class CoreTestMethods( + public open val pluginsSetupStage: (DokkaContext) -> Unit, + public open val verificationStage: (() -> Unit) -> Unit, + public open val documentablesCreationStage: (List<DModule>) -> Unit, + public open val documentablesMergingStage: (DModule) -> Unit, + public open val documentablesTransformationStage: (DModule) -> Unit, + public open val pagesGenerationStage: (RootPageNode) -> Unit, + public open val pagesTransformationStage: (RootPageNode) -> Unit, + public open val renderingStage: (RootPageNode, DokkaContext) -> Unit, +) : TestMethods + +public abstract class TestBuilder<M : TestMethods> { + public abstract fun build(): M +} + +public abstract class DokkaTestGenerator<T : TestMethods>( + protected val configuration: DokkaConfiguration, + protected val logger: DokkaLogger, + protected val testMethods: T, + protected val additionalPlugins: List<DokkaPlugin> = emptyList(), +) { + public abstract fun generate() +} |