From a0020db07d439f4fd4a1e3b3f1673d6fce2437d5 Mon Sep 17 00:00:00 2001 From: mdepaula Date: Wed, 5 Aug 2026 12:40:43 -0400 Subject: [PATCH 1/8] Replace @Timeout with assertTimeoutPreemptively to preserve JUnit 4 semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @Test(timeout=N) in JUnit 4 runs the test on a separate thread and preemptively aborts it via FutureTask.get(timeout). @Timeout without threadMode defaults to SAME_THREAD, which only sends Thread.interrupt() — a non-interruptible test (infinite loop, deadlock) blocks the runner thread indefinitely. Replace the @Timeout annotation with assertTimeoutPreemptively(Duration.ofMillis(N), () -> { body }), mirroring the existing @Test(expected=...) → assertThrows pattern. assertTimeoutPreemptively runs the body on a separate thread and preemptively fails, preserving JUnit 4's preemptive-abort semantics. Fixes https://github.com/openrewrite/rewrite-testing-frameworks/issues/1073 --- .../testing/junit5/UpdateTestAnnotation.java | 25 +++++++++++++------ .../junit5/UpdateTestAnnotationTest.java | 22 ++++++++++------ 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotation.java b/src/main/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotation.java index e13765906..1cc23a5ea 100644 --- a/src/main/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotation.java +++ b/src/main/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotation.java @@ -30,7 +30,6 @@ import org.openrewrite.marker.Markup; import org.openrewrite.staticanalysis.LambdaBlockToExpression; -import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Set; @@ -157,17 +156,29 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex } } if (cta.timeout != null) { - m = JavaTemplate.builder("@Timeout(value = #{any(long)}, unit = TimeUnit.MILLISECONDS)") + m = JavaTemplate.builder("org.junit.jupiter.api.function.Executable o = () -> #{};") .javaParser(javaParser) - .imports("org.junit.jupiter.api.Timeout", "java.util.concurrent.TimeUnit") .build() .apply( updateCursor(m), - m.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)), - cta.timeout + m.getCoordinates().replaceBody(), + m.getBody() ); - maybeAddImport("org.junit.jupiter.api.Timeout"); - maybeAddImport("java.util.concurrent.TimeUnit"); + + assert m.getBody() != null; + J.Lambda lambda = (J.Lambda) ((J.VariableDeclarations) m.getBody().getStatements().get(0)) + .getVariables().get(0).getInitializer(); + + assert lambda != null; + + m = JavaTemplate.builder("assertTimeoutPreemptively(Duration.ofMillis(#{any(long)}), #{any(org.junit.jupiter.api.function.Executable)});") + .javaParser(javaParser) + .staticImports("org.junit.jupiter.api.Assertions.assertTimeoutPreemptively") + .imports("java.time.Duration") + .build() + .apply(updateCursor(m), m.getCoordinates().replaceBody(), cta.timeout, lambda); + maybeAddImport("org.junit.jupiter.api.Assertions", "assertTimeoutPreemptively", false); + maybeAddImport("java.time.Duration"); } maybeAddImport("org.junit.jupiter.api.Test"); diff --git a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java index 9f29a01cd..f286dc605 100644 --- a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java +++ b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java @@ -337,6 +337,9 @@ public void test2() { @Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/450") @Test void annotationWithTimeout() { + // assertTimeoutPreemptively preserves JUnit 4's separate-thread preemptive-abort semantics. + // @Timeout without threadMode = SEPARATE_THREAD defaults to SAME_THREAD, which blocks the + // runner indefinitely when a test hangs and never responds to Thread.interrupt(). //language=java rewriteRun( java( @@ -352,15 +355,17 @@ public void test() { """, """ import org.junit.jupiter.api.Test; - import org.junit.jupiter.api.Timeout; - import java.util.concurrent.TimeUnit; + import java.time.Duration; + + import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; public class MyTest { @Test - @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) public void test() { + assertTimeoutPreemptively(Duration.ofMillis(500), () -> { + }); } } """ @@ -433,19 +438,20 @@ public void test() { """, """ import org.junit.jupiter.api.Test; - import org.junit.jupiter.api.Timeout; - import java.util.concurrent.TimeUnit; + import java.time.Duration; import static org.junit.jupiter.api.Assertions.assertThrows; + import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; public class MyTest { @Test - @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) public void test() { - assertThrows(IllegalArgumentException.class, () -> { - throw new IllegalArgumentException("boom"); + assertTimeoutPreemptively(Duration.ofMillis(500), () -> { + assertThrows(IllegalArgumentException.class, () -> { + throw new IllegalArgumentException("boom"); + }); }); } } From 0ce7296c1346e1970a63f25776f86e553dbfb3de Mon Sep 17 00:00:00 2001 From: mdepaula Date: Wed, 5 Aug 2026 12:48:23 -0400 Subject: [PATCH 2/8] Replace inline comment with @Issue annotation, following file conventions --- .../java/testing/junit5/UpdateTestAnnotationTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java index f286dc605..460dcc10f 100644 --- a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java +++ b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java @@ -335,11 +335,9 @@ public void test2() { } @Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/450") + @Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/1073") @Test void annotationWithTimeout() { - // assertTimeoutPreemptively preserves JUnit 4's separate-thread preemptive-abort semantics. - // @Timeout without threadMode = SEPARATE_THREAD defaults to SAME_THREAD, which blocks the - // runner indefinitely when a test hangs and never responds to Thread.interrupt(). //language=java rewriteRun( java( From fb8153014cdc726074278c2a2d0c64c08376c4ea Mon Sep 17 00:00:00 2001 From: mdepaula Date: Wed, 5 Aug 2026 12:49:10 -0400 Subject: [PATCH 3/8] Add test for timeout wrapping with non-empty method body --- .../junit5/UpdateTestAnnotationTest.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java index 460dcc10f..b26128fec 100644 --- a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java +++ b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java @@ -371,6 +371,46 @@ public void test() { ); } + @Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/1073") + @Test + void annotationWithTimeoutAndStatements() { + //language=java + rewriteRun( + java( + """ + import org.junit.Test; + + public class MyTest { + + @Test(timeout = 500) + public void test() { + String foo = "foo"; + System.out.println(foo); + } + } + """, + """ + import org.junit.jupiter.api.Test; + + import java.time.Duration; + + import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + + public class MyTest { + + @Test + public void test() { + assertTimeoutPreemptively(Duration.ofMillis(500), () -> { + String foo = "foo"; + System.out.println(foo); + }); + } + } + """ + ) + ); + } + @Test void annotationWithImportedException() { //language=java From 7eec1e8aeb4981e1397bc9656eb21e0a3cd097a6 Mon Sep 17 00:00:00 2001 From: mdepaula Date: Wed, 5 Aug 2026 12:57:00 -0400 Subject: [PATCH 4/8] Fix test formatting to match file conventions --- .../java/testing/junit5/UpdateTestAnnotationTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java index b26128fec..f6956663d 100644 --- a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java +++ b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java @@ -540,7 +540,6 @@ public void foo() { ); } - @Test void migrateDotClass() { //language=java @@ -594,8 +593,8 @@ public void test() { @Test void fullyQualified() { + //language=java rewriteRun( - //language=java java( """ public class MyTest { @@ -619,8 +618,8 @@ public void feature1() { @Test void mixedFullyQualifiedAndNot() { + //language=java rewriteRun( - //language=java java( """ import org.junit.Test; From c80bcf844305dd1ac4ed2acd31a30c2f2d344980 Mon Sep 17 00:00:00 2001 From: mdepaula Date: Wed, 5 Aug 2026 13:02:19 -0400 Subject: [PATCH 5/8] Add @Issue to annotationWithTimeoutAndException --- .../java/testing/junit5/UpdateTestAnnotationTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java index f6956663d..5a052de4b 100644 --- a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java +++ b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java @@ -458,6 +458,7 @@ public void test() { ); } + @Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/1073") @Test void annotationWithTimeoutAndException() { //language=java From 1a9c8c69436fb63cdfb84cf708a2ceaf6dc17e77 Mon Sep 17 00:00:00 2001 From: mdepaula Date: Wed, 5 Aug 2026 13:08:47 -0400 Subject: [PATCH 6/8] Skip assertTimeoutPreemptively when timeout=0 (JUnit 4 no-timeout sentinel) --- .../testing/junit5/UpdateTestAnnotation.java | 4 ++- .../junit5/UpdateTestAnnotationTest.java | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotation.java b/src/main/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotation.java index 1cc23a5ea..39a5b4527 100644 --- a/src/main/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotation.java +++ b/src/main/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotation.java @@ -155,7 +155,9 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex maybeAddImport("org.junit.jupiter.api.Assertions", "assertThrows", false); } } - if (cta.timeout != null) { + if (cta.timeout != null && + !J.Literal.isLiteralValue(cta.timeout, 0L) && + !J.Literal.isLiteralValue(cta.timeout, 0)) { m = JavaTemplate.builder("org.junit.jupiter.api.function.Executable o = () -> #{};") .javaParser(javaParser) .build() diff --git a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java index 5a052de4b..9a8ff2f0e 100644 --- a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java +++ b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java @@ -371,6 +371,38 @@ public void test() { ); } + @Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/1073") + @Test + void timeoutZeroIsNoOpInJUnit4SoAnnotationOnlyMigrated() { + //language=java + rewriteRun( + java( + """ + import org.junit.Test; + + public class MyTest { + + @Test(timeout = 0) + public void test() { + String foo = "foo"; + } + } + """, + """ + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + public void test() { + String foo = "foo"; + } + } + """ + ) + ); + } + @Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/1073") @Test void annotationWithTimeoutAndStatements() { From e4f17658f73a7e8814c645f7f34a494e701f9490 Mon Sep 17 00:00:00 2001 From: mdepaula Date: Wed, 5 Aug 2026 13:13:44 -0400 Subject: [PATCH 7/8] Add test asserting throws clause is preserved for checked exceptions with timeout --- .../junit5/UpdateTestAnnotationTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java index 9a8ff2f0e..5a9277424 100644 --- a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java +++ b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java @@ -490,6 +490,54 @@ public void test() { ); } + @Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/1073") + @Test + void annotationWithTimeoutAndCheckedExceptionInBody() { + //language=java + rewriteRun( + java( + """ + import org.junit.Test; + import java.io.IOException; + + public class MyTest { + + @Test(timeout = 500) + public void test() throws IOException { + foo(); + } + + void foo() throws IOException { + throw new IOException(); + } + } + """, + """ + import org.junit.jupiter.api.Test; + + import java.io.IOException; + import java.time.Duration; + + import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + + public class MyTest { + + @Test + public void test() throws IOException { + assertTimeoutPreemptively(Duration.ofMillis(500), () -> { + foo(); + }); + } + + void foo() throws IOException { + throw new IOException(); + } + } + """ + ) + ); + } + @Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/1073") @Test void annotationWithTimeoutAndException() { From 4af3442da5680181af9d385324231ae579fceb56 Mon Sep 17 00:00:00 2001 From: mdepaula Date: Wed, 5 Aug 2026 13:15:33 -0400 Subject: [PATCH 8/8] Revert "Add test asserting throws clause is preserved for checked exceptions with timeout" This reverts commit e4f17658f73a7e8814c645f7f34a494e701f9490. --- .../junit5/UpdateTestAnnotationTest.java | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java index 5a9277424..9a8ff2f0e 100644 --- a/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java +++ b/src/test/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotationTest.java @@ -490,54 +490,6 @@ public void test() { ); } - @Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/1073") - @Test - void annotationWithTimeoutAndCheckedExceptionInBody() { - //language=java - rewriteRun( - java( - """ - import org.junit.Test; - import java.io.IOException; - - public class MyTest { - - @Test(timeout = 500) - public void test() throws IOException { - foo(); - } - - void foo() throws IOException { - throw new IOException(); - } - } - """, - """ - import org.junit.jupiter.api.Test; - - import java.io.IOException; - import java.time.Duration; - - import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; - - public class MyTest { - - @Test - public void test() throws IOException { - assertTimeoutPreemptively(Duration.ofMillis(500), () -> { - foo(); - }); - } - - void foo() throws IOException { - throw new IOException(); - } - } - """ - ) - ); - } - @Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/1073") @Test void annotationWithTimeoutAndException() {