Skip to content

Commit 55495a8

Browse files
arodionovgithub-actions[bot]Laurens-Wknutwannhedentimtebeek
authored
Recipe to replace @rule Timeout to JUnit 5 Timeout annotation (#691)
* Recipe to replace @rule Timeout to JUnit 5 Timeout annotation * different JavaTemplates * Update src/main/java/org/openrewrite/java/testing/junit5/TimeoutRuleToClassAnnotation.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * added new recipe to JUnit4to5Migration * Inline the named public class, and remove the builder field * - fix failed test formating - remove JavaParser.Builder caching * - added strict checks for Timeout variable initializer and tests * Inline JavaParser methods for consistency * Use MethodMatcher to match methods, not the name as String * Resolve nullability warnings * Consistently use method matchers --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Laurens Westerlaken <laurens.westerlaken@jdriven.com> Co-authored-by: Knut Wannheden <knut@moderne.io> Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent 6dfc033 commit 55495a8

12 files changed

+510
-136
lines changed

src/main/java/org/openrewrite/java/testing/junit5/ExpectedExceptionToAssertThrows.java

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package org.openrewrite.java.testing.junit5;
1717

18-
import org.jspecify.annotations.Nullable;
1918
import org.openrewrite.ExecutionContext;
2019
import org.openrewrite.Preconditions;
2120
import org.openrewrite.Recipe;
@@ -60,18 +59,7 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
6059
return Preconditions.check(new UsesType<>("org.junit.rules.ExpectedException", false), new ExpectedExceptionToAssertThrowsVisitor());
6160
}
6261

