From 8e5c63d035ef44a269b8c43430f43f5c8eebfb63 Mon Sep 17 00:00:00 2001 From: Ignat Beresnev Date: Fri, 10 Nov 2023 11:46:54 +0100 Subject: Restructure the project to utilize included builds (#3174) * Refactor and simplify artifact publishing * Update Gradle to 8.4 * Refactor and simplify convention plugins and build scripts Fixes #3132 --------- Co-authored-by: Adam <897017+aSemy@users.noreply.github.com> Co-authored-by: Oleg Yukhnevich --- .../api/core-content-matcher-test-utils.api | 81 +++++++++ .../build.gradle.kts | 14 ++ .../tools/matchers/content/ContentMatchersDsl.kt | 191 +++++++++++++++++++++ .../test/tools/matchers/content/contentMatchers.kt | 191 +++++++++++++++++++++ 4 files changed, 477 insertions(+) create mode 100644 dokka-subprojects/core-content-matcher-test-utils/api/core-content-matcher-test-utils.api create mode 100644 dokka-subprojects/core-content-matcher-test-utils/build.gradle.kts create mode 100644 dokka-subprojects/core-content-matcher-test-utils/src/main/kotlin/org/jetbrains/dokka/test/tools/matchers/content/ContentMatchersDsl.kt create mode 100644 dokka-subprojects/core-content-matcher-test-utils/src/main/kotlin/org/jetbrains/dokka/test/tools/matchers/content/contentMatchers.kt (limited to 'dokka-subprojects/core-content-matcher-test-utils') diff --git a/dokka-subprojects/core-content-matcher-test-utils/api/core-content-matcher-test-utils.api b/dokka-subprojects/core-content-matcher-test-utils/api/core-content-matcher-test-utils.api new file mode 100644 index 00000000..58881a15 --- /dev/null +++ b/dokka-subprojects/core-content-matcher-test-utils/api/core-content-matcher-test-utils.api @@ -0,0 +1,81 @@ +public final class matchers/content/ContentMatcherBuilder { + public fun (Lkotlin/reflect/KClass;)V + public final fun build ()Lorg/jetbrains/dokka/test/tools/matchers/content/CompositeMatcher; + public final fun getChildren ()Ljava/util/List; + public final fun unaryPlus (Ljava/lang/String;)V +} + +public abstract interface annotation class matchers/content/ContentMatchersDsl : java/lang/annotation/Annotation { +} + +public final class matchers/content/ContentMatchersDslKt { + public static final fun after (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun assertNode (Lorg/jetbrains/dokka/pages/ContentNode;Lkotlin/jvm/functions/Function1;)V + public static final fun before (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun br (Lmatchers/content/ContentMatcherBuilder;)V + public static final fun caption (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun check (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun codeBlock (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun codeInline (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun divergent (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun divergentGroup (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun divergentInstance (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun group (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun hasExactText (Lmatchers/content/ContentMatcherBuilder;Ljava/lang/String;)V + public static final fun header (Lmatchers/content/ContentMatcherBuilder;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun header$default (Lmatchers/content/ContentMatcherBuilder;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static final fun link (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun list (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun p (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun platformHinted (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun skipAllNotMatching (Lmatchers/content/ContentMatcherBuilder;)V + public static final fun somewhere (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun tab (Lmatchers/content/ContentMatcherBuilder;Lorg/jetbrains/dokka/pages/TabbedContentType;Lkotlin/jvm/functions/Function1;)V + public static final fun tabbedGroup (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun table (Lmatchers/content/ContentMatcherBuilder;Lkotlin/jvm/functions/Function1;)V +} + +public final class org/jetbrains/dokka/test/tools/matchers/content/Anything : org/jetbrains/dokka/test/tools/matchers/content/MatcherElement { + public static final field INSTANCE Lorg/jetbrains/dokka/test/tools/matchers/content/Anything; +} + +public final class org/jetbrains/dokka/test/tools/matchers/content/CompositeMatcher : org/jetbrains/dokka/test/tools/matchers/content/NodeMatcher { + public fun (Lkotlin/reflect/KClass;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lkotlin/reflect/KClass;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun tryMatch (Lorg/jetbrains/dokka/pages/ContentNode;)V +} + +public abstract class org/jetbrains/dokka/test/tools/matchers/content/MatcherElement { +} + +public final class org/jetbrains/dokka/test/tools/matchers/content/MatcherError : java/lang/AssertionError { + public fun (Ljava/lang/String;Lorg/jetbrains/dokka/test/tools/matchers/content/MatcherElement;ZLjava/lang/Throwable;)V + public synthetic fun (Ljava/lang/String;Lorg/jetbrains/dokka/test/tools/matchers/content/MatcherElement;ZLjava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lorg/jetbrains/dokka/test/tools/matchers/content/MatcherElement; + public final fun component3 ()Z + public final fun component4 ()Ljava/lang/Throwable; + public final fun copy (Ljava/lang/String;Lorg/jetbrains/dokka/test/tools/matchers/content/MatcherElement;ZLjava/lang/Throwable;)Lorg/jetbrains/dokka/test/tools/matchers/content/MatcherError; + public static synthetic fun copy$default (Lorg/jetbrains/dokka/test/tools/matchers/content/MatcherError;Ljava/lang/String;Lorg/jetbrains/dokka/test/tools/matchers/content/MatcherElement;ZLjava/lang/Throwable;ILjava/lang/Object;)Lorg/jetbrains/dokka/test/tools/matchers/content/MatcherError; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnchor ()Lorg/jetbrains/dokka/test/tools/matchers/content/MatcherElement; + public final fun getAnchorAfter ()Z + public fun getCause ()Ljava/lang/Throwable; + public fun getMessage ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class org/jetbrains/dokka/test/tools/matchers/content/NodeMatcher : org/jetbrains/dokka/test/tools/matchers/content/MatcherElement { + public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAssertions ()Lkotlin/jvm/functions/Function1; + public final fun getKclass ()Lkotlin/reflect/KClass; + public fun tryMatch (Lorg/jetbrains/dokka/pages/ContentNode;)V +} + +public final class org/jetbrains/dokka/test/tools/matchers/content/TextMatcher : org/jetbrains/dokka/test/tools/matchers/content/MatcherElement { + public fun (Ljava/lang/String;)V + public final fun getText ()Ljava/lang/String; +} + diff --git a/dokka-subprojects/core-content-matcher-test-utils/build.gradle.kts b/dokka-subprojects/core-content-matcher-test-utils/build.gradle.kts new file mode 100644 index 00000000..4207c31b --- /dev/null +++ b/dokka-subprojects/core-content-matcher-test-utils/build.gradle.kts @@ -0,0 +1,14 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("dokkabuild.kotlin-jvm") +} + +dependencies { + implementation(projects.dokkaSubprojects.coreTestApi) + + implementation(kotlin("reflect")) + implementation(kotlin("test")) +} diff --git a/dokka-subprojects/core-content-matcher-test-utils/src/main/kotlin/org/jetbrains/dokka/test/tools/matchers/content/ContentMatchersDsl.kt b/dokka-subprojects/core-content-matcher-test-utils/src/main/kotlin/org/jetbrains/dokka/test/tools/matchers/content/ContentMatchersDsl.kt new file mode 100644 index 00000000..026f7b6b --- /dev/null +++ b/dokka-subprojects/core-content-matcher-test-utils/src/main/kotlin/org/jetbrains/dokka/test/tools/matchers/content/ContentMatchersDsl.kt @@ -0,0 +1,191 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package matchers.content + +import org.jetbrains.dokka.model.withDescendants +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.test.tools.matchers.content.* +import kotlin.reflect.KClass +import kotlin.test.assertEquals +import kotlin.test.asserter + +// entry point: +public fun ContentNode.assertNode(block: ContentMatcherBuilder.() -> 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 +public annotation class ContentMatchersDsl + +@ContentMatchersDsl +public class ContentMatcherBuilder @PublishedApi internal constructor(private val kclass: KClass) { + @PublishedApi + internal val children: MutableList = mutableListOf() + internal val assertions = mutableListOf Unit>() + + public fun build(): CompositeMatcher = CompositeMatcher(kclass, childrenOrSkip()) { assertions.forEach { it() } } + + // part of DSL that cannot be defined as an extension + public operator fun String.unaryPlus() { + children += TextMatcher(this) + } + + private fun childrenOrSkip() = if (children.isEmpty() && assertions.isNotEmpty()) listOf(Anything) else children +} + +public fun ContentMatcherBuilder.check(assertion: T.() -> Unit) { + assertions += assertion +} + +private val ContentComposite.extractedText + get() = withDescendants().filterIsInstance().joinToString(separator = "") { it.text } + +public fun ContentMatcherBuilder.hasExactText(expected: String) { + assertions += { + assertEquals(expected, this.extractedText) + } +} + +public inline fun ContentMatcherBuilder<*>.composite( + block: ContentMatcherBuilder.() -> Unit +) { + children += ContentMatcherBuilder(S::class).apply(block).build() +} + +public inline fun ContentMatcherBuilder<*>.node(noinline assertions: S.() -> Unit = {}) { + children += NodeMatcher(S::class, assertions) +} + +public fun ContentMatcherBuilder<*>.skipAllNotMatching() { + children += Anything +} + + +// Convenience functions: +public fun ContentMatcherBuilder<*>.group(block: ContentMatcherBuilder.() -> Unit) { + composite(block) +} + +public fun ContentMatcherBuilder<*>.tabbedGroup( + block: ContentMatcherBuilder.() -> Unit +) { + composite { + block() + check { assertContains(this.style, ContentStyle.TabbedContent) } + } +} + +public fun ContentMatcherBuilder<*>.tab( + tabbedContentType: TabbedContentType, block: ContentMatcherBuilder.() -> Unit +) { + composite { + block() + check { + assertEquals(tabbedContentType, this.extra[TabbedContentTypeExtra]?.value) + } + } +} + +public fun ContentMatcherBuilder<*>.header(expectedLevel: Int? = null, block: ContentMatcherBuilder.() -> Unit) { + composite { + block() + check { if (expectedLevel != null) assertEquals(expectedLevel, this.level) } + } +} + +public fun ContentMatcherBuilder<*>.p(block: ContentMatcherBuilder.() -> Unit) { + composite { + block() + check { assertContains(this.style, TextStyle.Paragraph) } + } +} + +public fun ContentMatcherBuilder<*>.link(block: ContentMatcherBuilder.() -> Unit) { + composite(block) +} + +public fun ContentMatcherBuilder<*>.table(block: ContentMatcherBuilder.() -> Unit) { + composite(block) +} + +public fun ContentMatcherBuilder<*>.platformHinted(block: ContentMatcherBuilder.() -> Unit) { + composite { group(block) } +} + +public fun ContentMatcherBuilder<*>.list(block: ContentMatcherBuilder.() -> Unit) { + composite(block) +} + +public fun ContentMatcherBuilder<*>.codeBlock(block: ContentMatcherBuilder.() -> Unit) { + composite(block) +} + +public fun ContentMatcherBuilder<*>.codeInline(block: ContentMatcherBuilder.() -> Unit) { + composite(block) +} + +public fun ContentMatcherBuilder<*>.caption(block: ContentMatcherBuilder.() -> Unit) { + composite { + block() + check { assertContains(this.style, ContentStyle.Caption) } + } +} + +public fun ContentMatcherBuilder<*>.br() { + node() +} + +public fun ContentMatcherBuilder<*>.somewhere(block: ContentMatcherBuilder<*>.() -> Unit) { + skipAllNotMatching() + block() + skipAllNotMatching() +} + +public fun ContentMatcherBuilder<*>.divergentGroup( + block: ContentMatcherBuilder.() -> Unit +) { + composite(block) +} + +public fun ContentMatcherBuilder.divergentInstance( + block: ContentMatcherBuilder.() -> Unit +) { + composite(block) +} + +public fun ContentMatcherBuilder.before( + block: ContentMatcherBuilder.() -> Unit +) { + composite(block) +} + +public fun ContentMatcherBuilder.divergent( + block: ContentMatcherBuilder.() -> Unit +) { + composite(block) +} + +public fun ContentMatcherBuilder.after( + block: ContentMatcherBuilder.() -> Unit +) { + composite(block) +} + +/* + * TODO replace with kotlin.test.assertContains after migrating to Kotlin language version 1.5+ + */ +private fun assertContains(iterable: Iterable, element: T) { + asserter.assertTrue( + { "Expected the collection to contain the element.\nCollection <$iterable>, element <$element>." }, + iterable.contains(element) + ) +} diff --git a/dokka-subprojects/core-content-matcher-test-utils/src/main/kotlin/org/jetbrains/dokka/test/tools/matchers/content/contentMatchers.kt b/dokka-subprojects/core-content-matcher-test-utils/src/main/kotlin/org/jetbrains/dokka/test/tools/matchers/content/contentMatchers.kt new file mode 100644 index 00000000..412f728b --- /dev/null +++ b/dokka-subprojects/core-content-matcher-test-utils/src/main/kotlin/org/jetbrains/dokka/test/tools/matchers/content/contentMatchers.kt @@ -0,0 +1,191 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch") + +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 + +public sealed class MatcherElement + +public class TextMatcher( + public val text: String +) : MatcherElement() + +public open class NodeMatcher( + public val kclass: KClass, + public val assertions: T.() -> Unit = {} +) : MatcherElement() { + + public 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) + } +} + +public class CompositeMatcher( + kclass: KClass, + private val children: List, + assertions: T.() -> Unit = {} +) : NodeMatcher(kclass, assertions) { + + internal val normalizedChildren: List by lazy { + children.fold(listOf()) { 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() + } +} + +public 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) + return when { + text == node.text -> rest.pop() + text.startsWith(node.text) -> TextMatcherState(text.removePrefix(node.text), 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, val parent: CompositeMatcher<*>) { + fun pop(): MatchWalkerState = when (val head = list.firstOrNull()) { + is TextMatcher -> TextMatcherState(head.text, 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.count { it !is Anything }.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├─ " + val lastOwnPrefix = "$childPrefix└─ " + val newChildPrefix = "$childPrefix│ " + 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} " + + "}" + ) +} + +public 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. -- cgit