Skip to content

Commit 9118afd

Browse files
committed
test: add Kotlin smoke test for checked exception handling
Adds a Maven-compiled Kotlin test source set to operator-framework-core and a smoke test verifying that a checked (non-RuntimeException) Exception thrown from a Kotlin DependentResource is properly caught and reported by the workflow executor, so that retries are triggered as expected. Kotlin does not have checked exceptions, so Kotlin code can throw a checked Exception from an overridden method without declaring it, even though the Java DependentResource#reconcile signature does not declare `throws Exception`. Before #2965 this exception would not have been caught by NodeExecutor, since it only handled RuntimeException, silently swallowing the error and preventing retries. Closes #2967
1 parent 67c47c7 commit 9118afd

2 files changed

Lines changed: 131 additions & 0 deletions

File tree

operator-framework-core/pom.xml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
<name>Operator SDK - Framework - Core</name>
3131
<description>Core framework for implementing Kubernetes operators</description>
3232

33+
<properties>
34+
<!-- Used only for test sources, to verify that the framework works properly with Kotlin. -->
35+
<kotlin.version>2.4.10</kotlin.version>
36+
</properties>
37+
3338
<dependencies>
3439
<dependency>
3540
<groupId>io.github.java-diff-utils</groupId>
@@ -101,6 +106,13 @@
101106
<artifactId>kube-api-test-client-inject</artifactId>
102107
<scope>test</scope>
103108
</dependency>
109+
<dependency>
110+
<!-- Test-only, used to verify JOSDK works properly when used from Kotlin. -->
111+
<groupId>org.jetbrains.kotlin</groupId>
112+
<artifactId>kotlin-stdlib</artifactId>
113+
<version>${kotlin.version}</version>
114+
<scope>test</scope>
115+
</dependency>
104116
</dependencies>
105117

106118
<build>
@@ -147,6 +159,37 @@
147159
</execution>
148160
</executions>
149161
</plugin>
162+
<plugin>
163+
<!--
164+
Compiles the Kotlin test sources under src/test/kotlin, used only to verify that JOSDK
165+
works properly when used from Kotlin (see https://github.com/operator-framework/java-operator-sdk/issues/2967).
166+
Bound to process-test-sources so the compiled Kotlin classes are on the classpath before
167+
the regular Java test sources are compiled.
168+
-->
169+
<groupId>org.jetbrains.kotlin</groupId>
170+
<artifactId>kotlin-maven-plugin</artifactId>
171+
<version>${kotlin.version}</version>
172+
<configuration>
173+
<jvmTarget>${java.version}</jvmTarget>
174+
</configuration>
175+
<executions>
176+
<execution>
177+
<id>kotlin-test-compile</id>
178+
<goals>
179+
<goal>test-compile</goal>
180+
</goals>
181+
<phase>process-test-sources</phase>
182+
<configuration>
183+
<sourceDirs>
184+
<sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
185+
<!-- Not compiled by Kotlin, only needed so the Kotlin sources can reference the
186+
existing Java test classes (e.g. TestCustomResource). -->
187+
<sourceDir>${project.basedir}/src/test/java</sourceDir>
188+
</sourceDirs>
189+
</configuration>
190+
</execution>
191+
</executions>
192+
</plugin>
150193
</plugins>
151194
</build>
152195
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.processing.dependent.workflow
17+
18+
import io.javaoperatorsdk.operator.AggregatedOperatorException
19+
import io.javaoperatorsdk.operator.api.reconciler.Context
20+
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource
21+
import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult
22+
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext
23+
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever
24+
import io.javaoperatorsdk.operator.sample.simple.TestCustomResource
25+
import java.util.concurrent.Executors
26+
import org.assertj.core.api.Assertions.assertThat
27+
import org.junit.jupiter.api.Test
28+
import org.junit.jupiter.api.assertThrows
29+
import org.mockito.Mockito.mock
30+
import org.mockito.Mockito.`when`
31+
32+
/**
33+
* Smoke test verifying that JOSDK works properly when used from Kotlin.
34+
*
35+
* Unlike Java, Kotlin does not distinguish between checked and unchecked exceptions, so a Kotlin
36+
* [DependentResource] can throw a plain [java.lang.Exception] from `reconcile` without declaring
37+
* it, even though the Java `DependentResource.reconcile` method does not declare `throws
38+
* Exception`. Before https://github.com/operator-framework/java-operator-sdk/pull/2965 such an
39+
* exception was not caught by [NodeExecutor], since it only handled [RuntimeException], so it
40+
* would not have been recorded as an error and retries would not have been triggered. This test
41+
* makes sure such an exception is properly caught and reported, see
42+
* https://github.com/operator-framework/java-operator-sdk/issues/2967.
43+
*/
44+
class KotlinCheckedExceptionDependentResourceTest {
45+
46+
private class CheckedException(message: String) : Exception(message)
47+
48+
private class ThrowingDependentResource :
49+
DependentResource<String, TestCustomResource> {
50+
override fun reconcile(
51+
primary: TestCustomResource,
52+
context: Context<TestCustomResource>
53+
): ReconcileResult<String> {
54+
throw CheckedException("checked exception thrown from Kotlin")
55+
}
56+
57+
override fun resourceType(): Class<String> = String::class.java
58+
}
59+
60+
@Test
61+
fun checkedExceptionThrownFromKotlinDependentResourceTriggersRetry() {
62+
val dependentResource = ThrowingDependentResource()
63+
64+
val workflow =
65+
WorkflowBuilder<TestCustomResource>()
66+
.addDependentResource(dependentResource)
67+
.withThrowExceptionFurther(false)
68+
.build()
69+
70+
@Suppress("UNCHECKED_CAST")
71+
val context = mock(Context::class.java) as Context<TestCustomResource>
72+
`when`(context.managedWorkflowAndDependentResourceContext())
73+
.thenReturn(mock(ManagedWorkflowAndDependentResourceContext::class.java))
74+
`when`(context.workflowExecutorService).thenReturn(Executors.newCachedThreadPool())
75+
@Suppress("UNCHECKED_CAST")
76+
`when`(context.eventSourceRetriever())
77+
.thenReturn(mock(EventSourceRetriever::class.java) as EventSourceRetriever<TestCustomResource>)
78+
79+
val result = workflow.reconcile(TestCustomResource(), context)
80+
81+
// the checked exception was caught by the workflow executor, not left uncaught, so it is
82+
// reported as an error for the dependent resource, which is what allows JOSDK to retry the
83+
// reconciliation.
84+
assertThat(result.erroredDependents).containsOnlyKeys(dependentResource)
85+
assertThat(result.erroredDependents[dependentResource]).isInstanceOf(CheckedException::class.java)
86+
assertThrows<AggregatedOperatorException> { result.throwAggregateExceptionIfErrorsPresent() }
87+
}
88+
}

0 commit comments

Comments
 (0)