Skip to content

Commit

Permalink
Convert Cucumber-Java8 Steps & Hooks to Cucumber-Java (#262)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tim te Beek authored Sep 30, 2022
1 parent 897be83 commit e02bb26
Show file tree
Hide file tree
Showing 7 changed files with 1,143 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ out/
.project
.settings/
bin/
*.log
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ dependencies {
implementation("org.openrewrite:rewrite-maven:$rewriteVersion")
runtimeOnly("com.fasterxml.jackson.core:jackson-core:2.12.+")
runtimeOnly("org.openrewrite:rewrite-java-17:$rewriteVersion")
runtimeOnly("io.cucumber:cucumber-java8:7.+")
runtimeOnly("io.cucumber:cucumber-java:7.+")

compileOnly("org.projectlombok:lombok:latest.release")
annotationProcessor("org.projectlombok:lombok:latest.release")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright 2022 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.testing.cucumber;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import lombok.RequiredArgsConstructor;
import org.openrewrite.ExecutionContext;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.*;
import org.openrewrite.java.tree.JavaType.FullyQualified;

@RequiredArgsConstructor
class CucumberJava8ClassVisitor extends JavaIsoVisitor<ExecutionContext> {

private static final String IO_CUCUMBER_JAVA = "io.cucumber.java";
private static final String IO_CUCUMBER_JAVA8 = "io.cucumber.java8";

private final FullyQualified stepDefinitionsClass;
private final String replacementImport;
private final String template;
private final Object[] templateParameters;

@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration cd, ExecutionContext p) {
J.ClassDeclaration classDeclaration = super.visitClassDeclaration(cd, p);
if (!TypeUtils.isOfType(classDeclaration.getType(), stepDefinitionsClass)) {
// We aren't looking at the specified class so return without making any modifications
return classDeclaration;
}

// Remove implement of Java8 interfaces & imports; return retained
List<TypeTree> retained = filterImplementingInterfaces(classDeclaration);

// Import Given/When/Then or Before/After as applicable
maybeAddImport(replacementImport);

// Remove empty constructor which might be left over after removing method invocations with typical usage
doAfterVisit(new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration md, ExecutionContext p) {
J.MethodDeclaration methodDeclaration = super.visitMethodDeclaration(md, p);
if (methodDeclaration.isConstructor() && methodDeclaration.getBody().getStatements().isEmpty()) {
return null;
}
return methodDeclaration;
}
});

// Remove nested braces from lambda body block inserted into new method
doAfterVisit(new org.openrewrite.java.cleanup.RemoveUnneededBlock());

// Remove unnecessary throws from templates that maybe-throw-exceptions
doAfterVisit(new org.openrewrite.java.cleanup.UnnecessaryThrows());

// Update implements & add new method
return classDeclaration
.withImplements(retained)
.withTemplate(JavaTemplate.builder(this::getCursor, template)
.javaParser(() -> JavaParser.fromJavaVersion().classpath(
"cucumber-java",
"cucumber-java8")
.build())
.imports(replacementImport)
.build(),
coordinatesForNewMethod(classDeclaration.getBody()),
templateParameters);
}

/**
* Remove imports & usage of Cucumber-Java8 interfaces.
*
* @param classDeclaration
* @return retained implementing interfaces
*/
private List<TypeTree> filterImplementingInterfaces(J.ClassDeclaration classDeclaration) {
List<TypeTree> retained = new ArrayList<>();
for (TypeTree typeTree : Optional.ofNullable(classDeclaration.getImplements())
.orElse(Collections.emptyList())) {
if (typeTree.getType() instanceof JavaType.Class) {
JavaType.Class clazz = (JavaType.Class) typeTree.getType();
if (IO_CUCUMBER_JAVA8.equals(clazz.getPackageName())) {
maybeRemoveImport(clazz.getFullyQualifiedName());
continue;
}
}
retained.add(typeTree);
}
return retained;
}

