diff options
Diffstat (limited to 'core')
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 +) |