Skip to content

Commit e02bb26

Browse files
author
Tim te Beek
authored
Convert Cucumber-Java8 Steps & Hooks to Cucumber-Java (#262)
1 parent 897be83 commit e02bb26

File tree

7 files changed

+1143
-0
lines changed

7 files changed

+1143
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ out/
66
.project
77
.settings/
88
bin/
9+
*.log

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ dependencies {
125125
implementation("org.openrewrite:rewrite-maven:$rewriteVersion")
126126
runtimeOnly("com.fasterxml.jackson.core:jackson-core:2.12.+")
127127
runtimeOnly("org.openrewrite:rewrite-java-17:$rewriteVersion")
128+
runtimeOnly("io.cucumber:cucumber-java8:7.+")
129+
runtimeOnly("io.cucumber:cucumber-java:7.+")
128130

129131
compileOnly("org.projectlombok:lombok:latest.release")
130132
annotationProcessor("org.projectlombok:lombok:latest.release")
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2022 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.cucumber;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collections;
20+
import java.util.List;
21+
import java.util.Optional;
22+
23+
import lombok.RequiredArgsConstructor;
24+
import org.openrewrite.ExecutionContext;
25+
import org.openrewrite.java.JavaIsoVisitor;
26+
import org.openrewrite.java.JavaParser;
27+
import org.openrewrite.java.JavaTemplate;
28+
import org.openrewrite.java.tree.*;
29+
import org.openrewrite.java.tree.JavaType.FullyQualified;
30+
31+
@RequiredArgsConstructor
32+
class CucumberJava8ClassVisitor extends JavaIsoVisitor<ExecutionContext> {
33+
34+
private static final String IO_CUCUMBER_JAVA = "io.cucumber.java";
35+
private static final String IO_CUCUMBER_JAVA8 = "io.cucumber.java8";
36+
37+
private final FullyQualified stepDefinitionsClass;
38+
private final String replacementImport;
39+
private final String template;
40+
private final Object[] templateParameters;
41+
42+
@Override
43+
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration cd, ExecutionContext p) {
44+
J.ClassDeclaration classDeclaration = super.visitClassDeclaration(cd, p);
45+
if (!TypeUtils.isOfType(classDeclaration.getType(), stepDefinitionsClass)) {
46+
// We aren't looking at the specified class so return without making any modifications
47+
return classDeclaration;
48+
}
49+
50+
// Remove implement of Java8 interfaces & imports; return retained
51+
List<TypeTree> retained = filterImplementingInterfaces(classDeclaration);
52+
53+
// Import Given/When/Then or Before/After as applicable
54+
maybeAddImport(replacementImport);
55+
56+
// Remove empty constructor which might be left over after removing method invocations with typical usage
57+
doAfterVisit(new JavaIsoVisitor<ExecutionContext>() {
58+
@Override
59+
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration md, ExecutionContext p) {
60+
J.MethodDeclaration methodDeclaration = super.visitMethodDeclaration(md, p);
61+
if (methodDeclaration.isConstructor() && methodDeclaration.getBody().getStatements().isEmpty()) {
62+
return null;
63+
}
64+
return methodDeclaration;
65+
}
66+
});
67+
68+
// Remove nested braces from lambda body block inserted into new method
69+
doAfterVisit(new org.openrewrite.java.cleanup.RemoveUnneededBlock());
70+
71+
// Remove unnecessary throws from templates that maybe-throw-exceptions
72+
doAfterVisit(new org.openrewrite.java.cleanup.UnnecessaryThrows());
73+
74+
// Update implements & add new method
75+
return classDeclaration
76+
.withImplements(retained)
77+
.withTemplate(JavaTemplate.builder(this::getCursor, template)
78+
.javaParser(() -> JavaParser.fromJavaVersion().classpath(
79+
"cucumber-java",
80+
"cucumber-java8")
81+
.build())
82+
.imports(replacementImport)
83+
.build(),
84+
coordinatesForNewMethod(classDeclaration.getBody()),
85+
templateParameters);
86+
}
87+
88+
/**
89+
* Remove imports & usage of Cucumber-Java8 interfaces.
90+
*
91+
* @param classDeclaration
92+
* @return retained implementing interfaces
93+
*/
94+
private List<TypeTree> filterImplementingInterfaces(J.ClassDeclaration classDeclaration) {
95+
List<TypeTree> retained = new ArrayList<>();
96+
for (TypeTree typeTree : Optional.ofNullable(classDeclaration.getImplements())
97+
.orElse(Collections.emptyList())) {
98+
if (typeTree.getType() instanceof JavaType.Class) {
99+
JavaType.Class clazz = (JavaType.Class) typeTree.getType();
100+
if (IO_CUCUMBER_JAVA8.equals(clazz.getPackageName())) {
101+
maybeRemoveImport(clazz.getFullyQualifiedName());
102+
continue;
103+
}
104+
}
105+
retained.add(typeTree);
106+
}
107+
return retained;
108+
}
109+
110+
/**
111+
* Place new methods after the last cucumber annotated method, or after the constructor, or at end of class.
112+
*
113+
* @param classDeclaration
114+
* @return
115+
*/
116+
private static JavaCoordinates coordinatesForNewMethod(J.Block body) {
117+
// After last cucumber annotated method
118+
return body.getStatements().stream()
119+
.filter(J.MethodDeclaration.class::isInstance)
120+
.map(firstMethod -> (J.MethodDeclaration) firstMethod)
121+
.filter(method -> method.getAllAnnotations().stream()
122+
.anyMatch(ann -> ((JavaType.Class) ann.getAnnotationType().getType()).getPackageName()
123+
.startsWith(IO_CUCUMBER_JAVA)))
124+
.map(method -> method.getCoordinates().after())
125+
.reduce((a, b) -> b)
126+
// After last constructor
127+
.orElseGet(() -> body.getStatements().stream()
128+
.filter(J.MethodDeclaration.class::isInstance)
129+
.map(firstMethod -> (J.MethodDeclaration) firstMethod)
130+
.filter(J.MethodDeclaration::isConstructor)
131+
.map(constructor -> constructor.getCoordinates().after())
132+
.reduce((a, b) -> b)
133+
// At end of class
134+
.orElseGet(() -> body.getCoordinates().lastStatement()));
135+
}
136+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/*
2+
* Copyright 2022 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.cucumber;
17+
18+
import java.time.Duration;
19+
import java.util.List;
20+
21+
import lombok.Value;
22+
import lombok.With;
23+
import org.openrewrite.Applicability;
24+
import org.openrewrite.ExecutionContext;
25+
import org.openrewrite.Recipe;
26+
import org.openrewrite.TreeVisitor;
27+
import org.openrewrite.internal.lang.Nullable;
28+
import org.openrewrite.java.JavaVisitor;
29+
import org.openrewrite.java.MethodMatcher;
30+
import org.openrewrite.java.search.UsesMethod;
31+
import org.openrewrite.java.tree.Expression;
32+
import org.openrewrite.java.tree.J;
33+
import org.openrewrite.java.tree.JavaType.Primitive;
34+
35+
public class CucumberJava8HookDefinitionToCucumberJava extends Recipe {
36+
37+
private static final String IO_CUCUMBER_JAVA8 = "io.cucumber.java8";
38+
private static final String IO_CUCUMBER_JAVA8_HOOK_BODY = "io.cucumber.java8.HookBody";
39+
private static final String IO_CUCUMBER_JAVA8_HOOK_NO_ARGS_BODY = "io.cucumber.java8.HookNoArgsBody";
40+
41+
private static final String HOOK_BODY_DEFINITION = IO_CUCUMBER_JAVA8
42+
+ ".LambdaGlue *(.., " + IO_CUCUMBER_JAVA8_HOOK_BODY + ")";
43+
private static final String HOOK_NO_ARGS_BODY_DEFINITION = IO_CUCUMBER_JAVA8
44+
+ ".LambdaGlue *(.., " + IO_CUCUMBER_JAVA8_HOOK_NO_ARGS_BODY + ")";
45+
46+
private static final MethodMatcher HOOK_BODY_DEFINITION_METHOD_MATCHER = new MethodMatcher(
47+
HOOK_BODY_DEFINITION);
48+
private static final MethodMatcher HOOK_NO_ARGS_BODY_DEFINITION_METHOD_MATCHER = new MethodMatcher(
49+
HOOK_NO_ARGS_BODY_DEFINITION);
50+
51+
@Override
52+
protected TreeVisitor<?, ExecutionContext> getSingleSourceApplicableTest() {
53+
return Applicability.or(
54+
new UsesMethod<>(HOOK_BODY_DEFINITION, true),
55+
new UsesMethod<>(HOOK_NO_ARGS_BODY_DEFINITION, true));
56+
}
57+
58+
@Override
59+
public String getDisplayName() {
60+
return "Replace Cucumber-Java8 hook definition with Cucumber-Java.";
61+
}
62+
63+
@Override
64+
public String getDescription() {
65+
return "Replace LamdbaGlue hook definitions with new annotated methods with the same body";
66+
}
67+
68+
@Override
69+
public @Nullable Duration getEstimatedEffortPerOccurrence() {
70+
return Duration.ofMinutes(10);
71+
}
72+
73+
@Override
74+
protected TreeVisitor<?, ExecutionContext> getVisitor() {
75+
return new CucumberJava8HooksVisitor();
76+
}
77+
78+
static final class CucumberJava8HooksVisitor extends JavaVisitor<ExecutionContext> {
79+
@Override
80+
public J visitMethodInvocation(J.MethodInvocation mi, ExecutionContext p) {
81+
J.MethodInvocation methodInvocation = (J.MethodInvocation) super.visitMethodInvocation(mi, p);
82+
if (!HOOK_BODY_DEFINITION_METHOD_MATCHER.matches(methodInvocation)
83+
&& !HOOK_NO_ARGS_BODY_DEFINITION_METHOD_MATCHER.matches(methodInvocation)) {
84+
return methodInvocation;
85+
}
86+
87+
// Replacement annotations can only handle literals or constants
88+
if (methodInvocation.getArguments().stream()
89+
.anyMatch(arg -> !(arg instanceof J.Literal) && !(arg instanceof J.Lambda))) {
90+
return methodInvocation
91+
.withMarkers(methodInvocation.getMarkers().searchResult("TODO Migrate manually"));
92+
}
93+
94+
// Extract arguments passed to method
95+
HookArguments hookArguments = parseHookArguments(methodInvocation.getSimpleName(),
96+
methodInvocation.getArguments());
97+
98+
// Add new template method at end of class declaration
99+
J.ClassDeclaration parentClass = getCursor()
100+
.dropParentUntil(J.ClassDeclaration.class::isInstance)
101+
.getValue();
102+
doAfterVisit(new CucumberJava8ClassVisitor(
103+
parentClass.getType(),
104+
hookArguments.replacementImport(),
105+
hookArguments.template(),
106+
hookArguments.parameters()));
107+
108+
// Remove original method invocation; it's replaced in the above visitor
109+
return null;
110+
}
111+
112+
/**
113+
* Parse up to three arguments:
114+
* - last one is always a Lambda;
115+
* - first can also be a String or int.
116+
* - second can be an int;
117+
*
118+
* @param arguments
119+
* @return
120+
*/
121+
HookArguments parseHookArguments(String methodName, List<Expression> arguments) {
122+
// Lambda is always last, and can either contain a body with Scenario argument, or without
123+
int argumentsSize = arguments.size();
124+
Expression lambdaArgument = arguments.get(argumentsSize - 1);
125+
HookArguments hookArguments = new HookArguments(
126+
methodName,
127+
null,
128+
null,
129+
(J.Lambda) lambdaArgument);
130+
if (argumentsSize == 1) {
131+
return hookArguments;
132+
}
133+
134+
J.Literal firstArgument = (J.Literal) arguments.get(0);
135+
if (argumentsSize == 2) {
136+
// First argument is either a String or an int
137+
if (firstArgument.getType() == Primitive.String) {
138+
return hookArguments.withTagExpression((String) firstArgument.getValue());
139+
}
140+
return hookArguments.withOrder((Integer) firstArgument.getValue());
141+
}
142+
// First argument is always a String, second argument always an int
143+
return hookArguments
144+
.withTagExpression((String) firstArgument.getValue())
145+
.withOrder((Integer) ((J.Literal) arguments.get(1)).getValue());
146+
}
147+
}
148+
149+
}
150+
151+
@Value
152+
class HookArguments {
153+
154+
String annotationName;
155+
@Nullable
156+
@With
157+
String tagExpression;
158+
@Nullable
159+
@With
160+
Integer order;
161+
J.Lambda lambda;
162+
163+
String replacementImport() {
164+
return String.format("io.cucumber.java.%s", annotationName);
165+
}
166+
167+
String template() {
168+
return "@#{}#{}\npublic void #{}(#{}) throws Exception {\n\t#{any()}\n}";
169+
}
170+
171+
private String formatAnnotationArguments() {
172+
if (tagExpression == null && order == null) {
173+
return "";
174+
}
175+
StringBuilder template = new StringBuilder();
176+
template.append('(');
177+
if (order != null) {
178+
template.append("order = ").append(order);
179+
if (tagExpression != null) {
180+
template.append(", value = \"").append(tagExpression).append('"');
181+
}
182+
} else {
183+
template.append('"').append(tagExpression).append('"');
184+
}
185+
template.append(')');
186+
return template.toString();
187+
}
188+
189+
private String formatMethodName() {
190+
return String.format("%s%s%s",
191+
annotationName
192+
.replaceFirst("^Before", "before")
193+
.replaceFirst("^After", "after"),
194+
tagExpression == null ? ""
195+
: "_tag_" + tagExpression
196+
.replaceAll("[^A-Za-z0-9]", "_"),
197+
order == null ? "" : "_order_" + order);
198+
}
199+
200+
private String formatMethodArguments() {
201+
J firstLambdaParameter = lambda.getParameters().getParameters().get(0);
202+
if (firstLambdaParameter instanceof J.VariableDeclarations) {
203+
return String.format("io.cucumber.java.Scenario %s",
204+
((J.VariableDeclarations) firstLambdaParameter).getVariables().get(0).getName());
205+
}
206+
return "";
207+
}
208+
209+
public Object[] parameters() {
210+
return new Object[] {
211+
annotationName,
212+
formatAnnotationArguments(),
213+
formatMethodName(),
214+
formatMethodArguments(),
215+
lambda.getBody() };
216+
}
217+
218+
}

0 commit comments

Comments
 (0)