/**
* Place new methods after the last cucumber annotated method, or after the constructor, or at end of class.
*
* @param classDeclaration
* @return
*/
private static JavaCoordinates coordinatesForNewMethod(J.Block body) {
// After last cucumber annotated method
return body.getStatements().stream()
.filter(J.MethodDeclaration.class::isInstance)
.map(firstMethod -> (J.MethodDeclaration) firstMethod)
.filter(method -> method.getAllAnnotations().stream()
.anyMatch(ann -> ((JavaType.Class) ann.getAnnotationType().getType()).getPackageName()
.startsWith(IO_CUCUMBER_JAVA)))
.map(method -> method.getCoordinates().after())
.reduce((a, b) -> b)
// After last constructor
.orElseGet(() -> body.getStatements().stream()
.filter(J.MethodDeclaration.class::isInstance)
.map(firstMethod -> (J.MethodDeclaration) firstMethod)
.filter(J.MethodDeclaration::isConstructor)
.map(constructor -> constructor.getCoordinates().after())
.reduce((a, b) -> b)
// At end of class
.orElseGet(() -> body.getCoordinates().lastStatement()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/*
* Copyright 2022 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.testing.cucumber;

import java.time.Duration;
import java.util.List;

import lombok.Value;
import lombok.With;
import org.openrewrite.Applicability;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.java.JavaVisitor;
import org.openrewrite.java.MethodMatcher;
import org.openrewrite.java.search.UsesMethod;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType.Primitive;

public class CucumberJava8HookDefinitionToCucumberJava extends Recipe {

private static final String IO_CUCUMBER_JAVA8 = "io.cucumber.java8";
private static final String IO_CUCUMBER_JAVA8_HOOK_BODY = "io.cucumber.java8.HookBody";
private static final String IO_CUCUMBER_JAVA8_HOOK_NO_ARGS_BODY = "io.cucumber.java8.HookNoArgsBody";

private static final String HOOK_BODY_DEFINITION = IO_CUCUMBER_JAVA8
+ ".LambdaGlue *(.., " + IO_CUCUMBER_JAVA8_HOOK_BODY + ")";
private static final String HOOK_NO_ARGS_BODY_DEFINITION = IO_CUCUMBER_JAVA8
+ ".LambdaGlue *(.., " + IO_CUCUMBER_JAVA8_HOOK_NO_ARGS_BODY + ")";

private static final MethodMatcher HOOK_BODY_DEFINITION_METHOD_MATCHER = new MethodMatcher(
HOOK_BODY_DEFINITION);
private static final MethodMatcher HOOK_NO_ARGS_BODY_DEFINITION_METHOD_MATCHER = new MethodMatcher(
HOOK_NO_ARGS_BODY_DEFINITION);

@Override
protected TreeVisitor<?, ExecutionContext> getSingleSourceApplicableTest() {
return Applicability.or(
new UsesMethod<>(HOOK_BODY_DEFINITION, true),
new UsesMethod<>(HOOK_NO_ARGS_BODY_DEFINITION, true));
}

@Override
public String getDisplayName() {
return "Replace Cucumber-Java8 hook definition with Cucumber-Java.";
}

@Override
public String getDescription() {
return "Replace LamdbaGlue hook definitions with new annotated methods with the same body";
}

@Override
public @Nullable Duration getEstimatedEffortPerOccurrence() {
return Duration.ofMinutes(10);
}

@Override
protected TreeVisitor<?, ExecutionContext> getVisitor() {
return new CucumberJava8HooksVisitor();
}

static final class CucumberJava8HooksVisitor extends JavaVisitor<ExecutionContext> {
@Override
public J visitMethodInvocation(J.MethodInvocation mi, ExecutionContext p) {
J.MethodInvocation methodInvocation = (J.MethodInvocation) super.visitMethodInvocation(mi, p);
if (!HOOK_BODY_DEFINITION_METHOD_MATCHER.matches(methodInvocation)
&& !HOOK_NO_ARGS_BODY_DEFINITION_METHOD_MATCHER.matches(methodInvocation)) {
return methodInvocation;
}

// Replacement annotations can only handle literals or constants
if (methodInvocation.getArguments().stream()
.anyMatch(arg -> !(arg instanceof J.Literal) && !(arg instanceof J.Lambda))) {
return methodInvocation
.withMarkers(methodInvocation.getMarkers().searchResult("TODO Migrate manually"));
}

// Extract arguments passed to method
HookArguments hookArguments = parseHookArguments(methodInvocation.getSimpleName(),
methodInvocation.getArguments());

// Add new template method at end of class declaration
J.ClassDeclaration parentClass = getCursor()
.dropParentUntil(J.ClassDeclaration.class::isInstance)
.getValue();
doAfterVisit(new CucumberJava8ClassVisitor(
parentClass.getType(),
hookArguments.replacementImport(),
hookArguments.template(),
hookArguments.parameters()));

// Remove original method invocation; it's replaced in the above visitor
return null;
}

/**
* Parse up to three arguments:
* - last one is always a Lambda;
* - first can also be a String or int.
* - second can be an int;
*
* @param arguments
* @return
*/
HookArguments parseHookArguments(String methodName, List<Expression> arguments) {
// Lambda is always last, and can either contain a body with Scenario argument, or without
int argumentsSize = arguments.size();
Expression lambdaArgument = arguments.get(argumentsSize - 1);
HookArguments hookArguments = new HookArguments(
methodName,
null,
null,
(J.Lambda) lambdaArgument);
if (argumentsSize == 1) {
return hookArguments;
}

J.Literal firstArgument = (J.Literal) arguments.get(0);
if (argumentsSize == 2) {
// First argument is either a String or an int
if (firstArgument.getType() == Primitive.String) {
return hookArguments.withTagExpression((String) firstArgument.getValue());
}
return hookArguments.withOrder((Integer) firstArgument.getValue());
}
// First argument is always a String, second argument always an int
return hookArguments
.withTagExpression((String) firstArgument.getValue())
.withOrder((Integer) ((J.Literal) arguments.get(1)).getValue());
}
}

}

@Value
class HookArguments {

String annotationName;
@Nullable
@With
String tagExpression;
@Nullable
@With
Integer order;
J.Lambda lambda;

String replacementImport() {
return String.format("io.cucumber.java.%s", annotationName);
}

String template() {
return "@#{}#{}\npublic void #{}(#{}) throws Exception {\n\t#{any()}\n}";
}

private String formatAnnotationArguments() {
if (tagExpression == null && order == null) {
return "";
}
StringBuilder template = new StringBuilder();
template.append('(');
if (order != null) {
template.append("order = ").append(order);
if (tagExpression != null) {
template.append(", value = \"").append(tagExpression).append('"');
}
} else {
template.append('"').append(tagExpression).append('"');
}
template.append(')');
return template.toString();
}

private String formatMethodName() {
return String.format("%s%s%s",
annotationName
.replaceFirst("^Before", "before")
.replaceFirst("^After", "after"),
tagExpression == null ? ""
: "_tag_" + tagExpression
.replaceAll("[^A-Za-z0-9]", "_"),
order == null ? "" : "_order_" + order);
}

private String formatMethodArguments() {
J firstLambdaParameter = lambda.getParameters().getParameters().get(0);
if (firstLambdaParameter instanceof J.VariableDeclarations) {
return String.format("io.cucumber.java.Scenario %s",
((J.VariableDeclarations) firstLambdaParameter).getVariables().get(0).getName());
}
return "";
}

public Object[] parameters() {
return new Object[] {
annotationName,
formatAnnotationArguments(),
formatMethodName(),
formatMethodArguments(),
lambda.getBody() };
}

}
Loading

0 comments on commit e02bb26

Please sign in to comment.