|
2 | 2 |
|
3 | 3 | import org.junit.jupiter.api.BeforeEach; |
4 | 4 | import org.junit.jupiter.api.Test; |
| 5 | +import org.junit.jupiter.api.io.TempDir; |
5 | 6 |
|
| 7 | +import java.io.IOException; |
6 | 8 | import java.lang.instrument.Instrumentation; |
| 9 | +import java.nio.charset.StandardCharsets; |
| 10 | +import java.nio.file.Files; |
| 11 | +import java.nio.file.Path; |
7 | 12 |
|
| 13 | +import static org.assertj.core.api.Assertions.assertThat; |
8 | 14 | import static org.assertj.core.api.Assertions.assertThatCode; |
9 | 15 |
|
10 | 16 | /** |
@@ -45,4 +51,46 @@ void premainDoesNotThrowOnConfigIdWithoutCredentials() { |
45 | 51 | assertThatCode(() -> PreMain.premain("config-id=some-config", null)) |
46 | 52 | .doesNotThrowAnyException(); |
47 | 53 | } |
| 54 | + |
| 55 | + /** |
| 56 | + * Exercises the top-level {@code try}/{@code catch} in {@link PreMain#premain(String, Instrumentation)} that routes |
| 57 | + * into {@code logStartupFailure} — the safety net for failures that surface <em>after</em> options have been parsed |
| 58 | + * (and are therefore not covered by the {@code AgentOption*Exception} catches inside {@code startProfiler}). |
| 59 | + * <p> |
| 60 | + * The trick: valid options pass parsing, then a {@code null} {@link Instrumentation} forces JaCoCo's runtime setup |
| 61 | + * to dereference {@code null} (first at {@code AgentModule.openPackage(inst, …)} inside |
| 62 | + * {@code JaCoCoPreMain.createRuntime}). The resulting {@link NullPointerException} is a stand-in for real-world |
| 63 | + * post-parse failures — e.g. a Jetty {@code BindException} when {@code http-server-port} is taken, or an |
| 64 | + * {@code UploaderException} during uploader construction — which would bubble up the same way. In production the |
| 65 | + * JVM always supplies a non-null {@code Instrumentation}; passing {@code null} here is only a cheap way to stage |
| 66 | + * the failure without a running JVM agent attach. |
| 67 | + * <p> |
| 68 | + * A custom {@code logging-config} points logback at a file inside {@code tempDir} so we can read back the emitted |
| 69 | + * error event. A plain {@link ch.qos.logback.core.read.ListAppender} attached in the test would be detached again |
| 70 | + * by the {@code LoggerContext.reset()} that {@code LoggingUtils.reconfigureLoggerContext} performs during option |
| 71 | + * parsing. |
| 72 | + */ |
| 73 | + @Test |
| 74 | + void premainLogsFailureWhenJaCoCoSetupThrows(@TempDir Path tempDir) throws IOException { |
| 75 | + Path logFile = tempDir.resolve("agent.log"); |
| 76 | + Path logbackConfig = tempDir.resolve("logback.xml"); |
| 77 | + Files.write(logbackConfig, ("<configuration>\n" |
| 78 | + + " <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n" |
| 79 | + + " <file>" + logFile.toAbsolutePath() + "</file>\n" |
| 80 | + + " <encoder><pattern>%-5level %logger - %msg%n%ex</pattern></encoder>\n" |
| 81 | + + " </appender>\n" |
| 82 | + + " <root level=\"INFO\"><appender-ref ref=\"FILE\"/></root>\n" |
| 83 | + + "</configuration>\n").getBytes(StandardCharsets.UTF_8)); |
| 84 | + |
| 85 | + assertThatCode(() -> PreMain.premain( |
| 86 | + "logging-config=" + logbackConfig.toAbsolutePath() + ",includes=com.example.*", null)) |
| 87 | + .doesNotThrowAnyException(); |
| 88 | + |
| 89 | + assertThat(logFile).exists(); |
| 90 | + String log = new String(Files.readAllBytes(logFile), StandardCharsets.UTF_8); |
| 91 | + assertThat(log) |
| 92 | + .contains("ERROR") |
| 93 | + .contains("failed to start up") |
| 94 | + .contains("NullPointerException"); |
| 95 | + } |
48 | 96 | } |
0 commit comments