Skip to content

Commit 55e4830

Browse files
committed
add test for KMP project custom shared WasmJS/WasmWASI source set
This recreates the issue reported in #4116
1 parent 38b0fd7 commit 55e4830

File tree

2 files changed

+251
-0
lines changed

2 files changed

+251
-0
lines changed

dokka-runners/dokka-gradle-plugin/src/testFixtures/kotlin/kotestFiles.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ package org.jetbrains.dokka.gradle.utils
66
import com.github.difflib.DiffUtils
77
import com.github.difflib.UnifiedDiffUtils
88
import io.kotest.assertions.fail
9+
import io.kotest.matchers.MatcherResult
10+
import io.kotest.matchers.neverNullMatcher
11+
import io.kotest.matchers.paths.shouldBeAFile
12+
import io.kotest.matchers.paths.shouldExist
13+
import io.kotest.matchers.should
14+
import io.kotest.matchers.shouldNot
915
import java.io.IOException
1016
import java.io.OutputStream
1117
import java.nio.file.Path
@@ -178,3 +184,44 @@ class NullOutputStream : OutputStream() {
178184
// do nothing
179185
}
180186
}
187+
188+
189+
/**
190+
* Assert the given [Path] does not contain the given [text].
191+
*/
192+
fun Path.shouldContainText(
193+
text: String,
194+
ignoreCase: Boolean = false,
195+
) {
196+
this.shouldExist()
197+
this.shouldBeAFile()
198+
this should containText(text, ignoreCase)
199+
}
200+
201+
/**
202+
* Assert the given [Path] does not contain the given [text].
203+
*/
204+
fun Path.shouldNotContainText(
205+
text: String,
206+
ignoreCase: Boolean = false,
207+
) {
208+
this.shouldExist()
209+
this.shouldBeAFile()
210+
this shouldNot containText(text, ignoreCase)
211+
}
212+
213+
private fun containText(
214+
text: String,
215+
ignoreCase: Boolean = false,
216+
) =
217+
neverNullMatcher<Path> { value ->
218+
MatcherResult(
219+
value.useLines { lines ->
220+
lines.any { line ->
221+
line.contains(text, ignoreCase = ignoreCase)
222+
}
223+
},
224+
{ "$value should include '$text'" + if (ignoreCase) " (ignoring case)" else "" },
225+
{ "$value should not include '$text'" + if (ignoreCase) " (ignoring case)" else "" },
226+
)
227+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* Copyright 2014-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
package org.jetbrains.dokka.gradle
5+
6+
import io.kotest.core.spec.style.FunSpec
7+
import io.kotest.inspectors.shouldForAll
8+
import io.kotest.inspectors.shouldForNone
9+
import io.kotest.matchers.paths.shouldBeAFile
10+
import io.kotest.matchers.sequences.shouldNotBeEmpty
11+
import io.kotest.matchers.string.shouldContain
12+
import org.jetbrains.dokka.gradle.internal.DokkaConstants
13+
import org.jetbrains.dokka.gradle.utils.*
14+
import kotlin.io.path.*
15+
16+
/**
17+
* Small reproducer for issue reported by kotlinx-io
18+
* https://github.com/Kotlin/dokka/issues/4116
19+
*/
20+
class KmpSharedWasmTest : FunSpec({
21+
context("given a KMP project with a custom shared Wasm source set") {
22+
val project = initKmpSharedWasmProject()
23+
24+
project.runner
25+
.addArguments(
26+
":dokkaGenerate",
27+
"--stacktrace",
28+
)
29+
.forwardOutput()
30+
.build {
31+
32+
test("expect project builds successfully") {
33+
output shouldContain "BUILD SUCCESSFUL"
34+
}
35+
36+
test("expect no 'unknown class' message in HTML files") {
37+
val htmlFiles = project.projectDir
38+
.resolve("build/dokka/html")
39+
.walk()
40+
.filter { it.isRegularFile() && it.extension == "html" }
41+
42+
htmlFiles.shouldNotBeEmpty()
43+
44+
htmlFiles.forEach { htmlFile ->
45+
htmlFile.relativeTo(project.projectDir).apply {
46+
shouldNotContainText("Error class: unknown class")
47+
shouldNotContainText("ERROR CLASS: Symbol not found")
48+
}
49+
}
50+
}
51+
52+
test("expect all Dokka workers are successful") {
53+
project
54+
.findFiles { it.name == "dokka-worker.log" }
55+
.shouldForAll { dokkaWorkerLog ->
56+
dokkaWorkerLog.shouldBeAFile()
57+
dokkaWorkerLog.useLines { lines ->
58+
lines.shouldForNone { line ->
59+
line.startsWith("[ERROR]") || line.startsWith("[WARN]")
60+
}
61+
}
62+
}
63+
}
64+
}
65+
}
66+
})
67+
68+
private fun initKmpSharedWasmProject(
69+
config: GradleProjectTest.() -> Unit = {},
70+
): GradleProjectTest {
71+
return gradleKtsProjectTest("kmp-shared-wasm") {
72+
73+
settingsGradleKts += """
74+
|
75+
""".trimMargin()
76+
77+
buildGradleKts = """
78+
|plugins {
79+
| kotlin("multiplatform") version embeddedKotlinVersion
80+
| id("org.jetbrains.dokka") version "${DokkaConstants.DOKKA_VERSION}"
81+
|}
82+
|
83+
|@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
84+
|kotlin {
85+
| wasmJs { browser() }
86+
| wasmWasi { nodejs() }
87+
|
88+
| @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
89+
| applyDefaultHierarchyTemplate {
90+
| common {
91+
| group("wasm") {
92+
| withWasmJs()
93+
| withWasmWasi()
94+
| }
95+
| }
96+
| }
97+
|}
98+
|""".trimMargin()
99+
100+
101+
dir("src/commonMain/kotlin") {
102+
createKotlinFile(
103+
"IOException.kt",
104+
"""
105+
|/**
106+
| * Signals about a general issue occurred during I/O operation.
107+
| */
108+
|expect open class IOException : Exception {
109+
| constructor()
110+
| constructor(message: String?)
111+
| constructor(cause: Throwable?)
112+
| constructor(message: String?, cause: Throwable?)
113+
|}
114+
|""".trimMargin()
115+
)
116+
117+
createKotlinFile(
118+
"RawSink.kt",
119+
"""
120+
|/**
121+
| * Receives a stream of bytes. RawSink is a base interface for `kotlinx-io` data receivers.
122+
| *
123+
| * This interface should be implemented to write data wherever it's needed: to the network, storage,
124+
| * or a buffer in memory. Sinks may be layered to transform received data, such as to compress, encrypt, throttle,
125+
| * or add protocol framing.
126+
| *
127+
| * Most application code shouldn't operate on a raw sink directly, but rather on a buffered [Sink] which
128+
| * is both more efficient and more convenient. Use [buffered] to wrap any raw sink with a buffer.
129+
| *
130+
| * Implementors should abstain from throwing exceptions other than those that are documented for RawSink methods.
131+
| *
132+
| * ### Thread-safety guarantees
133+
| *
134+
| * [RawSink] implementations are not required to be thread safe.
135+
| * However, if an implementation provides some thread safety guarantees, it is recommended to explicitly document them.
136+
| */
137+
|expect interface RawSink : AutoCloseable {
138+
| /**
139+
| * Removes [byteCount] bytes from [source] and appends them to this sink.
140+
| *
141+
| * @param source the source to read data from.
142+
| * @param byteCount the number of bytes to write.
143+
| *
144+
| * @throws IllegalArgumentException when the [source]'s size is below [byteCount] or [byteCount] is negative.
145+
| * @throws IllegalStateException when the sink is closed.
146+
| * @throws IOException when some I/O error occurs.
147+
| */
148+
| fun write(source: Buffer, byteCount: Long)
149+
|
150+
| /**
151+
| * Pushes all buffered bytes to their final destination.
152+
| *
153+
| * @throws IllegalStateException when the sink is closed.
154+
| * @throws IOException when some I/O error occurs.
155+
| */
156+
| fun flush()
157+
|
158+
| /**
159+
| * Pushes all buffered bytes to their final destination and releases the resources held by this
160+
| * sink. It is an error to write a closed sink. It is safe to close a sink more than once.
161+
| *
162+
| * @throws IOException when some I/O error occurs.
163+
| */
164+
| override fun close()
165+
|}
166+
|""".trimMargin()
167+
)
168+
169+
createKotlinFile(
170+
"Buffer.kt", """
171+
|interface Buffer
172+
|""".trimMargin()
173+
)
174+
}
175+
dir("src/wasmMain/kotlin") {
176+
createKotlinFile(
177+
"IOException.wasm.kt",
178+
"""
179+
|actual open class IOException : Exception {
180+
| actual constructor() : super()
181+
| actual constructor(message: String?) : super(message)
182+
| actual constructor(cause: Throwable?) : super(cause)
183+
| actual constructor(message: String?, cause: Throwable?) : super(message, cause)
184+
|}
185+
|""".trimMargin()
186+
)
187+
188+
createKotlinFile(
189+
"RawSink.wasm.kt",
190+
"""
191+
|actual interface RawSink : AutoCloseable {
192+
| actual fun write(source: Buffer, byteCount: Long)
193+
|
194+
| actual fun flush()
195+
|
196+
| actual override fun close()
197+
|}
198+
|""".trimMargin()
199+
)
200+
}
201+
202+
config()
203+
}
204+
}

0 commit comments

Comments
 (0)