Skip to content

Commit 959d798

Browse files
amishra-utimtebeek
andauthored
Add support to migrate EnvironmentVariables TestRule (#705)
* Add support to migrate EnvironmentVariables TestRule * Apply formatter * Apply best practices * Replace packaged jars with type table entries * Remove before/after comments: IDE provides such hints already * fix test failure * use traits * auto code review suggestion --------- Co-authored-by: Tim te Beek <[email protected]>
1 parent 15905d7 commit 959d798

File tree

5 files changed

+458
-0
lines changed

5 files changed

+458
-0
lines changed

build.gradle.kts

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ description = "A rewrite module automating best practices and major version migr
88

99
recipeDependencies {
1010
parserClasspath("com.github.database-rider:rider-junit5:1.44.0")
11+
parserClasspath("com.github.stefanbirkner:system-rules:1.19.0")
1112
parserClasspath("com.github.tomakehurst:wiremock-jre8:2.35.0")
1213
parserClasspath("com.squareup.okhttp3:mockwebserver:3.14.9")
1314
parserClasspath("com.squareup.okhttp3:mockwebserver:4.10.0")
@@ -32,6 +33,8 @@ recipeDependencies {
3233
parserClasspath("org.testcontainers:testcontainers:1.20.6")
3334
parserClasspath("pl.pragmatists:JUnitParams:1.+")
3435
parserClasspath("org.easytesting:fest-assert-core:2.+")
36+
parserClasspath("uk.org.webcompere:system-stubs-core:2.1.8")
37+
parserClasspath("uk.org.webcompere:system-stubs-jupiter:2.1.8")
3538
}
3639

3740
val rewriteVersion = rewriteRecipe.rewriteVersion.get()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
* Copyright 2024 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.NonNull;
19+
import org.jspecify.annotations.Nullable;
20+
import org.openrewrite.ExecutionContext;
21+
import org.openrewrite.Recipe;
22+
import org.openrewrite.TreeVisitor;
23+
import org.openrewrite.java.*;
24+
import org.openrewrite.java.tree.*;
25+
26+
import static java.util.Comparator.comparing;
27+
import static org.openrewrite.java.testing.junit5.Junit4Utils.CLASS_RULE;
28+
import static org.openrewrite.java.testing.junit5.Junit4Utils.RULE;
29+
import static org.openrewrite.java.trait.Traits.annotated;
30+
31+
/**
32+
* A recipe to replace JUnit 4's EnvironmentVariables rule from contrib with the JUnit 5-compatible
33+
* `SystemStubsExtension` and `EnvironmentVariables` from the System Stubs library.
34+
*/
35+
public class EnvironmentVariables extends Recipe {
36+
public static final String ENVIRONMENT_VARIABLES = "org.junit.contrib.java.lang.system.EnvironmentVariables";
37+
public static final String ENVIRONMENT_VARIABLES_STUB = "uk.org.webcompere.systemstubs.environment.EnvironmentVariables";
38+
public static final String SYSTEM_STUBS_EXTENSION = "uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension";
39+
public static final String SYSTEM_STUB = "uk.org.webcompere.systemstubs.jupiter.SystemStub";
40+
private static final String EXTEND_WITH = "org.junit.jupiter.api.extension.ExtendWith";
41+
42+
@Override
43+
public @NonNull String getDisplayName() {
44+
return "Migrate JUnit 4 environmentVariables rule to JUnit 5 system stubs extension";
45+
}
46+
47+
@Override
48+
public @NonNull String getDescription() {
49+
return "Replaces usage of the JUnit 4 `@Rule EnvironmentVariables` with the JUnit 5-compatible " +
50+
"`SystemStubsExtension` and `@SystemStub EnvironmentVariables` from the System Stubs " +
51+
"library.";
52+
}
53+
54+
@Override
55+
public @NonNull TreeVisitor<?, ExecutionContext> getVisitor() {
56+
return new EnvironmentVariablesVisitor();
57+
}
58+
59+
private static class EnvironmentVariablesVisitor extends JavaVisitor<ExecutionContext> {
60+
61+
private static final String HAS_ENV_VAR_RULE = "hasEnvVarRule";
62+
private static final MethodMatcher ENV_VAR_CLEAR =
63+
new MethodMatcher(ENVIRONMENT_VARIABLES + " clear(String[])");
64+
65+
@Override
66+
public @NonNull J visitCompilationUnit(
67+
J.@NonNull CompilationUnit cu, @NonNull ExecutionContext ctx) {
68+
maybeRemoveImport(RULE);
69+
maybeRemoveImport(CLASS_RULE);
70+
maybeRemoveImport(ENVIRONMENT_VARIABLES);
71+
maybeAddImport(SYSTEM_STUBS_EXTENSION);
72+
maybeAddImport(SYSTEM_STUB);
73+
maybeAddImport(EXTEND_WITH);
74+
maybeAddImport(ENVIRONMENT_VARIABLES_STUB);
75+
return super.visitCompilationUnit(cu, ctx);
76+
}
77+
78+
@Override
79+
public @NonNull J visitClassDeclaration(
80+
J.@NonNull ClassDeclaration classDecl, @NonNull ExecutionContext ctx) {
81+
J.ClassDeclaration cd = (J.ClassDeclaration) super.visitClassDeclaration(classDecl, ctx);
82+
Boolean hasEnvVarRule = getCursor().getMessage(HAS_ENV_VAR_RULE);
83+
84+
if (!Boolean.TRUE.equals(hasEnvVarRule)) {
85+
return cd;
86+
}
87+
// Add @ExtendWith(SystemStubsExtension.class) annotation to class.
88+
return systemStubExtensionTemplate(ctx).apply(
89+
updateCursor(cd),
90+
cd.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName)));
91+
}
92+
93+
@Override
94+
public @NonNull J visitVariableDeclarations(
95+
J.@NonNull VariableDeclarations variableDecls, @NonNull ExecutionContext ctx) {
96+
// missing type attribution, possibly parsing error.
97+
if (variableDecls.getType() == null || !TypeUtils.isAssignableTo(ENVIRONMENT_VARIABLES, variableDecls.getType())) {
98+
return variableDecls;
99+
}
100+
J.VariableDeclarations vd = (J.VariableDeclarations) annotated("@org.junit.*Rule").asVisitor(a ->
101+
(new JavaIsoVisitor<ExecutionContext>() {
102+
@Override
103+
public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) {
104+
return systemStubsTemplate(ctx).apply(updateCursor(annotation), annotation.getCoordinates().replace());
105+
}
106+
}).visit(a.getTree(), ctx, a.getCursor().getParentOrThrow()))
107+
.visit(variableDecls, ctx, getCursor().getParentOrThrow());
108+
109+
if (variableDecls != vd) {
110+
// put message to first enclosing ClassDeclaration, to inform that we have an env var rule.
111+
getCursor()
112+
.dropParentUntil(c -> c instanceof J.ClassDeclaration)
113+
.putMessage(HAS_ENV_VAR_RULE, true);
114+
}
115+
116+
return super.visitVariableDeclarations(vd, ctx);
117+
}
118+
119+
@Override
120+
public @Nullable J visitMethodInvocation(
121+
J.@NonNull MethodInvocation method, @NonNull ExecutionContext ctx) {
122+
123+
J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, ctx);
124+
// Replace EnvironmentVariables.clear() with EnvironmentVariables.remove()
125+
if (ENV_VAR_CLEAR.matches(method) && m.getSelect() != null /* NullAway */) {
126+
int argCount = argCount(m);
127+
J j =
128+
getEnvVarClearTemplate(ctx, argCount)
129+
.apply(updateCursor(m), m.getCoordinates().replace(), getArgs(m, argCount));
130+
131+
if (getCursor().getParentTreeCursor().getValue() instanceof J.Block &&
132+
!(j instanceof Statement)) {
133+
return null;
134+
}
135+
return j;
136+
}
137+
return m;
138+
}
139+
140+
@Override
141+
public @Nullable JavaType visitType(@Nullable JavaType type, @NonNull ExecutionContext ctx) {
142+
if (type instanceof JavaType.FullyQualified) {
143+
String fullyQualifiedName = ((JavaType.FullyQualified) type).getFullyQualifiedName();
144+
if (fullyQualifiedName.equals(ENVIRONMENT_VARIABLES)) {
145+
return JavaType.buildType(ENVIRONMENT_VARIABLES_STUB);
146+
}
147+
}
148+
return super.visitType(type, ctx);
149+
}
150+
151+
private static JavaTemplate systemStubExtensionTemplate(ExecutionContext ctx) {
152+
return JavaTemplate.builder("@ExtendWith(SystemStubsExtension.class)")
153+
.imports(EXTEND_WITH, SYSTEM_STUBS_EXTENSION)
154+
.javaParser(
155+
JavaParser.fromJavaVersion().classpathFromResources(ctx, "system-stubs-jupiter", "junit-jupiter-api"))
156+
.build();
157+
}
158+
159+
private static JavaTemplate systemStubsTemplate(ExecutionContext ctx) {
160+
return JavaTemplate.builder("@SystemStub")
161+
.imports(SYSTEM_STUB)
162+
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "system-stubs-jupiter"))
163+
.build();
164+
}
165+
166+
private static JavaTemplate getEnvVarClearTemplate(ExecutionContext ctx, int argsSize) {
167+
StringBuilder template = new StringBuilder("#{any(").append(ENVIRONMENT_VARIABLES_STUB).append(")}");
168+
for (int i = 0; i < argsSize; i++) {
169+
template.append(".remove(#{any(java.lang.String)})");
170+
}
171+
return JavaTemplate.builder(template.toString())
172+
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "system-stubs-jupiter", "system-stubs-core"))
173+
.build();
174+
}
175+
176+
private static int argCount(J.MethodInvocation methodInvocation) {
177+
if (methodInvocation.getArguments().size() == 1) {
178+
// method call with empty args contains an element of type J.Empty in LST.
179+
return methodInvocation.getArguments().get(0) instanceof J.Empty ? 0 : 1;
180+
}
181+
return methodInvocation.getArguments().size();
182+
}
183+
184+
private static Expression[] getArgs(J.MethodInvocation methodInvocation, int argCount) {
185+
Expression[] args = new Expression[argCount + 1];
186+
args[0] = methodInvocation.getSelect();
187+
for (int i = 0; i < argCount; i++) {
188+
args[i + 1] = methodInvocation.getArguments().get(i);
189+
}
190+
return args;
191+
}
192+
}
193+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2024 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 lombok.experimental.UtilityClass;
19+
20+
import java.util.Arrays;
21+
import java.util.HashSet;
22+
import java.util.Set;
23+
24+
/**
25+
* Utility class containing JUnit4 related helper methods used in JUnit4 to JUnit5 migration recipes
26+
*/
27+
@UtilityClass
28+
public class Junit4Utils {
29+
static final String AFTER = "org.junit.After";
30+
static final String AFTER_CLASS = "org.junit.AfterClass";
31+
static final String BEFORE = "org.junit.Before";
32+
static final String BEFORE_CLASS = "org.junit.BeforeClass";
33+
static final String CLASS_RULE = "org.junit.ClassRule";
34+
static final String FIX_METHOD_ORDER = "org.junit.FixMethodOrder";
35+
static final String IGNORE = "org.junit.Ignore";
36+
static final String PARAMETERIZED_PARAMETERS = "org.junit.runners.Parameterized.Parameters";
37+
static final String RULE = "org.junit.Rule";
38+
static final String RUN_WITH = "org.junit.runner.RunWith";
39+
static final String TEST = "org.junit.Test";
40+
41+
static Set<String> classAnnotations() {
42+
return new HashSet<>(Arrays.asList(RUN_WITH, FIX_METHOD_ORDER, IGNORE));
43+
}
44+
45+
static Set<String> methodAnnotations() {
46+
return new HashSet<>(Arrays.asList(
47+
BEFORE,
48+
AFTER,
49+
BEFORE_CLASS,
50+
AFTER_CLASS,
51+
TEST,
52+
PARAMETERIZED_PARAMETERS,
53+
IGNORE,
54+
RULE,
55+
CLASS_RULE));
56+
}
57+
58+
static Set<String> fieldAnnotations() {
59+
return new HashSet<>(Arrays.asList(RULE, CLASS_RULE));
60+
}
61+
}
Binary file not shown.

0 commit comments

Comments
 (0)