aboutsummaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/build.gradle.kts2
-rw-r--r--core/content-matcher-test-utils/build.gradle.kts6
-rw-r--r--core/content-matcher-test-utils/src/main/kotlin/matchers/content/ContentMatchersDsl.kt120
-rw-r--r--core/content-matcher-test-utils/src/main/kotlin/matchers/content/contentMatchers.kt183
-rw-r--r--core/test-api/build.gradle.kts18
-rw-r--r--core/test-api/src/main/kotlin/testApi/context/MockContext.kt47
-rw-r--r--core/test-api/src/main/kotlin/testApi/logger/TestLogger.kt79
-rw-r--r--core/test-api/src/main/kotlin/testApi/testRunner/DokkaTestGenerator.kt45
-rw-r--r--core/test-api/src/main/kotlin/testApi/testRunner/TestRunner.kt289
9 files changed, 788 insertions, 1 deletions
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index ad3394a6..f4fb1017 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -12,7 +12,7 @@ dependencies {
implementation("org.jsoup:jsoup:1.12.1")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.1")
- testImplementation(project(":testApi"))
+ testImplementation(project(":core:test-api"))
testImplementation(kotlin("test-junit"))
}
diff --git a/core/content-matcher-test-utils/build.gradle.kts b/core/content-matcher-test-utils/build.gradle.kts
new file mode 100644
index 00000000..135fc514
--- /dev/null
+++ b/core/content-matcher-test-utils/build.gradle.kts
@@ -0,0 +1,6 @@
+dependencies {
+ implementation(project(":core:test-api"))
+ implementation(kotlin("stdlib-jdk8"))
+ implementation(kotlin("reflect"))
+ implementation("com.willowtreeapps.assertk:assertk-jvm:0.22")
+}
diff --git a/core/content-matcher-test-utils/src/main/kotlin/matchers/content/ContentMatchersDsl.kt b/core/content-matcher-test-utils/src/main/kotlin/matchers/content/ContentMatchersDsl.kt
new file mode 100644
index 00000000..67c0e692
--- /dev/null
+++ b/core/content-matcher-test-utils/src/main/kotlin/matchers/content/ContentMatchersDsl.kt
@@ -0,0 +1,120 @@
+package matchers.content
+
+import assertk.assertThat
+import assertk.assertions.contains
+import assertk.assertions.isEqualTo
+import assertk.assertions.matches
+import org.jetbrains.dokka.model.withDescendants
+import org.jetbrains.dokka.pages.*
+import org.jetbrains.dokka.test.tools.matchers.content.*
+import kotlin.reflect.KClass
+
+// entry point:
+fun ContentNode.assertNode(block: ContentMatcherBuilder<ContentComposite>.() -> Unit) {
+ val matcher = ContentMatcherBuilder(ContentComposite::class).apply(block).build()
+ try {
+ matcher.tryMatch(this)
+ } catch (e: MatcherError) {
+ throw AssertionError(e.message + "\n" + matcher.toDebugString(e.anchor, e.anchorAfter))
+ }
+}
+
+
+// DSL:
+@DslMarker
+annotation class ContentMatchersDsl
+
+@ContentMatchersDsl
+class ContentMatcherBuilder<T : ContentComposite> @PublishedApi internal constructor(private val kclass: KClass<T>) {
+ @PublishedApi
+ internal val children = mutableListOf<MatcherElement>()
+ internal val assertions = mutableListOf<T.() -> Unit>()
+
+ fun build() = CompositeMatcher(kclass, childrenOrSkip()) { assertions.forEach { it() } }
+
+ // part of DSL that cannot be defined as an extension
+ operator fun String.unaryPlus() {
+ children += TextMatcher(this)
+ }
+
+ private fun childrenOrSkip() = if (children.isEmpty() && assertions.isNotEmpty()) listOf(Anything) else children
+}
+
+fun <T : ContentComposite> ContentMatcherBuilder<T>.check(assertion: T.() -> Unit) {
+ assertions += assertion
+}
+
+private val ContentComposite.extractedText
+ get() = withDescendants().filterIsInstance<ContentText>().joinToString(separator = "") { it.text }
+
+fun <T : ContentComposite> ContentMatcherBuilder<T>.hasExactText(expected: String) {
+ assertions += {
+ assertThat(this::extractedText).isEqualTo(expected)
+ }
+}
+
+fun <T : ContentComposite> ContentMatcherBuilder<T>.textMatches(pattern: Regex) {
+ assertions += {
+ assertThat(this::extractedText).matches(pattern)
+ }
+}
+
+inline fun <reified S : ContentComposite> ContentMatcherBuilder<*>.composite(
+ block: ContentMatcherBuilder<S>.() -> Unit
+) {
+ children += ContentMatcherBuilder(S::class).apply(block).build()
+}
+
+inline fun <reified S : ContentNode> ContentMatcherBuilder<*>.node(noinline assertions: S.() -> Unit = {}) {
+ children += NodeMatcher(S::class, assertions)
+}
+
+fun ContentMatcherBuilder<*>.skipAllNotMatching() {
+ children += Anything
+}
+
+
+// Convenience functions:
+fun ContentMatcherBuilder<*>.group(block: ContentMatcherBuilder<ContentGroup>.() -> Unit) = composite(block)
+
+fun ContentMatcherBuilder<*>.header(expectedLevel: Int? = null, block: ContentMatcherBuilder<ContentHeader>.() -> Unit) =
+ composite<ContentHeader> {
+ block()
+ check { if (expectedLevel != null) assertThat(this::level).isEqualTo(expectedLevel) }
+ }
+
+fun ContentMatcherBuilder<*>.p(block: ContentMatcherBuilder<ContentGroup>.() -> Unit) =
+ composite<ContentGroup> {
+ block()
+ check { assertThat(this::style).contains(TextStyle.Paragraph) }
+ }
+
+fun ContentMatcherBuilder<*>.link(block: ContentMatcherBuilder<ContentLink>.() -> Unit) = composite(block)
+
+fun ContentMatcherBuilder<*>.table(block: ContentMatcherBuilder<ContentTable>.() -> Unit) = composite(block)
+
+fun ContentMatcherBuilder<*>.platformHinted(block: ContentMatcherBuilder<ContentGroup>.() -> Unit) =
+ composite<PlatformHintedContent> { group(block) }
+
+fun ContentMatcherBuilder<*>.br() = node<ContentBreakLine>()
+
+fun ContentMatcherBuilder<*>.somewhere(block: ContentMatcherBuilder<*>.() -> Unit) {
+ skipAllNotMatching()
+ block()
+ skipAllNotMatching()
+}
+
+fun ContentMatcherBuilder<*>.divergentGroup(block: ContentMatcherBuilder<ContentDivergentGroup>.() -> Unit) =
+ composite(block)
+
+fun ContentMatcherBuilder<ContentDivergentGroup>.divergentInstance(block: ContentMatcherBuilder<ContentDivergentInstance>.() -> Unit) =
+ composite(block)
+
+fun ContentMatcherBuilder<ContentDivergentInstance>.before(block: ContentMatcherBuilder<ContentComposite>.() -> Unit) =
+ composite(block)
+
+fun ContentMatcherBuilder<ContentDivergentInstance>.divergent(block: ContentMatcherBuilder<ContentComposite>.() -> Unit) =
+ composite(block)
+
+fun ContentMatcherBuilder<ContentDivergentInstance>.after(block: ContentMatcherBuilder<ContentComposite>.() -> Unit) =
+ composite(block) \ No newline at end of file
diff --git a/core/content-matcher-test-utils/src/main/kotlin/matchers/content/contentMatchers.kt b/core/content-matcher-test-utils/src/main/kotlin/matchers/content/contentMatchers.kt
new file mode 100644
index 00000000..f42e28f3
--- /dev/null
+++ b/core/content-matcher-test-utils/src/main/kotlin/matchers/content/contentMatchers.kt
@@ -0,0 +1,183 @@
+package org.jetbrains.dokka.test.tools.matchers.content
+
+import org.jetbrains.dokka.model.asPrintableTree
+import org.jetbrains.dokka.pages.ContentComposite
+import org.jetbrains.dokka.pages.ContentNode
+import org.jetbrains.dokka.pages.ContentText
+import kotlin.reflect.KClass
+import kotlin.reflect.full.cast
+import kotlin.reflect.full.safeCast
+
+sealed class MatcherElement
+
+class TextMatcher(val text: String) : MatcherElement()
+
+open class NodeMatcher<T : ContentNode>(
+ val kclass: KClass<T>,
+ val assertions: T.() -> Unit = {}
+) : MatcherElement() {
+ open fun tryMatch(node: ContentNode) {
+ kclass.safeCast(node)?.apply {
+ try {
+ assertions()
+ } catch (e: AssertionError) {
+ throw MatcherError(
+ "${e.message.orEmpty()}\nin node:\n${node.debugRepresentation()}",
+ this@NodeMatcher,
+ cause = e
+ )
+ }
+ } ?: throw MatcherError("Expected ${kclass.simpleName} but got:\n${node.debugRepresentation()}", this)
+ }
+}
+
+class CompositeMatcher<T : ContentComposite>(
+ kclass: KClass<T>,
+ private val children: List<MatcherElement>,
+ assertions: T.() -> Unit = {}
+) : NodeMatcher<T>(kclass, assertions) {
+ internal val normalizedChildren: List<MatcherElement> by lazy {
+ children.fold(listOf<MatcherElement>()) { acc, e ->
+ when {
+ acc.lastOrNull() is Anything && e is Anything -> acc
+ acc.lastOrNull() is TextMatcher && e is TextMatcher ->
+ acc.dropLast(1) + TextMatcher((acc.lastOrNull() as TextMatcher).text + e.text)
+ else -> acc + e
+ }
+ }
+ }
+
+ override fun tryMatch(node: ContentNode) {
+ super.tryMatch(node)
+ kclass.cast(node).children.asSequence()
+ .filter { it !is ContentText || it.text.isNotBlank() }
+ .fold(FurtherSiblings(normalizedChildren, this).pop()) { acc, n -> acc.next(n) }.finish()
+ }
+}
+
+object Anything : MatcherElement()
+
+private sealed class MatchWalkerState {
+ abstract fun next(node: ContentNode): MatchWalkerState
+ abstract fun finish()
+}
+
+private class TextMatcherState(
+ val text: String,
+ val rest: FurtherSiblings,
+ val anchor: TextMatcher
+) : MatchWalkerState() {
+ override fun next(node: ContentNode): MatchWalkerState {
+ node as? ContentText ?: throw MatcherError("Expected text: \"$text\" but got\n${node.debugRepresentation()}", anchor)
+ val trimmed = node.text.trim()
+ return when {
+ text == trimmed -> rest.pop()
+ text.startsWith(trimmed) -> TextMatcherState(text.removePrefix(node.text).trim(), rest, anchor)
+ else -> throw MatcherError("Expected text: \"$text\", but got: \"${node.text}\"", anchor)
+ }
+ }
+
+ override fun finish() = throw MatcherError("\"$text\" was not found" + rest.messageEnd, anchor)
+}
+
+private class EmptyMatcherState(val parent: CompositeMatcher<*>) : MatchWalkerState() {
+ override fun next(node: ContentNode): MatchWalkerState {
+ throw MatcherError("Unexpected node:\n${node.debugRepresentation()}", parent, anchorAfter = true)
+ }
+
+ override fun finish() = Unit
+}
+
+private class NodeMatcherState(
+ val matcher: NodeMatcher<*>,
+ val rest: FurtherSiblings
+) : MatchWalkerState() {
+ override fun next(node: ContentNode): MatchWalkerState {
+ matcher.tryMatch(node)
+ return rest.pop()
+ }
+
+ override fun finish() =
+ throw MatcherError("Content of type ${matcher.kclass} was not found" + rest.messageEnd, matcher)
+}
+
+private class SkippingMatcherState(
+ val innerState: MatchWalkerState
+) : MatchWalkerState() {
+ override fun next(node: ContentNode): MatchWalkerState = runCatching { innerState.next(node) }.getOrElse { this }
+
+ override fun finish() = innerState.finish()
+}
+
+private class FurtherSiblings(val list: List<MatcherElement>, val parent: CompositeMatcher<*>) {
+ fun pop(): MatchWalkerState = when (val head = list.firstOrNull()) {
+ is TextMatcher -> TextMatcherState(head.text.trim(), drop(), head)
+ is NodeMatcher<*> -> NodeMatcherState(head, drop())
+ is Anything -> SkippingMatcherState(drop().pop())
+ null -> EmptyMatcherState(parent)
+ }
+
+ fun drop() = FurtherSiblings(list.drop(1), parent)
+
+ val messageEnd: String
+ get() = list.filter { it !is Anything }
+ .count().takeIf { it > 0 }
+ ?.let { " and $it further matchers were not satisfied" } ?: ""
+}
+
+
+internal fun MatcherElement.toDebugString(anchor: MatcherElement?, anchorAfter: Boolean): String {
+ fun Appendable.append(element: MatcherElement, ownPrefix: String, childPrefix: String) {
+ if (anchor != null) {
+ if (element != anchor || anchorAfter) append(" ".repeat(4))
+ else append("--> ")
+ }
+
+ append(ownPrefix)
+ when (element) {
+ is Anything -> append("skipAllNotMatching\n")
+ is TextMatcher -> append("\"${element.text}\"\n")
+ is CompositeMatcher<*> -> {
+ append("${element.kclass.simpleName.toString()}\n")
+ if (element.normalizedChildren.isNotEmpty()) {
+ val newOwnPrefix = childPrefix + '\u251c' + '\u2500' + ' '
+ val lastOwnPrefix = childPrefix + '\u2514' + '\u2500' + ' '
+ val newChildPrefix = childPrefix + '\u2502' + ' ' + ' '
+ val lastChildPrefix = childPrefix + ' ' + ' ' + ' '
+ element.normalizedChildren.forEachIndexed { n, e ->
+ if (n != element.normalizedChildren.lastIndex) append(e, newOwnPrefix, newChildPrefix)
+ else append(e, lastOwnPrefix, lastChildPrefix)
+ }
+ }
+ if (element == anchor && anchorAfter) {
+ append("--> $childPrefix\n")
+ }
+ }
+ is NodeMatcher<*> -> append("${element.kclass.simpleName}\n")
+ }
+ }
+
+ return buildString { append(this@toDebugString, "", "") }
+}
+
+private fun ContentNode.debugRepresentation() = asPrintableTree { element ->
+ append(if (element is ContentText) """"${element.text}"""" else element::class.simpleName)
+ append(
+ " { " +
+ "kind=${element.dci.kind}, " +
+ "dri=${element.dci.dri}, " +
+ "style=${element.style}, " +
+ "sourceSets=${element.sourceSets} " +
+ "}"
+ )
+}
+
+data class MatcherError(
+ override val message: String,
+ val anchor: MatcherElement,
+ val anchorAfter: Boolean = false,
+ override val cause: Throwable? = null
+) : AssertionError(message, cause)
+
+// Creating this whole mechanism was most scala-like experience I had since I stopped using scala.
+// I don't know how I should feel about it. \ No newline at end of file
diff --git a/core/test-api/build.gradle.kts b/core/test-api/build.gradle.kts
new file mode 100644
index 00000000..fc882f44
--- /dev/null
+++ b/core/test-api/build.gradle.kts
@@ -0,0 +1,18 @@
+import org.jetbrains.registerDokkaArtifactPublication
+
+plugins {
+ `maven-publish`
+ id("com.jfrog.bintray")
+}
+
+dependencies {
+ api(project(":core"))
+ implementation(project(":kotlin-analysis"))
+ implementation("junit:junit:4.13") // TODO: remove dependency to junit
+ implementation(kotlin("stdlib"))
+ implementation(kotlin("reflect"))
+}
+
+registerDokkaArtifactPublication("dokkaTestApi") {
+ artifactId = "dokka-test-api"
+}
diff --git a/core/test-api/src/main/kotlin/testApi/context/MockContext.kt b/core/test-api/src/main/kotlin/testApi/context/MockContext.kt
new file mode 100644
index 00000000..97347695
--- /dev/null
+++ b/core/test-api/src/main/kotlin/testApi/context/MockContext.kt
@@ -0,0 +1,47 @@
+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 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
+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 = DokkaConsoleLogger
+
+ 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/core/test-api/src/main/kotlin/testApi/logger/TestLogger.kt b/core/test-api/src/main/kotlin/testApi/logger/TestLogger.kt
new file mode 100644
index 00000000..4f81f098
--- /dev/null
+++ b/core/test-api/src/main/kotlin/testApi/logger/TestLogger.kt
@@ -0,0 +1,79 @@
+package org.jetbrains.dokka.testApi.logger
+
+import org.jetbrains.dokka.utilities.DokkaLogger
+
+class TestLogger(private val logger: DokkaLogger) : DokkaLogger {
+ override var warningsCount: Int by logger::warningsCount
+ override var errorsCount: Int by logger::errorsCount
+
+ private var _debugMessages = mutableListOf<String>()
+ val debugMessages: List<String> get() = _debugMessages.toList()
+
+ private var _infoMessages = mutableListOf<String>()
+ val infoMessages: List<String> get() = _infoMessages.toList()
+
+ private var _progressMessages = mutableListOf<String>()
+ val progressMessages: List<String> get() = _progressMessages.toList()
+
+ private var _warnMessages = mutableListOf<String>()
+ val warnMessages: List<String> get() = _warnMessages.toList()
+
+ private var _errorMessages = mutableListOf<String>()
+ 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)
+ }
+}
+
+class FilteringLogger(
+ private val minLevel: Level,
+ private val downstream: DokkaLogger
+) : DokkaLogger {
+ enum class Level { Debug, Info, Progress, Warn, Error }
+
+ override var warningsCount: Int by downstream::warningsCount
+
+ override var errorsCount by downstream::errorsCount
+
+ override fun debug(message: String) {
+ if (minLevel <= Level.Debug) downstream.debug(message)
+ }
+
+ override fun info(message: String) {
+ if (minLevel <= Level.Info) downstream.info(message)
+ }
+
+ override fun progress(message: String) {
+ if (minLevel <= Level.Progress) downstream.progress(message)
+ }
+
+ override fun warn(message: String) {
+ if (minLevel <= Level.Warn) downstream.warn(message)
+ }
+
+ override fun error(message: String) {
+ if (minLevel <= Level.Error) downstream.error(message)
+ }
+}
diff --git a/core/test-api/src/main/kotlin/testApi/testRunner/DokkaTestGenerator.kt b/core/test-api/src/main/kotlin/testApi/testRunner/DokkaTestGenerator.kt
new file mode 100644
index 00000000..414919dc
--- /dev/null
+++ b/core/test-api/src/main/kotlin/testApi/testRunner/DokkaTestGenerator.kt
@@ -0,0 +1,45 @@
+package org.jetbrains.dokka.testApi.testRunner
+
+import org.jetbrains.dokka.DokkaConfiguration
+import org.jetbrains.dokka.DokkaGenerator
+import org.jetbrains.dokka.plugability.DokkaPlugin
+import org.jetbrains.dokka.utilities.DokkaLogger
+
+internal class DokkaTestGenerator(
+ private val configuration: DokkaConfiguration,
+ private val logger: DokkaLogger,
+ private val testMethods: TestMethods,
+ private val additionalPlugins: List<DokkaPlugin> = emptyList()
+) {
+
+ fun generate() = with(testMethods) {
+ val dokkaGenerator = DokkaGenerator(configuration, logger)
+
+ val context =
+ dokkaGenerator.initializePlugins(configuration, logger, additionalPlugins)
+ pluginsSetupStage(context)
+
+ val modulesFromPlatforms = dokkaGenerator.createDocumentationModels(context)
+ documentablesCreationStage(modulesFromPlatforms)
+
+ val filteredModules = dokkaGenerator.transformDocumentationModelBeforeMerge(modulesFromPlatforms, context)
+ documentablesFirstTransformationStep(filteredModules)
+
+ val documentationModel = dokkaGenerator.mergeDocumentationModels(filteredModules, context)
+ documentablesMergingStage(documentationModel)
+
+ val transformedDocumentation = dokkaGenerator.transformDocumentationModelAfterMerge(documentationModel, context)
+ documentablesTransformationStage(transformedDocumentation)
+
+ val pages = dokkaGenerator.createPages(transformedDocumentation, context)
+ pagesGenerationStage(pages)
+
+ val transformedPages = dokkaGenerator.transformPages(pages, context)
+ pagesTransformationStage(transformedPages)
+
+ dokkaGenerator.render(transformedPages, context)
+ renderingStage(transformedPages, context)
+
+ dokkaGenerator.reportAfterRendering(context)
+ }
+}
diff --git a/core/test-api/src/main/kotlin/testApi/testRunner/TestRunner.kt b/core/test-api/src/main/kotlin/testApi/testRunner/TestRunner.kt
new file mode 100644
index 00000000..a23c2713
--- /dev/null
+++ b/core/test-api/src/main/kotlin/testApi/testRunner/TestRunner.kt
@@ -0,0 +1,289 @@
+package org.jetbrains.dokka.testApi.testRunner
+
+import com.intellij.openapi.application.PathManager
+import org.jetbrains.dokka.*
+import org.jetbrains.dokka.model.DModule
+import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet
+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.DokkaConsoleLogger
+import org.jetbrains.dokka.utilities.DokkaLogger
+import org.junit.rules.TemporaryFolder
+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
+abstract class AbstractCoreTest(
+ protected val logger: TestLogger = TestLogger(DokkaConsoleLogger)
+) {
+ protected fun getTestDataDir(name: String) =
+ File("src/test/resources/$name").takeIf { it.exists() }?.toPath()
+ ?: throw InvalidPathException(name, "Cannot be found")
+
+ protected fun testFromData(
+ configuration: DokkaConfigurationImpl,
+ cleanupOutput: Boolean = true,
+ pluginOverrides: List<DokkaPlugin> = emptyList(),
+ block: TestBuilder.() -> Unit
+ ) {
+ val testMethods = TestBuilder().apply(block).build()
+ val tempDir = getTempDir(cleanupOutput)
+ if (!cleanupOutput)
+ logger.info("Output generated under: ${tempDir.root.absolutePath}")
+ val newConfiguration =
+ configuration.copy(
+ outputDir = tempDir.root
+ )
+ DokkaTestGenerator(
+ newConfiguration,
+ logger,
+ testMethods,
+ pluginOverrides
+ ).generate()
+ }
+
+ protected fun testInline(
+ query: String,
+ configuration: DokkaConfigurationImpl,
+ cleanupOutput: Boolean = true,
+ pluginOverrides: List<DokkaPlugin> = emptyList(),
+ loggerForTest: DokkaLogger = logger,
+ block: TestBuilder.() -> Unit
+ ) {
+ val testMethods = TestBuilder().apply(block).build()
+ val testDirPath = getTempDir(cleanupOutput).root.toPath()
+ val fileMap = query.toFileMap()
+ fileMap.materializeFiles(testDirPath.toAbsolutePath())
+ if (!cleanupOutput)
+ loggerForTest.info("Output generated under: ${testDirPath.toAbsolutePath()}")
+ val newConfiguration = configuration.copy(
+ outputDir = testDirPath.toFile(),
+ sourceSets = configuration.sourceSets.map { sourceSet ->
+ sourceSet.copy(
+ sourceRoots = sourceSet.sourceRoots.map { file ->
+ testDirPath.toFile().resolve(file)
+ }.toSet(),
+ suppressedFiles = sourceSet.suppressedFiles.map { file ->
+ testDirPath.toFile().resolve(file)
+ }.toSet(),
+ sourceLinks = sourceSet.sourceLinks.map { link ->
+ link.copy(
+ localDirectory = testDirPath.toFile().resolve(link.localDirectory).canonicalPath
+ )
+ }.toSet()
+ )
+ }
+ )
+ DokkaTestGenerator(
+ newConfiguration,
+ loggerForTest,
+ 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() }
+ .map { fileDeclaration ->
+ val filePathAndContent = fileDeclaration.split("\n", limit = 2)
+ val filePath = filePathAndContent.first().removePrefix("/").trim()
+ val content = filePathAndContent.last().trim()
+ filePath to content
+ }
+ .toMap()
+ }
+
+ 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))
+ }
+
+ private fun getTempDir(cleanupOutput: Boolean) = if (cleanupOutput) {
+ TemporaryFolder().apply { create() }
+ } else {
+ object : TemporaryFolder() {
+ override fun after() {}
+ }.apply { create() }
+ }
+
+ protected class TestBuilder {
+ var pluginsSetupStage: (DokkaContext) -> Unit = {}
+ var documentablesCreationStage: (List<DModule>) -> Unit = {}
+ var documentablesFirstTransformationStep: (List<DModule>) -> Unit = {}
+ var documentablesMergingStage: (DModule) -> Unit = {}
+ var documentablesTransformationStage: (DModule) -> Unit = {}
+ var pagesGenerationStage: (RootPageNode) -> Unit = {}
+ var pagesTransformationStage: (RootPageNode) -> Unit = {}
+ var renderingStage: (RootPageNode, DokkaContext) -> Unit = { a, b -> }
+
+ @PublishedApi
+ internal fun build() = TestMethods(
+ pluginsSetupStage,
+ documentablesCreationStage,
+ documentablesFirstTransformationStep,
+ documentablesMergingStage,
+ documentablesTransformationStage,
+ pagesGenerationStage,
+ pagesTransformationStage,
+ renderingStage
+ )
+ }
+
+ protected fun dokkaConfiguration(block: DokkaConfigurationBuilder.() -> Unit): DokkaConfigurationImpl =
+ DokkaConfigurationBuilder().apply(block).build()
+
+ @DslMarker
+ protected annotation class DokkaConfigurationDsl
+
+ @DokkaConfigurationDsl
+ protected class DokkaConfigurationBuilder {
+ var outputDir: String = "out"
+ var format: String = "html"
+ var offlineMode: Boolean = false
+ var cacheRoot: String? = null
+ var pluginsClasspath: List<File> = emptyList()
+ var pluginsConfigurations: Map<String, String> = emptyMap()
+ var failOnWarning: Boolean = false
+ private val sourceSets = mutableListOf<DokkaSourceSetImpl>()
+ fun build() = DokkaConfigurationImpl(
+ outputDir = File(outputDir),
+ cacheRoot = cacheRoot?.let(::File),
+ offlineMode = offlineMode,
+ sourceSets = sourceSets.toList(),
+ pluginsClasspath = pluginsClasspath,
+ pluginsConfiguration = pluginsConfigurations,
+ modules = emptyList(),
+ failOnWarning = failOnWarning
+ )
+
+ fun sourceSets(block: SourceSetsBuilder.() -> Unit) {
+ sourceSets.addAll(SourceSetsBuilder().apply(block))
+ }
+ }
+
+ @DokkaConfigurationDsl
+ protected class SourceSetsBuilder : ArrayList<DokkaSourceSetImpl>() {
+ fun sourceSet(block: DokkaSourceSetBuilder.() -> Unit): DokkaSourceSet =
+ DokkaSourceSetBuilder().apply(block).build().apply(::add)
+ }
+
+ @DokkaConfigurationDsl
+ protected class DokkaSourceSetBuilder(
+ var moduleName: String = "root",
+ var moduleDisplayName: String? = null,
+ var name: String = "main",
+ var displayName: String = "JVM",
+ var classpath: List<String> = emptyList(),
+ var sourceRoots: List<String> = emptyList(),
+ var dependentSourceSets: Set<DokkaSourceSetID> = emptySet(),
+ var samples: List<String> = emptyList(),
+ var includes: List<String> = emptyList(),
+ var includeNonPublic: Boolean = false,
+ var includeRootPackage: Boolean = true,
+ var reportUndocumented: Boolean = false,
+ var skipEmptyPackages: Boolean = false,
+ var skipDeprecated: Boolean = false,
+ var jdkVersion: Int = 8,
+ var languageVersion: String? = null,
+ var apiVersion: String? = null,
+ var noStdlibLink: Boolean = false,
+ var noJdkLink: Boolean = false,
+ var suppressedFiles: List<String> = emptyList(),
+ var analysisPlatform: String = "jvm",
+ var perPackageOptions: List<PackageOptionsImpl> = emptyList(),
+ var externalDocumentationLinks: List<ExternalDocumentationLinkImpl> = emptyList(),
+ var sourceLinks: List<SourceLinkDefinitionImpl> = emptyList()
+ ) {
+ fun build() = DokkaSourceSetImpl(
+ moduleDisplayName = moduleDisplayName ?: moduleName,
+ 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,
+ 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)
+ )
+ }
+
+ protected val jvmStdlibPath: String? by lazy {
+ PathManager.getResourceRoot(Strictfp::class.java, "/kotlin/jvm/Strictfp.class")
+ }
+
+ protected val jsStdlibPath: String? by lazy {
+ PathManager.getResourceRoot(Any::class.java, "/kotlin/jquery")
+ }
+
+ protected val commonStdlibPath: String? by lazy {
+ // TODO: feels hacky, find a better way to do it
+ ClassLoader.getSystemResource("kotlin/UInt.kotlin_metadata")
+ ?.file
+ ?.replace("file:", "")
+ ?.replaceAfter(".jar", "")
+ }
+
+ protected val stdlibExternalDocumentationLink = ExternalDocumentationLinkImpl(
+ URL("https://kotlinlang.org/api/latest/jvm/stdlib/"),
+ URL("https://kotlinlang.org/api/latest/jvm/stdlib/package-list")
+ )
+
+ companion object {
+ private val filePathRegex = Regex("""[\n^](/\w+)+(\.\w+)?\s*\n""")
+ }
+}
+
+data class TestMethods(
+ val pluginsSetupStage: (DokkaContext) -> Unit,
+ val documentablesCreationStage: (List<DModule>) -> Unit,
+ val documentablesFirstTransformationStep: (List<DModule>) -> Unit,
+ val documentablesMergingStage: (DModule) -> Unit,
+ val documentablesTransformationStage: (DModule) -> Unit,
+ val pagesGenerationStage: (RootPageNode) -> Unit,
+ val pagesTransformationStage: (RootPageNode) -> Unit,
+ val renderingStage: (RootPageNode, DokkaContext) -> Unit
+)