Skip to content

Commit 29d5cbc

Browse files
authored
Handle InvocationTargetException exception explicitly in DokkaBootstrap (#4257)
Fixes #4235 Show better error messages in DGPv2, as Gradle will not show the cause of `InvocationTargetException` without `--stacktrace` flag
1 parent e572fc9 commit 29d5cbc

File tree

4 files changed

+199
-33
lines changed

4 files changed

+199
-33
lines changed

dokka-runners/dokka-gradle-plugin/src/classicMain/kotlin/dokkaBootstrapFactory.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
package org.jetbrains.dokka.gradle
66

77
import org.gradle.api.artifacts.Configuration
8-
import org.jetbrains.dokka.gradle.internal.DokkaBootstrap
98
import org.jetbrains.dokka.DokkaBootstrap
9+
import org.jetbrains.dokka.gradle.internal.DokkaBootstrapProxy
1010
import kotlin.reflect.KClass
1111

1212

1313
@Deprecated(DOKKA_V1_DEPRECATION_MESSAGE)
1414
@Suppress("DeprecatedCallableAddReplaceWith")
1515
fun DokkaBootstrap(configuration: Configuration, bootstrapClass: KClass<out DokkaBootstrap>): DokkaBootstrap {
16-
return DokkaBootstrap(
16+
return DokkaBootstrapProxy(
1717
classpath = configuration.resolve(),
1818
bootstrapClass = bootstrapClass,
1919
)

dokka-runners/dokka-gradle-plugin/src/main/kotlin/internal/dokkaBootstrapFactory.kt

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,59 @@ import org.jetbrains.dokka.DokkaBootstrapImpl
99
import org.jetbrains.dokka.DokkaConfiguration
1010
import org.jetbrains.dokka.toCompactJsonString
1111
import java.io.File
12+
import java.lang.reflect.InvocationTargetException
1213
import java.net.URLClassLoader
1314
import java.util.concurrent.atomic.AtomicReference
1415
import java.util.function.BiConsumer
1516
import kotlin.reflect.KClass
1617

17-
internal fun DokkaBootstrap(classpath: Set<File>, bootstrapClass: KClass<out DokkaBootstrap>): DokkaBootstrap {
18-
val runtimeClassLoader = URLClassLoader(
19-
classpath.map { it.toURI().toURL() }.toTypedArray(),
20-
ClassLoader.getSystemClassLoader().parent
18+
internal class DokkaBootstrapProxy
19+
// this constructor is used in tests (DokkaBootstrapTest)
20+
internal constructor(
21+
runtimeClassLoader: ClassLoader,
22+
bootstrapClass: KClass<out DokkaBootstrap>
23+
) : DokkaBootstrap {
24+
constructor(
25+
classpath: Set<File>,
26+
bootstrapClass: KClass<out DokkaBootstrap>
27+
) : this(
28+
runtimeClassLoader = URLClassLoader(
29+
classpath.map { it.toURI().toURL() }.toTypedArray(),
30+
ClassLoader.getSystemClassLoader().parent
31+
),
32+
bootstrapClass = bootstrapClass
2133
)
2234

23-
val runtimeClassloaderBootstrapClass = runtimeClassLoader.loadClass(bootstrapClass.qualifiedName)
24-
val runtimeClassloaderBootstrapInstance = runtimeClassloaderBootstrapClass.constructors.first().newInstance()
35+
private val runtimeClassloaderBootstrapClass = runtimeClassLoader.loadClass(bootstrapClass.qualifiedName)
36+
private val runtimeClassloaderBootstrapInstance =
37+
runtimeClassloaderBootstrapClass.constructors.first().newInstance()
2538

26-
return object : DokkaBootstrap {
27-
override fun configure(
28-
serializedConfigurationJSON: String,
29-
logger: BiConsumer<String, String>
30-
) {
31-
val configureMethod = runtimeClassloaderBootstrapClass.getMethod(
32-
"configure",
33-
String::class.java,
34-
BiConsumer::class.java // Use java.util.function.BiConsumer from *your* loader
35-
)
36-
configureMethod.invoke(
37-
runtimeClassloaderBootstrapInstance,
38-
serializedConfigurationJSON,
39-
logger
40-
)
41-
}
39+
private fun invokeMethod(
40+
name: String,
41+
parameterTypes: Array<Class<*>>,
42+
args: Array<Any?>
43+
): Any? = try {
44+
runtimeClassloaderBootstrapClass
45+
.getMethod(name, *parameterTypes)
46+
.invoke(runtimeClassloaderBootstrapInstance, *args)
47+
} catch (e: InvocationTargetException) {
48+
throw e.targetException
49+
}
4250

43-
override fun generate() {
44-
val generateMethod = runtimeClassloaderBootstrapClass.getMethod(
45-
"generate",
46-
)
47-
generateMethod.invoke(
48-
runtimeClassloaderBootstrapInstance
49-
)
50-
}
51+
override fun configure(serializedConfigurationJSON: String, logger: BiConsumer<String, String>) {
52+
invokeMethod(
53+
name = "configure",
54+
parameterTypes = arrayOf(String::class.java, BiConsumer::class.java),
55+
args = arrayOf(serializedConfigurationJSON, logger)
56+
)
57+
}
58+
59+
override fun generate() {
60+
invokeMethod(
61+
name = "generate",
62+
parameterTypes = emptyArray(),
63+
args = emptyArray()
64+
)
5165
}
5266
}
5367

@@ -56,7 +70,7 @@ internal fun generateDocumentationViaDokkaBootstrap(
5670
dokkaConfiguration: DokkaConfiguration,
5771
logger: BiConsumer<String, String>
5872
) {
59-
DokkaBootstrap(dokkaClasspath, DokkaBootstrapImpl::class).apply {
73+
DokkaBootstrapProxy(dokkaClasspath, DokkaBootstrapImpl::class).apply {
6074
configure(dokkaConfiguration.toCompactJsonString(), logger)
6175
val uncaughtExceptionHolder = AtomicReference<Throwable?>()
6276
/**
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2014-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package org.jetbrains.dokka.gradle.internal
6+
7+
import org.jetbrains.dokka.DokkaBootstrap
8+
import java.util.function.BiConsumer
9+
import kotlin.test.Test
10+
import kotlin.test.assertEquals
11+
import kotlin.test.assertFailsWith
12+
13+
// should be top-level
14+
private class FailingTestBootstrap : DokkaBootstrap {
15+
override fun configure(serializedConfigurationJSON: String, logger: BiConsumer<String, String>) {
16+
throw TestException("Test Exception Message: configure", Exception("Cause Exception Message: configure"))
17+
}
18+
19+
override fun generate() {
20+
throw TestException("Test Exception Message: generate", Exception("Cause Exception Message: generate"))
21+
}
22+
}
23+
24+
private class TestException(message: String, cause: Throwable?) : Exception(message, cause)
25+
26+
class DokkaBootstrapProxyTest {
27+
@Test
28+
fun `exception thrown in DokkaBootstrap is not wrapped inside InvocationTargetException`() {
29+
val proxy = DokkaBootstrapProxy(
30+
FailingTestBootstrap::class.java.classLoader,
31+
FailingTestBootstrap::class
32+
)
33+
34+
assertFailsWith<TestException> {
35+
proxy.configure("") { _, _ -> }
36+
}.also { exception ->
37+
assertEquals("Test Exception Message: configure", exception.message)
38+
assertEquals("Cause Exception Message: configure", exception.cause?.message)
39+
}
40+
assertFailsWith<TestException> {
41+
proxy.generate()
42+
}.also { exception ->
43+
assertEquals("Test Exception Message: generate", exception.message)
44+
assertEquals("Cause Exception Message: generate", exception.cause?.message)
45+
}
46+
}
47+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package org.jetbrains.dokka.gradle
6+
7+
import io.kotest.core.spec.style.FunSpec
8+
import io.kotest.matchers.string.shouldContain
9+
import org.jetbrains.dokka.gradle.internal.DokkaConstants.DOKKA_VERSION
10+
import org.jetbrains.dokka.gradle.utils.*
11+
12+
class DokkaGeneratorFailureTest : FunSpec({
13+
context("DokkaGenerator failure:") {
14+
val project = createProject()
15+
16+
test("expect failure message from checkers to be shown by default") {
17+
project.runner
18+
.addArguments(
19+
":dokkaGenerateModuleHtml",
20+
"--rerun",
21+
)
22+
.buildAndFail {
23+
output shouldContain "Pre-generation validity check failed: Some failure"
24+
}
25+
26+
project.runner
27+
.addArguments(
28+
":dokkaGeneratePublicationHtml",
29+
"--rerun",
30+
)
31+
.buildAndFail {
32+
output shouldContain "Pre-generation validity check failed: Some failure"
33+
}
34+
}
35+
}
36+
})
37+
38+
private fun createProject(): GradleProjectTest = gradleKtsProjectTest("dokka-generator-failure") {
39+
buildGradleKts = """
40+
|import org.jetbrains.dokka.gradle.tasks.*
41+
|
42+
|plugins {
43+
| kotlin("jvm") version embeddedKotlinVersion
44+
| id("org.jetbrains.dokka") version "$DOKKA_VERSION"
45+
|}
46+
|
47+
|dependencies {
48+
| dokkaPlugin(project(":dokka-test-plugin"))
49+
|}
50+
|
51+
""".trimMargin()
52+
53+
settingsGradleKts += """
54+
|include(":dokka-test-plugin")
55+
|
56+
""".trimMargin()
57+
58+
createKotlinFile("src/main/kotlin/Foo.kt", "class Foo")
59+
60+
dir("dokka-test-plugin") {
61+
buildGradleKts = """
62+
|plugins {
63+
| kotlin("jvm")
64+
|}
65+
|
66+
|dependencies {
67+
| compileOnly("org.jetbrains.dokka:dokka-core:$DOKKA_VERSION")
68+
| compileOnly("org.jetbrains.dokka:dokka-base:$DOKKA_VERSION")
69+
|}
70+
""".trimMargin()
71+
72+
createKotlinFile(
73+
"src/main/kotlin/DokkaTestPlugin.kt", """
74+
|package dokkatest
75+
|
76+
|import org.jetbrains.dokka.*
77+
|import org.jetbrains.dokka.plugability.*
78+
|import org.jetbrains.dokka.validity.*
79+
|
80+
|class DokkaTestPlugin : DokkaPlugin() {
81+
|
82+
| @DokkaPluginApiPreview
83+
| override fun pluginApiPreviewAcknowledgement() = PluginApiPreviewAcknowledgement
84+
|
85+
| val failingPreGenerationChecker by extending {
86+
| CoreExtensions.preGenerationCheck providing ::FailingPreGenerationChecker
87+
| }
88+
|}
89+
|
90+
|class FailingPreGenerationChecker(private val context: DokkaContext) : PreGenerationChecker {
91+
| override fun invoke(): PreGenerationCheckerOutput = PreGenerationCheckerOutput(
92+
| result = false,
93+
| messages = listOf("Some failure")
94+
| )
95+
|}
96+
|
97+
""".trimMargin()
98+
)
99+
100+
createFile(
101+
"src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin",
102+
"dokkatest.DokkaTestPlugin",
103+
)
104+
}
105+
}

0 commit comments

Comments
 (0)