diff options
author | Paweł Marks <pmarks@virtuslab.com> | 2020-03-25 16:11:21 +0100 |
---|---|---|
committer | Paweł Marks <Kordyjan@users.noreply.github.com> | 2020-03-31 16:02:09 +0200 |
commit | 136a835671ef0cdf09c3475db1b61e15aee1be48 (patch) | |
tree | 5884860a75c1f835cf6139539a7625ade7d95e49 | |
parent | a8e5b94f8e95138dbfa913b74a4d2d65fd5993c1 (diff) | |
download | dokka-136a835671ef0cdf09c3475db1b61e15aee1be48.tar.gz dokka-136a835671ef0cdf09c3475db1b61e15aee1be48.tar.bz2 dokka-136a835671ef0cdf09c3475db1b61e15aee1be48.zip |
Add test-tools module and matchers for content inside of it
-rw-r--r-- | .idea/compiler.xml | 2 | ||||
-rw-r--r-- | settings.gradle.kts | 3 | ||||
-rw-r--r-- | test-tools/build.gradle.kts | 4 | ||||
-rw-r--r-- | test-tools/src/main/kotlin/matchers/content/ContentMatchersDsl.kt | 61 | ||||
-rw-r--r-- | test-tools/src/main/kotlin/matchers/content/contentMatchers.kt | 105 |
5 files changed, 174 insertions, 1 deletions
diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 0848aa0c..924f38f8 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -66,6 +66,8 @@ <module name="dokka.runners.maven-plugin.main" target="1.8" /> <module name="dokka.runners.maven-plugin.test" target="1.8" /> <module name="dokka.runners.test" target="1.8" /> + <module name="dokka.test-tools.main" target="1.8" /> + <module name="dokka.test-tools.test" target="1.8" /> <module name="dokka.testApi.main" target="1.8" /> <module name="dokka.testApi.test" target="1.8" /> <module name="fatjar_main" target="1.8" /> diff --git a/settings.gradle.kts b/settings.gradle.kts index 263a1b09..8b4d4050 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ rootProject.name = "dokka" include("core") include("coreDependencies") include("testApi") +include("test-tools") include("runners:gradle-plugin") include("runners:cli") include("runners:maven-plugin") @@ -26,4 +27,4 @@ pluginManagement { jcenter() gradlePluginPortal() } -}
\ No newline at end of file +} diff --git a/test-tools/build.gradle.kts b/test-tools/build.gradle.kts new file mode 100644 index 00000000..4f6d2500 --- /dev/null +++ b/test-tools/build.gradle.kts @@ -0,0 +1,4 @@ +dependencies { + implementation(project(":testApi")) + implementation(kotlin("stdlib-jdk8")) +}
\ No newline at end of file diff --git a/test-tools/src/main/kotlin/matchers/content/ContentMatchersDsl.kt b/test-tools/src/main/kotlin/matchers/content/ContentMatchersDsl.kt new file mode 100644 index 00000000..ae3ecab7 --- /dev/null +++ b/test-tools/src/main/kotlin/matchers/content/ContentMatchersDsl.kt @@ -0,0 +1,61 @@ +package matchers.content + +import org.jetbrains.dokka.pages.ContentComposite +import org.jetbrains.dokka.pages.ContentGroup +import org.jetbrains.dokka.pages.ContentNode +import org.jetbrains.dokka.test.tools.matchers.content.* +import kotlin.reflect.KClass + +// entry point: +fun ContentNode.assertNode(block: ContentMatcherBuilder<ContentComposite>.() -> Unit) { + ContentMatcherBuilder(ContentComposite::class).apply(block).build().tryMatch(this) +} + + +// 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, children) { assertions.forEach { it() } } + + // part of DSL that cannot be defined as an extension + operator fun String.unaryPlus() { + children += TextMatcher(this) + } +} + +fun <T : ContentComposite> ContentMatcherBuilder<T>.check(assertion: T.() -> Unit) { + assertions += assertion +} + +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) { + children += ContentMatcherBuilder(ContentGroup::class).apply(block).build() +} + +fun ContentMatcherBuilder<*>.somewhere(block: ContentMatcherBuilder<*>.() -> Unit) { + skipAllNotMatching() + block() + skipAllNotMatching() +}
\ No newline at end of file diff --git a/test-tools/src/main/kotlin/matchers/content/contentMatchers.kt b/test-tools/src/main/kotlin/matchers/content/contentMatchers.kt new file mode 100644 index 00000000..c4400646 --- /dev/null +++ b/test-tools/src/main/kotlin/matchers/content/contentMatchers.kt @@ -0,0 +1,105 @@ +package org.jetbrains.dokka.test.tools.matchers.content + +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 + +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) { + assertions(kclass.cast(node)) + } +} + +class CompositeMatcher<T : ContentComposite>( + kclass: KClass<T>, + private val children: List<MatcherElement>, + assertions: T.() -> Unit = {} +) : NodeMatcher<T>(kclass, assertions) { + private 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.fold(normalizedChildren.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: List<MatcherElement>) : MatchWalkerState() { + override fun next(node: ContentNode): MatchWalkerState { + node as ContentText + return when { + text == node.text -> rest.pop() + text.startsWith(node.text) -> TextMatcherState(text.removePrefix(node.text), rest) + else -> throw AssertionError("Expected text: \"$text\", but found: \"${node.text}\"") + } + } + + override fun finish() = throw AssertionError("\"$text\" was not found" + rest.messageEnd) +} + +private object EmptyMatcherState : MatchWalkerState() { + override fun next(node: ContentNode): MatchWalkerState { + throw AssertionError("Unexpected $node") + } + + override fun finish() = Unit +} + +private class NodeMatcherState( + val matcher: NodeMatcher<*>, + val rest: List<MatcherElement> +) : MatchWalkerState() { + override fun next(node: ContentNode): MatchWalkerState { + matcher.tryMatch(node) + return rest.pop() + } + + override fun finish() = throw AssertionError("Composite of type ${matcher.kclass} was not found" + rest.messageEnd) +} + +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 fun List<MatcherElement>.pop(): MatchWalkerState = when (val head = firstOrNull()) { + is TextMatcher -> TextMatcherState(head.text, drop(1)) + is NodeMatcher<*> -> NodeMatcherState(head, drop(1)) + is Anything -> SkippingMatcherState(drop(1).pop()) + null -> EmptyMatcherState +} + +private val List<MatcherElement>.messageEnd: String + get() = filter { it !is Anything } + .count().takeIf { it > 0 } + ?.let { " and $it further matchers were not satisfied" } ?: "" + +// 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 |