Skip to content

Commit be7bcd3

Browse files
runningcodeclaude
andcommitted
test(core): Check that the shutdown hook delivers what an app reported before exiting
The console samples' system tests ran each sample in its own JVM, asserted that it exited with 0 within 30 seconds, and relied on the shutdown hook to send everything, because the samples never flush or close Sentry. The in-process end-to-end tests flush explicitly, so neither the clean exit nor the hook's flush was tested any more. Run a small app in a child JVM that returns from main without flushing, and check that it exits with 0 and that its event, log and metric arrive. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent dfc36af commit be7bcd3

2 files changed

Lines changed: 69 additions & 4 deletions

File tree

‎sentry-test-support/src/main/kotlin/io/sentry/test/ChildJvmApp.kt‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import kotlin.concurrent.thread
1111

1212
/**
1313
* An app running in a child JVM, for apps that must not share the test's JVM, such as one with the
14-
* OpenTelemetry agent attached.
14+
* OpenTelemetry agent attached or one whose exit is under test.
1515
*
16-
* The app writes the port it listens on to the file named by the `PORTFILE` environment variable,
17-
* which Spring Boot's `WebServerPortFileWriter` reads too. The app's output goes to the test's
18-
* standard output as it arrives, so Gradle's test report shows it with the test that was running.
16+
* A server app writes the port it listens on to the file named by the `PORTFILE` environment
17+
* variable, which Spring Boot's `WebServerPortFileWriter` reads too. The app's output goes to the
18+
* test's standard output as it arrives, so Gradle's test report shows it with the test that was
19+
* running.
1920
*/
2021
class ChildJvmApp private constructor(private val process: Process, private val workDir: File) :
2122
Closeable {
@@ -37,6 +38,14 @@ class ChildJvmApp private constructor(private val process: Process, private val
3738
}
3839
}
3940

41+
/** Waits for the app to exit on its own and returns its exit code. */
42+
fun awaitExit(timeoutSeconds: Long = 30): Int {
43+
check(process.waitFor(timeoutSeconds, TimeUnit.SECONDS)) {
44+
"App did not exit within ${timeoutSeconds}s"
45+
}
46+
return process.exitValue()
47+
}
48+
4049
override fun close() {
4150
process.destroyForcibly().waitFor()
4251
workDir.deleteRecursively()
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.sentry
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import io.sentry.test.ChildJvmApp
5+
import io.sentry.test.MockSentryServer
6+
import io.sentry.test.event
7+
import io.sentry.test.hasLogWithBody
8+
import io.sentry.test.hasMetric
9+
import io.sentry.test.logs
10+
import io.sentry.test.metrics
11+
import kotlin.test.AfterTest
12+
import kotlin.test.Test
13+
14+
/**
15+
* Runs an app in its own JVM that returns from `main` without flushing or closing Sentry, as most
16+
* apps do, so only the shutdown hook can deliver what it reported.
17+
*/
18+
class ShutdownHookEndToEndTest {
19+
private val server = MockSentryServer()
20+
21+
@AfterTest
22+
fun teardown() {
23+
server.close()
24+
}
25+
26+
@Test
27+
fun `the shutdown hook delivers what an app reported before it exited`() {
28+
ChildJvmApp.start(
29+
mainClass = ExitWithoutFlushApp::class.java.name,
30+
classpath = System.getProperty("java.class.path"),
31+
args = listOf(server.dsn),
32+
)
33+
.use { app ->
34+
// A timeout here means a thread the SDK started kept the JVM from exiting.
35+
assertThat(app.awaitExit()).isEqualTo(0)
36+
}
37+
38+
server.awaitEnvelope { it.event()?.message?.formatted == "Reported before exit" }
39+
server.awaitEnvelope { it.logs()?.hasLogWithBody("Logged before exit") == true }
40+
server.awaitEnvelope { it.metrics()?.hasMetric("exitMetric", "counter", 1.0) == true }
41+
}
42+
}
43+
44+
/** Reports an event, a log and a metric, then returns without flushing or closing Sentry. */
45+
object ExitWithoutFlushApp {
46+
@JvmStatic
47+
fun main(args: Array<String>) {
48+
Sentry.init {
49+
it.dsn = args[0]
50+
it.logs.isEnabled = true
51+
}
52+
Sentry.captureMessage("Reported before exit")
53+
Sentry.logger().info("Logged before exit")
54+
Sentry.metrics().count("exitMetric")
55+
}
56+
}

0 commit comments

Comments
 (0)