slawekjaranowski commented on issue #3460:
URL:
https://github.com/apache/maven-surefire/issues/3460#issuecomment-5583344061
We hit the same regression from a different direction. Two things to add: a
minimal deterministic reproducer, and a second, much more confusing failure
mode.
### Minimal reproducer
No Jakarta Validation, no `ForkJoinPool`, no race — just a thread with a
`null` context class loader writing one line:
```java
import org.junit.jupiter.api.Test;
class NullTcclPrintTest {
@Test
void printFromThreadWithNullContextClassLoader() throws Exception {
Throwable[] failure = new Throwable[1];
Thread t = new Thread(() -> {
try {
System.err.println("hello from null-TCCL thread");
} catch (Throwable e) {
failure[0] = e;
}
});
t.setContextClassLoader(null);
t.start();
t.join();
if (failure[0] != null) {
throw new AssertionError("println failed", failure[0]);
}
}
}
```
Plain JUnit 5 project, nothing else needed. Fails on 3.6.0, passes on 3.5.6:
```
Caused by: java.lang.ExceptionInInitializerError
at
org.apache.maven.surefire.api.report.StackTraceProvider.getStack(StackTraceProvider.java:107)
at
org.apache.maven.surefire.api.report.TestOutputReportEntry.<init>(TestOutputReportEntry.java:52)
at
org.apache.maven.surefire.api.report.TestOutputReportEntry.<init>(TestOutputReportEntry.java:63)
at
org.apache.maven.surefire.api.report.TestOutputReportEntry.stdErrln(TestOutputReportEntry.java:132)
at
org.apache.maven.surefire.api.report.ConsoleOutputCapture$ForwardingPrintStream.println(ConsoleOutputCapture.java:116)
at
NullTcclPrintTest.lambda$printFromThreadWithNullContextClassLoader$0(NullTcclPrintTest.java:10)
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.lang.NullPointerException: Cannot invoke
"java.lang.ClassLoader.loadClass(String)" because "classLoader" is null
at
org.apache.maven.surefire.api.util.ReflectionUtils.tryLoadClass(ReflectionUtils.java:152)
at
org.apache.maven.surefire.api.report.StackWalkerStrategy.<clinit>(StackWalkerStrategy.java:60)
```
Reproduces with `forkCount=1` and `forkCount=0`, and with
`useSystemClassLoader` both `true` and `false`.
### Second failure mode: dynamically attached JVM agents
This one does not surface as a `StackWalkerStrategy` error at all, which
makes it hard to trace back here.
Our case is a Reactor BlockHound test (`BlockHound.install()` in
`@BeforeAll`) on JDK 21:
1. `BlockHound.install()` attaches a Byte Buddy agent to the running JVM.
2. On JDK 9+ the JVM prints `WARNING: A Java agent has been loaded
dynamically ...` from
`sun.instrument.InstrumentationImpl.<init>`, running on the **`Attach
Listener`** thread — whose
context class loader is `null` by design.
3. That write goes through `ConsoleOutputCapture$ForwardingPrintStream` and
throws
`ExceptionInInitializerError` as above.
4. The exception escapes the `InstrumentationImpl` constructor, so the JVM
aborts agent loading:
```
*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with
message
call constructor on InstrumentationImpl failed at
src/java.instrument/share/native/libinstrument/JPLISAgent.c line: 526
*** java.lang.instrument ASSERTION FAILED ***: "success" with message
createInstrumentationImpl failed at
src/java.instrument/share/native/libinstrument/InvocationAdapter.c line: 425
```
5. What the user actually sees is Byte Buddy's downstream error, which
points at the agent and the JDK,
not at surefire:
```
java.lang.IllegalStateException: Could not self-attach to current VM using
external process
at
net.bytebuddy.agent.ByteBuddyAgent.installExternal(ByteBuddyAgent.java:710)
```
So on 3.6.0 any test that attaches an agent at runtime (BlockHound,
`ByteBuddyAgent.install()`,
profilers, Mockito's inline mock maker in setups where it self-attaches) can
break, with no hint that
the report layer is involved. It took an `-Xlog:exceptions` dump to find the
`Attach Listener` thread
in the middle of it.
### On the fix
Agreed on catching `RuntimeException` in `tryLoadClass`. Two additions worth
considering:
- `StackWalkerStrategy.<clinit>` does not need the TCCL at all —
`java.lang.StackWalker` is a JDK class,
so `StackWalkerStrategy.class.getClassLoader()` (or
`ClassLoader.getPlatformClassLoader()`) removes the
dependency on whatever the current thread happens to carry.
- `StackTraceProvider.getStack()` and the rest of the `ConsoleOutputCapture`
path could be made
exception-proof independently: failing to collect diagnostic stack data
should degrade to an empty
stack, never propagate into user code that is only printing a line. As the
agent case shows, an
exception thrown out of a `println` can break things far outside
surefire's own reporting.
### Workaround
For the agent-attach case, suppressing the JDK warning is enough, since
nothing is then printed from the
`Attach Listener` thread:
```xml
<argLine>-XX:+EnableDynamicAgentLoading</argLine>
```
Otherwise pinning maven-surefire-plugin to 3.5.6 works.
Environment: maven-surefire-plugin 3.6.0, Temurin 21.0.12+8-LTS, macOS
26.6.2 (aarch64).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]