Description
JUnit 4's @Test(timeout = N) runs the test body in a daemon thread via FailOnTimeout — it wraps the test in a FutureTask and calls FutureTask.get(timeout, timeUnit). When the timeout elapses the runner thread unblocks immediately and reports failure. A hanging test never blocks the suite.
UpdateTestAnnotation migrates this to:
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
@Timeout without an explicit threadMode inherits ThreadMode.INFERRED, which resolves to SAME_THREAD by default. In SAME_THREAD mode the test runs synchronously in the runner thread. A ScheduledExecutorService sends an interrupt after N ms, but if the test body does not respond to interruption (infinite loop, lock contention without a blocking call) delegate.proceed() never returns and the runner thread blocks indefinitely.
End-to-end verification
Using the real JUnit Platform Launcher with two identical infinite-loop test methods — the only difference is threadMode:
-- CURRENT recipe output (@Timeout with SAME_THREAD — the bug) --
RESULT : HUNG — runner still blocked after 3000ms
A non-interruptible loop ignores SAME_THREAD's interrupt signal.
In CI this causes the build to hang indefinitely.
-- CORRECT recipe output (@Timeout with SEPARATE_THREAD — the fix) --
RESULT : completed in 319ms
Tests run=1 failed=1
Failure : hang() timed out after 300 milliseconds
Why SEPARATE_THREAD is safe for migrated tests
JUnit 4's FailOnTimeout was already running these tests in a separate daemon thread. Any test that passed under @Test(timeout=N) was already thread-isolated. Emitting SEPARATE_THREAD restores that exact model and cannot introduce threading issues that did not already exist in JUnit 4.
Expected output
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
Affected recipe
UpdateTestAnnotation
Description
JUnit 4's
@Test(timeout = N)runs the test body in a daemon thread viaFailOnTimeout— it wraps the test in aFutureTaskand callsFutureTask.get(timeout, timeUnit). When the timeout elapses the runner thread unblocks immediately and reports failure. A hanging test never blocks the suite.UpdateTestAnnotationmigrates this to:@Timeoutwithout an explicitthreadModeinheritsThreadMode.INFERRED, which resolves toSAME_THREADby default. InSAME_THREADmode the test runs synchronously in the runner thread. AScheduledExecutorServicesends an interrupt after N ms, but if the test body does not respond to interruption (infinite loop, lock contention without a blocking call)delegate.proceed()never returns and the runner thread blocks indefinitely.End-to-end verification
Using the real JUnit Platform Launcher with two identical infinite-loop test methods — the only difference is
threadMode:Why
SEPARATE_THREADis safe for migrated testsJUnit 4's
FailOnTimeoutwas already running these tests in a separate daemon thread. Any test that passed under@Test(timeout=N)was already thread-isolated. EmittingSEPARATE_THREADrestores that exact model and cannot introduce threading issues that did not already exist in JUnit 4.Expected output
Affected recipe
UpdateTestAnnotation