From 136a835671ef0cdf09c3475db1b61e15aee1be48 Mon Sep 17 00:00:00 2001 From: Paweł Marks Date: Wed, 25 Mar 2020 16:11:21 +0100 Subject: Add test-tools module and matchers for content inside of it --- test-tools/build.gradle.kts | 4 + .../kotlin/matchers/content/ContentMatchersDsl.kt | 61 ++++++++++++ .../kotlin/matchers/content/contentMatchers.kt | 105 +++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 test-tools/build.gradle.kts create mode 100644 test-tools/src/main/kotlin/matchers/content/ContentMatchersDsl.kt create mode 100644 test-tools/src/main/kotlin/matchers/content/contentMatchers.kt (limited to 'test-tools') 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.() -> Unit) { + ContentMatcherBuilder(ContentComposite::class).apply(block).build().tryMatch(this) +} + + +// DSL: +@DslMarker +annotation class ContentMatchersDsl + +@ContentMatchersDsl +class ContentMatcherBuilder @PublishedApi internal constructor(private val kclass: KClass) { + @PublishedApi + internal val children = mutableListOf() + internal val assertions = mutableListOf 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 ContentMatcherBuilder.check(assertion: T.() -> Unit) { + assertions += assertion +} + +inline fun ContentMatcherBuilder<*>.composite( + block: ContentMatcherBuilder.() -> Unit +) { + children += ContentMatcherBuilder(S::class).apply(block).build() +} + +inline fun ContentMatcherBuilder<*>.node(noinline assertions: S.() -> Unit) { + children += NodeMatcher(S::class, assertions) +} + +fun ContentMatcherBuilder<*>.skipAllNotMatching() { + children += Anything +} + + +// Convenience functions: +fun ContentMatcherBuilder<*>.group(block: ContentMatcherBuilder.() -> 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( + val kclass: KClass, + val assertions: T.() -> Unit = {} +): MatcherElement() { + open fun tryMatch(node: ContentNode) { + assertions(kclass.cast(node)) + } +} + +class CompositeMatcher( + kclass: KClass, + private val children: List, + assertions: T.() -> Unit = {} +) : NodeMatcher(kclass, assertions) { + private 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.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) : 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 +) : 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.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.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 -- cgit From 810f3c922fb4f11dc3fbdddee82d919189ed8526 Mon Sep 17 00:00:00 2001 From: Paweł Marks Date: Thu, 26 Mar 2020 13:17:38 +0100 Subject: Adds simple tests for parameter rendering --- plugins/base/build.gradle.kts | 1 + .../kotlin/content/params/ContentForParamsTest.kt | 407 +++++++++++++++++++++ plugins/base/src/test/kotlin/utils/contentUtils.kt | 56 +++ test-tools/build.gradle.kts | 1 + .../kotlin/matchers/content/ContentMatchersDsl.kt | 41 ++- .../kotlin/matchers/content/contentMatchers.kt | 110 ++++-- 6 files changed, 584 insertions(+), 32 deletions(-) create mode 100644 plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt create mode 100644 plugins/base/src/test/kotlin/utils/contentUtils.kt (limited to 'test-tools') diff --git a/plugins/base/build.gradle.kts b/plugins/base/build.gradle.kts index 08f8601e..1a3d6c1d 100644 --- a/plugins/base/build.gradle.kts +++ b/plugins/base/build.gradle.kts @@ -6,6 +6,7 @@ plugins { dependencies { implementation("org.jsoup:jsoup:1.12.1") + testImplementation(project(":test-tools")) } publishing { diff --git a/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt b/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt new file mode 100644 index 00000000..9f8ebb00 --- /dev/null +++ b/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt @@ -0,0 +1,407 @@ +package content.params + +import matchers.content.* +import org.jetbrains.dokka.pages.ContentPage +import org.jetbrains.dokka.testApi.testRunner.AbstractCoreTest +import org.junit.jupiter.api.Test +import utils.pWrapped +import utils.signature +import utils.signatureWithReceiver + +class ContentForParamsTest : AbstractCoreTest() { + private val testConfiguration = dokkaConfiguration { + passes { + pass { + sourceRoots = listOf("src/") + analysisPlatform = "jvm" + targets = listOf("jvm") + } + } + } + + @Test + fun `undocumented function`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + header(1) { +"function" } + signature("function", null, "abc" to "String") + } + } + } + } + + @Test + fun `undocumented parameter`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + header(1) { +"function" } + signature("function", null, "abc" to "String") + header(3) { +"Description" } + platformHinted { + pWrapped("comment to function") + } + } + } + } + } + + @Test + fun `undocumented parameter and other tags without function comment`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @author Kordyjan + | * @since 0.11 + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + header(1) { +"function" } + signature("function", null, "abc" to "String") + header(3) { +"Description" } + platformHinted { + header(4) { +"Author" } + +"Kordyjan" + header(4) { +"Since" } + +"0.11" + } + } + } + } + } + + @Test + fun `undocumented parameter and other tags`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | * @author Kordyjan + | * @since 0.11 + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + header(1) { +"function" } + signature("function", null, "abc" to "String") + header(3) { +"Description" } + platformHinted { + pWrapped("comment to function") + header(4) { +"Author" } + +"Kordyjan" + header(4) { +"Since" } + +"0.11" + } + } + } + } + } + + @Test + fun `single parameter`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | * @param abc comment to param + | */ + |fun function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + header(1) { +"function" } + signature("function", null, "abc" to "String") + header(3) { +"Description" } + platformHinted { + pWrapped("comment to function") + header(4) { +"Parameters" } + table { + group { + +"abc" + +"comment to param" + } + } + } + } + } + } + } + + @Test + fun `multiple parameters`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | * @param first comment to first param + | * @param second comment to second param + | * @param[third] comment to third param + | */ + |fun function(first: String, second: Int, third: Double) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + header(1) { +"function" } + signature("function", null, "first" to "String", "second" to "Int", "third" to "Double") + header(3) { +"Description" } + platformHinted { + pWrapped("comment to function") + header(4) { +"Parameters" } + table { + group { + +"first" + +"comment to first param" + } + group { + +"second" + +"comment to second param" + } + group { + +"third" + +"comment to third param" + } + } + } + } + } + } + } + + @Test + fun `multiple parameters without function description`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * @param first comment to first param + | * @param second comment to second param + | * @param[third] comment to third param + | */ + |fun function(first: String, second: Int, third: Double) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + header(1) { +"function" } + signature("function", null, "first" to "String", "second" to "Int", "third" to "Double") + header(3) { +"Description" } + platformHinted { + header(4) { +"Parameters" } + table { + group { + +"first" + +"comment to first param" + } + group { + +"second" + +"comment to second param" + } + group { + +"third" + +"comment to third param" + } + } + } + } + } + } + } + + @Test + fun `function with receiver`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | * @param abc comment to param + | * @receiver comment to receiver + | */ + |fun String.function(abc: String) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + header(1) { +"function" } + signatureWithReceiver("String", "function", null, "abc" to "String") + header(3) { +"Description" } + platformHinted { + pWrapped("comment to function") + header(4) { +"Parameters" } + table { + group { + +"" + +"comment to receiver" + } + group { + +"abc" + +"comment to param" + } + } + } + } + } + } + } + + @Test + fun `missing parameter documentation`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | * @param first comment to first param + | * @param[third] comment to third param + | */ + |fun function(first: String, second: Int, third: Double) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + header(1) { +"function" } + signature("function", null, "first" to "String", "second" to "Int", "third" to "Double") + header(3) { +"Description" } + platformHinted { + pWrapped("comment to function") + header(4) { +"Parameters" } + table { + group { + +"first" + +"comment to first param" + } + group { + +"third" + +"comment to third param" + } + } + } + } + } + } + } + + @Test + fun `parameters mixed with other tags`() { + testInline( + """ + |/src/main/kotlin/test/source.kt + |package test + | /** + | * comment to function + | * @param first comment to first param + | * @author Kordyjan + | * @param second comment to second param + | * @since 0.11 + | * @param[third] comment to third param + | */ + |fun function(first: String, second: Int, third: Double) { + | println(abc) + |} + """.trimIndent(), testConfiguration + ) { + pagesTransformationStage = { module -> + val page = module.children.single { it.name == "test" } + .children.single { it.name == "function" } as ContentPage + page.content.assertNode { + header(1) { +"function" } + signature("function", null, "first" to "String", "second" to "Int", "third" to "Double") + header(3) { +"Description" } + platformHinted { + pWrapped("comment to function") + header(4) { +"Parameters" } + table { + group { + +"first" + +"comment to first param" + } + group { + +"second" + +"comment to second param" + } + group { + +"third" + +"comment to third param" + } + } + header(4) { +"Author" } + +"Kordyjan" + header(4) { +"Since" } + +"0.11" + } + } + } + } + } +} \ No newline at end of file diff --git a/plugins/base/src/test/kotlin/utils/contentUtils.kt b/plugins/base/src/test/kotlin/utils/contentUtils.kt new file mode 100644 index 00000000..4bb36553 --- /dev/null +++ b/plugins/base/src/test/kotlin/utils/contentUtils.kt @@ -0,0 +1,56 @@ +package utils + +import matchers.content.* + +//TODO: Try to unify those functions after update to 1.4 +fun ContentMatcherBuilder<*>.signature( + name: String, + returnType: String? = null, + vararg params: Pair +) = + platformHinted { + group { // TODO: remove it when double wrapping for signatures will be resolved + +"final fun" + link { +name } + +"(" + params.forEachIndexed { id, (n, t) -> + +"$n:" + group { link { +t } } + if (id != params.lastIndex) + +", " + } + +")" + returnType?.let { +": $it" } + } + } + +fun ContentMatcherBuilder<*>.signatureWithReceiver( + receiver: String, + name: String, + returnType: String? = null, + vararg params: Pair +) = + platformHinted { + group { // TODO: remove it when double wrapping for signatures will be resolved + +"final fun" + group { + link { +receiver } + } + +"." + link { +name } + +"(" + params.forEach { (n, t) -> + +"$n:" + group { link { +t } } + } + +")" + returnType?.let { +": $it" } + } + } + + +fun ContentMatcherBuilder<*>.pWrapped(text: String) = + group {// TODO: remove it when double wrapping for descriptions will be resolved + group { +text } + br() + } \ No newline at end of file diff --git a/test-tools/build.gradle.kts b/test-tools/build.gradle.kts index 4f6d2500..7fd8a879 100644 --- a/test-tools/build.gradle.kts +++ b/test-tools/build.gradle.kts @@ -1,4 +1,5 @@ dependencies { implementation(project(":testApi")) implementation(kotlin("stdlib-jdk8")) + implementation("com.willowtreeapps.assertk:assertk-jvm:0.22") } \ 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 index ae3ecab7..41d73378 100644 --- a/test-tools/src/main/kotlin/matchers/content/ContentMatchersDsl.kt +++ b/test-tools/src/main/kotlin/matchers/content/ContentMatchersDsl.kt @@ -1,14 +1,20 @@ package matchers.content -import org.jetbrains.dokka.pages.ContentComposite -import org.jetbrains.dokka.pages.ContentGroup -import org.jetbrains.dokka.pages.ContentNode +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.isEqualTo +import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.test.tools.matchers.content.* import kotlin.reflect.KClass // entry point: fun ContentNode.assertNode(block: ContentMatcherBuilder.() -> Unit) { - ContentMatcherBuilder(ContentComposite::class).apply(block).build().tryMatch(this) + 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)) + } } @@ -40,7 +46,7 @@ inline fun ContentMatcherBuilder<*>.composite( children += ContentMatcherBuilder(S::class).apply(block).build() } -inline fun ContentMatcherBuilder<*>.node(noinline assertions: S.() -> Unit) { +inline fun ContentMatcherBuilder<*>.node(noinline assertions: S.() -> Unit = {}) { children += NodeMatcher(S::class, assertions) } @@ -50,9 +56,28 @@ fun ContentMatcherBuilder<*>.skipAllNotMatching() { // Convenience functions: -fun ContentMatcherBuilder<*>.group(block: ContentMatcherBuilder.() -> Unit) { - children += ContentMatcherBuilder(ContentGroup::class).apply(block).build() -} +fun ContentMatcherBuilder<*>.group(block: ContentMatcherBuilder.() -> Unit) = composite(block) + +fun ContentMatcherBuilder<*>.header(expectedLevel: Int? = null, block: ContentMatcherBuilder.() -> Unit) = + composite { + block() + check { if (expectedLevel != null) assertThat(this::level).isEqualTo(expectedLevel) } + } + +fun ContentMatcherBuilder<*>.p(block: ContentMatcherBuilder.() -> Unit) = + composite { + block() + check { assertThat(this::style).contains(TextStyle.Paragraph) } + } + +fun ContentMatcherBuilder<*>.link(block: ContentMatcherBuilder.() -> Unit) = composite(block) + +fun ContentMatcherBuilder<*>.table(block: ContentMatcherBuilder.() -> Unit) = composite(block) + +fun ContentMatcherBuilder<*>.platformHinted(block: ContentMatcherBuilder.() -> Unit) = + composite { group(block) } + +fun ContentMatcherBuilder<*>.br() = node() fun ContentMatcherBuilder<*>.somewhere(block: ContentMatcherBuilder<*>.() -> Unit) { skipAllNotMatching() diff --git a/test-tools/src/main/kotlin/matchers/content/contentMatchers.kt b/test-tools/src/main/kotlin/matchers/content/contentMatchers.kt index c4400646..2284c88d 100644 --- a/test-tools/src/main/kotlin/matchers/content/contentMatchers.kt +++ b/test-tools/src/main/kotlin/matchers/content/contentMatchers.kt @@ -5,17 +5,24 @@ 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( +open class NodeMatcher( val kclass: KClass, val assertions: T.() -> Unit = {} -): MatcherElement() { +) : MatcherElement() { open fun tryMatch(node: ContentNode) { - assertions(kclass.cast(node)) + kclass.safeCast(node)?.apply { + try { + assertions() + } catch (e: AssertionError) { + throw MatcherError(e.message.orEmpty(), this@NodeMatcher, cause = e) + } + } ?: throw MatcherError("Expected ${kclass.simpleName} but got: $node", this) } } @@ -24,7 +31,7 @@ class CompositeMatcher( private val children: List, assertions: T.() -> Unit = {} ) : NodeMatcher(kclass, assertions) { - private val normalizedChildren: List by lazy { + internal val normalizedChildren: List by lazy { children.fold(listOf()) { acc, e -> when { acc.lastOrNull() is Anything && e is Anything -> acc @@ -37,7 +44,9 @@ class CompositeMatcher( override fun tryMatch(node: ContentNode) { super.tryMatch(node) - kclass.cast(node).children.fold(normalizedChildren.pop()) { acc, n -> acc.next(n) }.finish() + kclass.cast(node).children.asSequence() + .filter { it !is ContentText || it.text.isNotBlank() } + .fold(FurtherSiblings(normalizedChildren, this).pop()) { acc, n -> acc.next(n) }.finish() } } @@ -48,22 +57,27 @@ private sealed class MatchWalkerState { abstract fun finish() } -private class TextMatcherState(val text: String, val rest: List) : MatchWalkerState() { +private class TextMatcherState( + val text: String, + val rest: FurtherSiblings, + val anchor: TextMatcher +) : MatchWalkerState() { override fun next(node: ContentNode): MatchWalkerState { - node as ContentText + node as? ContentText ?: throw MatcherError("Expected text: \"$text\" but got $node", anchor) + val trimmed = node.text.trim() 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}\"") + 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 AssertionError("\"$text\" was not found" + rest.messageEnd) + override fun finish() = throw MatcherError("\"$text\" was not found" + rest.messageEnd, anchor) } -private object EmptyMatcherState : MatchWalkerState() { +private class EmptyMatcherState(val parent: CompositeMatcher<*>) : MatchWalkerState() { override fun next(node: ContentNode): MatchWalkerState { - throw AssertionError("Unexpected $node") + throw MatcherError("Unexpected $node", parent, anchorAfter = true) } override fun finish() = Unit @@ -71,14 +85,15 @@ private object EmptyMatcherState : MatchWalkerState() { private class NodeMatcherState( val matcher: NodeMatcher<*>, - val rest: List + val rest: FurtherSiblings ) : 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) + override fun finish() = + throw MatcherError("Content of type ${matcher.kclass} was not found" + rest.messageEnd, matcher) } private class SkippingMatcherState( @@ -89,17 +104,64 @@ private class SkippingMatcherState( override fun finish() = innerState.finish() } -private fun List.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 class FurtherSiblings(val list: List, 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 val List.messageEnd: String - get() = filter { it !is Anything } - .count().takeIf { it > 0 } - ?.let { " and $it further matchers were not satisfied" } ?: "" +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 -- cgit From 5451627eb0cf8d95dafd23e96665e062ef023d75 Mon Sep 17 00:00:00 2001 From: Kamil Doległo Date: Mon, 18 May 2020 17:14:23 +0200 Subject: Add test utils for ContentDivergent and fix the tests --- .../documentables/DefaultPageCreator.kt | 1 - .../documentables/PageContentBuilder.kt | 33 +- .../kotlin/content/params/ContentForParamsTest.kt | 341 +++++++++++++-------- .../content/seealso/ContentForSeeAlsoTest.kt | 238 ++++++++------ .../test/kotlin/renderers/html/DivergentTest.kt | 28 +- plugins/base/src/test/kotlin/utils/contentUtils.kt | 68 ++-- .../kotlin/matchers/content/ContentMatchersDsl.kt | 17 +- 7 files changed, 446 insertions(+), 280 deletions(-) (limited to 'test-tools') diff --git a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt index b0687a06..4252de3b 100644 --- a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt +++ b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt @@ -207,7 +207,6 @@ open class DefaultPageCreator( } } } - } } diff --git a/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt b/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt index 2f04e2a0..9c80a9ea 100644 --- a/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt +++ b/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt @@ -212,14 +212,14 @@ open class PageContentBuilder( styles: Set