63-
public static class ExpectedExceptionToAssertThrowsVisitor extends JavaIsoVisitor<ExecutionContext> {
64-
65-
private JavaParser.@Nullable Builder<?, ?> javaParser;
66-
67-
private JavaParser.Builder<?, ?> javaParser(ExecutionContext ctx) {
68-
if (javaParser == null) {
69-
javaParser = JavaParser.fromJavaVersion()
70-
.classpathFromResources(ctx, "junit-jupiter-api-5", "hamcrest-3");
71-
}
72-
return javaParser;
73-
74-
}
62+
private static class ExpectedExceptionToAssertThrowsVisitor extends JavaIsoVisitor<ExecutionContext> {
7563

7664
@Override
7765
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
@@ -164,7 +152,8 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDecl
164152
String templateString = expectedExceptionParam instanceof String ? "#{}assertThrows(#{}, () -> #{any()});" : "#{}assertThrows(#{any()}, () -> #{any()});";
165153
m = JavaTemplate.builder(templateString)
166154
.contextSensitive()
167-
.javaParser(javaParser(ctx))
155+
.javaParser(JavaParser.fromJavaVersion()
156+
.classpathFromResources(ctx, "junit-jupiter-api-5", "hamcrest-3"))
168157
.staticImports("org.junit.jupiter.api.Assertions.assertThrows")
169158
.build()
170159
.apply(
@@ -189,7 +178,8 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDecl
189178
if (expectMessageMethodInvocation != null && !isExpectMessageArgAMatcher && m.getBody() != null) {
190179
m = JavaTemplate.builder("assertTrue(exception.getMessage().contains(#{any(java.lang.String)}));")
191180
.contextSensitive()
192-
.javaParser(javaParser(ctx))
181+
.javaParser(JavaParser.fromJavaVersion()
182+
.classpathFromResources(ctx, "junit-jupiter-api-5", "hamcrest-3"))
193183
.staticImports("org.junit.jupiter.api.Assertions.assertTrue")
194184
.build()
195185
.apply(
@@ -202,7 +192,8 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDecl
202192

203193
JavaTemplate assertThatTemplate = JavaTemplate.builder("assertThat(#{}, #{any()});")
204194
.contextSensitive()
205-
.javaParser(javaParser(ctx))
195+
.javaParser(JavaParser.fromJavaVersion()
196+
.classpathFromResources(ctx, "junit-jupiter-api-5", "hamcrest-3"))
206197
.staticImports("org.hamcrest.MatcherAssert.assertThat")
207198
.build();
208199

src/main/java/org/openrewrite/java/testing/junit5/JUnitParamsRunnerToParameterized.java

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public class JUnitParamsRunnerToParameterized extends Recipe {
5555

5656
@Override
5757
public String getDisplayName() {
58-
return "Pragmatists @RunWith(JUnitParamsRunner.class) to JUnit Jupiter Parameterized Tests";
58+
return "Pragmatists `@RunWith(JUnitParamsRunner.class)` to JUnit Jupiter `@Parameterized` tests";
5959
}
6060

6161
@Override
@@ -176,18 +176,6 @@ public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ct
176176
*/
177177
private static class ParametersNoArgsImplicitMethodSource extends JavaIsoVisitor<ExecutionContext> {
178178

179-
private JavaParser.@Nullable Builder<?, ?> javaParser;
180-
181-
private JavaParser.Builder<?, ?> javaParser(ExecutionContext ctx) {
182-
if (javaParser == null) {
183-
javaParser = JavaParser.fromJavaVersion()
184-
.classpathFromResources(ctx, "junit-jupiter-api-5", "hamcrest-3", "junit-jupiter-params-5");
185-
}
186-
return javaParser;
187-
188-
}
189-
190-
191179
private final Set<String> initMethods;
192180
private final Set<String> unsupportedConversions;
193181
private final Map<String, String> initMethodReferences;
@@ -202,16 +190,18 @@ public ParametersNoArgsImplicitMethodSource(Set<String> initMethods, Map<String,
202190
this.unsupportedConversions = unsupportedConversions;
203191

204192
// build @ParameterizedTest template
193+
JavaParser.Builder<?, ?> javaParser = JavaParser.fromJavaVersion()
194+
.classpathFromResources(ctx, "junit-jupiter-api-5", "hamcrest-3", "junit-jupiter-params-5");
205195
this.parameterizedTestTemplate = JavaTemplate.builder("@ParameterizedTest")
206-
.javaParser(javaParser(ctx))
196+
.javaParser(javaParser)
207197
.imports("org.junit.jupiter.params.ParameterizedTest").build();
208198
// build @ParameterizedTest(#{}) template
209199
this.parameterizedTestTemplateWithName = JavaTemplate.builder("@ParameterizedTest(name = \"#{}\")")
210-
.javaParser(javaParser(ctx))
200+
.javaParser(javaParser)
211201
.imports("org.junit.jupiter.params.ParameterizedTest").build();
212202
// build @MethodSource("...") template
213203
this.methodSourceTemplate = JavaTemplate.builder("@MethodSource(#{})")
214-
.javaParser(javaParser(ctx))
204+
.javaParser(javaParser)
215205
.imports("org.junit.jupiter.params.provider.MethodSource").build();
216206
}
217207

src/main/java/org/openrewrite/java/testing/junit5/TemporaryFolderToTempDir.java

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,9 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
4949
final AnnotationMatcher classRule = new AnnotationMatcher("@org.junit.ClassRule");
5050
final AnnotationMatcher rule = new AnnotationMatcher("@org.junit.Rule");
5151

52-
53-
private JavaParser.@Nullable Builder<?, ?> javaParser;
54-
5552
private JavaParser.Builder<?, ?> javaParser(ExecutionContext ctx) {
56-
if (javaParser == null) {
57-
javaParser = JavaParser.fromJavaVersion()
53+
return JavaParser.fromJavaVersion()
5854
.classpathFromResources(ctx, "junit-jupiter-api-5");
59-
}
60-
return javaParser;
61-
6255
}
6356

6457
@Override
@@ -157,18 +150,6 @@ private J convertToNewFile(J.MethodInvocation mi, ExecutionContext ctx) {
157150
private static class AddNewFolderMethod extends JavaIsoVisitor<ExecutionContext> {
158151
private final J.MethodInvocation methodInvocation;
159152

160-
161-
private JavaParser.@Nullable Builder<?, ?> javaParser;
162-
163-
private JavaParser.Builder<?, ?> javaParser(ExecutionContext ctx) {
164-
if (javaParser == null) {
165-
javaParser = JavaParser.fromJavaVersion()
166-
.classpathFromResources(ctx, "junit-jupiter-api-5");
167-
}
168-
return javaParser;
169-
170-
}
171-
172153
public AddNewFolderMethod(J.MethodInvocation methodInvocation) {
173154
this.methodInvocation = methodInvocation;
174155
}
@@ -220,7 +201,8 @@ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, Ex
220201
"}")
221202
.contextSensitive()
222203
.imports("java.io.File", "java.io.IOException")
223-
.javaParser(javaParser(ctx))
204+
.javaParser(JavaParser.fromJavaVersion()
205+
.classpathFromResources(ctx, "junit-jupiter-api-5"))
224206
.build()
225207
.apply(updateCursor(cd), cd.getBody().getCoordinates().lastStatement());
226208
newFolderMethodDeclaration = ((J.MethodDeclaration) cd.getBody().getStatements().get(cd.getBody().getStatements().size() - 1)).getMethodType();
@@ -236,16 +218,9 @@ private static class TranslateNewFolderMethodInvocation extends JavaVisitor<Exec
236218
J.MethodInvocation methodScope;
237219
JavaType.Method newMethodType;
238220

239-
240-
private JavaParser.@Nullable Builder<?, ?> javaParser;
241-
242221
private JavaParser.Builder<?, ?> javaParser(ExecutionContext ctx) {
243-
if (javaParser == null) {
244-
javaParser = JavaParser.fromJavaVersion()
222+
return JavaParser.fromJavaVersion()
245223
.classpathFromResources(ctx, "junit-jupiter-api-5");
246-
}
247-
return javaParser;
248-
249224
}
250225

251226
public TranslateNewFolderMethodInvocation(J.MethodInvocation method, JavaType.Method newMethodType) {

src/main/java/org/openrewrite/java/testing/junit5/TestRuleToTestInfo.java

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,6 @@ private static class TestRuleToTestInfoVisitor extends JavaIsoVisitor<ExecutionC
5454
private static final AnnotationMatcher JUNIT_BEFORE_MATCHER = new AnnotationMatcher("@org.junit.Before");
5555
private static final AnnotationMatcher JUPITER_BEFORE_EACH_MATCHER = new AnnotationMatcher("@org.junit.jupiter.api.BeforeEach");
5656

57-
private JavaParser.@Nullable Builder<?, ?> javaParser;
58-
59-
private JavaParser.Builder<?, ?> javaParser(ExecutionContext ctx) {
60-
if (javaParser == null) {
61-
javaParser = JavaParser.fromJavaVersion()
62-
.classpathFromResources(ctx, "junit-jupiter-api-5");
63-
}
64-
return javaParser;
65-
}
66-
6757
@Override
6858
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
6959
J.CompilationUnit compilationUnit = super.visitCompilationUnit(cu, ctx);
@@ -134,7 +124,8 @@ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, Ex
134124
"public void setup(TestInfo testInfo) {" + testMethodStatement + "}";
135125
cd = JavaTemplate.builder(t)
136126
.contextSensitive()
137-
.javaParser(javaParser(ctx))
127+
.javaParser(JavaParser.fromJavaVersion()
128+
.classpathFromResources(ctx, "junit-jupiter-api-5"))
138129
.imports("org.junit.jupiter.api.TestInfo",
139130
"org.junit.jupiter.api.BeforeEach",
140131
"java.util.Optional",
@@ -161,14 +152,9 @@ private static class BeforeMethodToTestInfoVisitor extends JavaIsoVisitor<Execut
161152
private final J.VariableDeclarations varDecls;
162153
private final String testMethodStatement;
163154

164-
private JavaParser.@Nullable Builder<?, ?> javaParser;
165-
166155
private JavaParser.Builder<?, ?> javaParser(ExecutionContext ctx) {
167-
if (javaParser == null) {
168-
javaParser = JavaParser.fromJavaVersion()
156+
return JavaParser.fromJavaVersion()
169157
.classpathFromResources(ctx, "junit-jupiter-api-5");
170-
}
171-
return javaParser;
172158
}
173159

174160
public BeforeMethodToTestInfoVisitor(J.MethodDeclaration beforeMethod, J.VariableDeclarations varDecls, String testMethodStatement) {
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.testing.junit5;
17+
18+
import org.jspecify.annotations.Nullable;
19+
import org.openrewrite.ExecutionContext;
20+
import org.openrewrite.Preconditions;
21+
import org.openrewrite.Recipe;
22+
import org.openrewrite.TreeVisitor;
23+
import org.openrewrite.internal.ListUtils;
24+
import org.openrewrite.java.JavaIsoVisitor;
25+
import org.openrewrite.java.JavaParser;
26+
import org.openrewrite.java.JavaTemplate;
27+
import org.openrewrite.java.MethodMatcher;
28+
import org.openrewrite.java.search.UsesType;
29+
import org.openrewrite.java.tree.Expression;
30+
import org.openrewrite.java.tree.J;
31+
import org.openrewrite.java.tree.TypeUtils;
32+
33+
import java.util.Comparator;
34+
import java.util.List;
35+
import java.util.concurrent.atomic.AtomicReference;
36+
37+
public class TimeoutRuleToClassAnnotation extends Recipe {
38+
39+
private static final MethodMatcher TIMEOUT_CONSTRUCTOR_MATCHER = new MethodMatcher("org.junit.rules.Timeout <constructor>(..)");
40+
private static final MethodMatcher MILLIS_SECONDS_MATCHER = new MethodMatcher("org.junit.rules.Timeout *(long)");
41+
42+
@Override
43+
public String getDisplayName() {
44+
return "JUnit 4 `@Rule Timeout` to JUnit Jupiter's `Timeout`";
45+
}
46+
47+
@Override
48+
public String getDescription() {
49+
return "Replace usages of JUnit 4's `@Rule Timeout` with JUnit 5 `Timeout` class annotation.";
50+
}
51+
52+
@Override
53+
public TreeVisitor<?, ExecutionContext> getVisitor() {
54+
return Preconditions.check(new UsesType<>("org.junit.rules.Timeout", false), new JavaIsoVisitor<ExecutionContext>() {
55+
@Override
56+
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
57+
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
58+
59+
AtomicReference<@Nullable Expression> initializer = new AtomicReference<>();
60+
61+
cd = cd.withBody(cd.getBody().withStatements(ListUtils.map(cd.getBody().getStatements(), statement -> {
62+
if (statement instanceof J.VariableDeclarations) {
63+
//noinspection ConstantConditions
64+
if (TypeUtils.isOfClassType(((J.VariableDeclarations) statement).getTypeExpression().getType(),
65+
"org.junit.rules.Timeout")) {
66+
List<J.VariableDeclarations.NamedVariable> variables = ((J.VariableDeclarations) statement).getVariables();
67+
if (!variables.isEmpty()) {
68+
Expression timeoutInitializer = variables.get(0).getInitializer();
69+
if (TIMEOUT_CONSTRUCTOR_MATCHER.matches(timeoutInitializer) ||
70+
MILLIS_SECONDS_MATCHER.matches(timeoutInitializer)) {
71+
initializer.set(timeoutInitializer);
72+
return null;
73+
}
74+
}
75+
}
76+
}
77+
return statement;
78+
})));
79+
80+
Expression initializerValue = initializer.get();
81+
if (initializerValue != null) {
82+
maybeRemoveImport("org.junit.Rule");
83+
maybeRemoveImport("org.junit.rules.Timeout");
84+
return insertTimeoutAnnotation(initializerValue, cd, ctx);
85+
}
86+
return cd;
87+
}
88+
89+
private J.ClassDeclaration insertTimeoutAnnotation(Expression ex, J.ClassDeclaration cd, ExecutionContext ctx) {
90+
String template;
91+
Object[] params;
92+
if (TIMEOUT_CONSTRUCTOR_MATCHER.matches(ex)) {
93+
List<Expression> arguments = ((J.NewClass) ex).getArguments();
94+
if (arguments.size() == 2) {
95+
template = "@Timeout(value = #{any(long)}, unit = #{any(TimeUnit)})";
96+
params = new Object[]{arguments.get(0), arguments.get(1)};
97+
} else {
98+
template = "@Timeout(value = #{any(long)}, unit = TimeUnit.MILLISECONDS)";
99+
params = new Object[]{arguments.get(0)};
100+
}
101+
} else if (MILLIS_SECONDS_MATCHER.matches(ex)) {
102+
String simpleName = ((J.MethodInvocation) ex).getSimpleName();
103+
String units = simpleName.equals("millis") ? "MILLISECONDS" : "SECONDS";
104+
template = "@Timeout(value = #{any(long)}, unit = TimeUnit." + units + ")";
105+
params = new Object[]{((J.MethodInvocation) ex).getArguments().get(0)};
106+
} else {
107+
return cd;
108+
}
109+
110+
maybeAddImport("org.junit.jupiter.api.Timeout");
111+
maybeAddImport("java.util.concurrent.TimeUnit");
112+
return JavaTemplate.builder(template)
113+
.javaParser(JavaParser.fromJavaVersion()
114+
.classpathFromResources(ctx, "junit-jupiter-api-5", "hamcrest-3"))
115+
.imports("org.junit.jupiter.api.Timeout", "java.util.concurrent.TimeUnit")
116+
.build()
117+
.apply(updateCursor(cd),
118+
cd.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)),
119+
params);
120+
}
121+
});
122+
}
123+
}

0 commit comments

Comments
 (0)