Skip to content

Commit 402a8cb

Browse files
committed
Add GradleUseJunitJupiter recipe so that Gradle is configured to use Junit 5 as part of the Junit 5 migration
1 parent 60f7584 commit 402a8cb

File tree

3 files changed

+467
-0
lines changed

3 files changed

+467
-0
lines changed
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (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://www.apache.org/licenses/LICENSE-2.0
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 lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.openrewrite.ExecutionContext;
21+
import org.openrewrite.Recipe;
22+
import org.openrewrite.TreeVisitor;
23+
import org.openrewrite.gradle.GradleParser;
24+
import org.openrewrite.gradle.marker.GradleProject;
25+
import org.openrewrite.groovy.GroovyIsoVisitor;
26+
import org.openrewrite.groovy.tree.G;
27+
import org.openrewrite.internal.ListUtils;
28+
import org.openrewrite.internal.lang.Nullable;
29+
import org.openrewrite.java.MethodMatcher;
30+
import org.openrewrite.java.tree.J;
31+
import org.openrewrite.java.tree.JavaType;
32+
import org.openrewrite.java.tree.TypeUtils;
33+
34+
import java.util.Optional;
35+
import java.util.concurrent.atomic.AtomicBoolean;
36+
import java.util.stream.Collectors;
37+
38+
import static java.util.Objects.requireNonNull;
39+
40+
@Value
41+
@EqualsAndHashCode(callSuper = false)
42+
public class GradleUseJunitJupiter extends Recipe {
43+
@Override
44+
public String getDisplayName() {
45+
return "Gradle `Test` use JUnit Jupiter";
46+
}
47+
48+
@Override
49+
public String getDescription() {
50+
return "By default Gradle's `Test` tasks use JUnit 4." +
51+
"Gradle `Test` tasks must be configured with `useJUnitPlatform()` to run JUnit Jupiter tests. " +
52+
"This recipe adds the `useJUnitPlatform()` method call to the `Test` task configuration.";
53+
}
54+
55+
private static final String USE_JUNIT_PLATFORM_PATTERN = "org.gradle.api.tasks.testing.Test useJUnitPlatform()";
56+
private static final MethodMatcher USE_JUNIT_PLATFORM_MATCHER = new MethodMatcher(USE_JUNIT_PLATFORM_PATTERN);
57+
private static final MethodMatcher USE_JUNIT4_MATCHER = new MethodMatcher("org.gradle.api.tasks.testing.Test useJUnit()");
58+
private static final MethodMatcher USE_JUNIT4_ALTERNATE_MATCHER = new MethodMatcher("RewriteTestSpec useJUnit()");
59+
private static final MethodMatcher TEST_DSL_MATCHER = new MethodMatcher("RewriteGradleProject test(..)");
60+
@Override
61+
public TreeVisitor<?, ExecutionContext> getVisitor() {
62+
//noinspection NotNullFieldNotInitialized
63+
return new GroovyIsoVisitor<ExecutionContext>() {
64+
65+
GradleProject gp;
66+
67+
@Override
68+
public G.CompilationUnit visitCompilationUnit(G.CompilationUnit compilationUnit, ExecutionContext ctx) {
69+
//noinspection DataFlowIssue
70+
gp = compilationUnit.getMarkers().findFirst(GradleProject.class).orElse(null);
71+
if(gp == null) {
72+
return compilationUnit;
73+
}
74+
if(gp.getPlugins().stream().noneMatch(plugin -> plugin.getFullyQualifiedClassName().contains("org.gradle.api.plugins.JavaBasePlugin"))) {
75+
return compilationUnit;
76+
}
77+
if(containsJUnitPlatformInvocation(compilationUnit)) {
78+
return compilationUnit;
79+
}
80+
// If anywhere in the tree there is a useJunit() we can swap it out for useJUnitPlatform() and be done in one step
81+
G.CompilationUnit cu = (G.CompilationUnit) new UpdateExistingUseJunit4()
82+
.visitNonNull(compilationUnit, ctx, requireNonNull(getCursor().getParent()));
83+
if (cu != compilationUnit) {
84+
return cu;
85+
}
86+
// No useJUnit(), but there might already be configuration of a Test task, add useJUnitPlatform() to it
87+
cu = (G.CompilationUnit) new AddJUnitPlatformToExistingTestDsl()
88+
.visitNonNull(cu, ctx, requireNonNull(getCursor().getParent()));
89+
if(cu != compilationUnit) {
90+
return cu;
91+
}
92+
// No existing test task configuration seems to exist, add a whole new one
93+
return (G.CompilationUnit) new AddUseJUnitPlatform()
94+
.visitNonNull(cu, ctx, getCursor().getParent());
95+
}
96+
};
97+
}
98+
99+
private static boolean containsJUnitPlatformInvocation(G.CompilationUnit cu) {
100+
AtomicBoolean found = new AtomicBoolean(false);
101+
new GroovyIsoVisitor<AtomicBoolean>() {
102+
@Override
103+
public @Nullable J preVisit(J tree, AtomicBoolean found) {
104+
if(found.get()) {
105+
stopAfterPreVisit();
106+
return tree;
107+
}
108+
return super.preVisit(tree, found);
109+
}
110+
111+
@Override
112+
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation m, AtomicBoolean found) {
113+
// Groovy gradle scripts being weakly type-attributed means we will miss likely-correct changes if we are too strict
114+
if ("useJUnitPlatform".equals(m.getSimpleName()) && (m.getArguments().isEmpty() || m.getArguments().size() == 1 && m.getArguments().get(0) instanceof J.Empty)) {
115+
found.set(true);
116+
return m;
117+
}
118+
return super.visitMethodInvocation(m, found);
119+
}
120+
}.visit(cu, found);
121+
return found.get();
122+
}
123+
124+
private static class UpdateExistingUseJunit4 extends GroovyIsoVisitor<ExecutionContext> {
125+
@Override
126+
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
127+
J.MethodInvocation m = super.visitMethodInvocation(method, ctx);
128+
// Groovy gradle scripts being weakly type-attributed means we will miss changes if we are too strict
129+
if ("useJUnit".equals(m.getSimpleName()) && (m.getArguments().isEmpty() || m.getArguments().size() == 1 && m.getArguments().get(0) instanceof J.Empty)) {
130+
JavaType.Method useJUnitPlatformType = Optional.ofNullable(m.getMethodType())
131+
.map(JavaType.Method::getDeclaringType)
132+
.flatMap(declaringType -> declaringType.getMethods()
133+
.stream()
134+
.filter(method1 -> method1.getName().equals("useJUnitPlatform"))
135+
.findFirst())
136+
.orElse(null);
137+
return m.withName(m.getName().withSimpleName("useJUnitPlatform"))
138+
.withMethodType(useJUnitPlatformType);
139+
}
140+
return m;
141+
}
142+
}
143+
144+
private static class AddUseJUnitPlatform extends GroovyIsoVisitor<ExecutionContext> {
145+
@Override
146+
public G.CompilationUnit visitCompilationUnit(G.CompilationUnit cu, ExecutionContext executionContext) {
147+
G.CompilationUnit template = GradleParser.builder()
148+
.build()
149+
.parse("plugins {\n" +
150+
" id 'java'\n" +
151+
"}\n" +
152+
"tasks.withType(Test).configureEach {\n" +
153+
" useJUnitPlatform()\n" +
154+
"}")
155+
.map(G.CompilationUnit.class::cast)
156+
.collect(Collectors.toList())
157+
.get(0);
158+
J.MethodInvocation configureEachInvocation = (J.MethodInvocation) template.getStatements().get(1);
159+
return cu.withStatements(ListUtils.concat(cu.getStatements(), configureEachInvocation));
160+
}
161+
}
162+
163+
private static class AddJUnitPlatformToExistingTestDsl extends GroovyIsoVisitor<ExecutionContext> {
164+
@Override
165+
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
166+
J.MethodInvocation m = super.visitMethodInvocation(method, ctx);
167+
String mName = m.getSimpleName();
168+
// A non-exhaustive list of common ways by which the task may already be configured
169+
// test { }
170+
// tasks.withType(Test) { }
171+
// tasks.withType(Test).configureEach { }
172+
// tasks.named("test") { }
173+
// tasks.named("test", Test) { }
174+
switch (mName) {
175+
case "test":
176+
if (!(m.getArguments().size() == 1 && m.getArguments().get(0) instanceof J.Lambda)) {
177+
return m;
178+
}
179+
break;
180+
case "named":
181+
if (m.getArguments().isEmpty()) {
182+
return m;
183+
}
184+
if (!(m.getArguments().get(0) instanceof J.Literal && "test".equals(((J.Literal) m.getArguments().get(0)).getValue()))) {
185+
return m;
186+
}
187+
// The final argument must be a J.Lambda
188+
if (!(m.getArguments().get(m.getArguments().size() - 1) instanceof J.Lambda)) {
189+
return m;
190+
}
191+
break;
192+
case "withType":
193+
if (m.getSelect() == null
194+
|| !TypeUtils.isOfClassType(m.getSelect().getType(), "org.gradle.api.tasks.TaskContainer")
195+
|| !(m.getArguments().get(0) instanceof J.Identifier && "Test".equals(((J.Identifier) m.getArguments().get(0)).getSimpleName()))) {
196+
return m;
197+
}
198+
break;
199+
case "configureEach":
200+
if(m.getArguments().size() != 1 || !(m.getArguments().get(0) instanceof J.Lambda)) {
201+
return m;
202+
}
203+
if(m.getSelect() == null || !(m.getSelect() instanceof J.MethodInvocation)) {
204+
return m;
205+
}
206+
J.MethodInvocation select = (J.MethodInvocation) m.getSelect();
207+
if(!"withType".equals(select.getSimpleName())
208+
|| select.getArguments().size() != 1
209+
|| !(select.getArguments().get(0) instanceof J.Identifier)
210+
|| !"Test".equals(((J.Identifier) select.getArguments().get(0)).getSimpleName())) {
211+
return m;
212+
}
213+
break;
214+
default:
215+
return m;
216+
}
217+
218+
return (J.MethodInvocation) new AddJUnitPlatformAsLastStatementInClosure()
219+
.visitNonNull(m, ctx, requireNonNull(getCursor().getParent()));
220+
}
221+
}
222+
223+
private static class AddJUnitPlatformAsLastStatementInClosure extends GroovyIsoVisitor<ExecutionContext> {
224+
@Override
225+
public J.Lambda visitLambda(J.Lambda l, ExecutionContext ctx) {
226+
if(!(l.getBody() instanceof J.Block)) {
227+
return l;
228+
}
229+
G.CompilationUnit cu = GradleParser.builder()
230+
.build()
231+
.parse("plugins {\n" +
232+
" id 'java'\n" +
233+
"}\n" +
234+
"tasks.withType(Test) {\n" +
235+
" useJUnitPlatform()\n" +
236+
"}")
237+
.map(G.CompilationUnit.class::cast)
238+
.collect(Collectors.toList())
239+
.get(0);
240+
J.MethodInvocation useJUnitPlatform = Optional.of(cu.getStatements().get(1))
241+
.map(J.MethodInvocation.class::cast)
242+
.map(J.MethodInvocation::getArguments)
243+
.map(args -> args.get(1))
244+
.map(J.Lambda.class::cast)
245+
.map(J.Lambda::getBody)
246+
.map(J.Block.class::cast)
247+
.map(J.Block::getStatements)
248+
.map(statements -> statements.get(0))
249+
.map(J.Return.class::cast)
250+
.map(J.Return::getExpression)
251+
.map(J.MethodInvocation.class::cast)
252+
.orElse(null);
253+
if(useJUnitPlatform == null) {
254+
return l;
255+
}
256+
J.Block b = (J.Block) l.getBody();
257+
l = l.withBody(b.withStatements(ListUtils.concat(b.getStatements(), useJUnitPlatform)));
258+
return autoFormat(l, ctx, requireNonNull(getCursor().getParent()));
259+
}
260+
}
261+
}

src/main/resources/META-INF/rewrite/junit5.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ recipeList:
146146
groupId: org.apache.maven.plugins
147147
artifactId: maven-failsafe-plugin
148148
newVersion: 3.1.x
149+
- org.openrewrite.java.testing.junit5.GradleUseJunitJupiter
149150

150151
---
151152
type: specs.openrewrite.org/v1beta/recipe

0 commit comments

Comments
 (0)