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() };
+ }
+
+}
diff --git a/src/main/java/io/cucumber/migration/CucumberJava8StepDefinitionToCucumberJava.java b/src/main/java/io/cucumber/migration/CucumberJava8StepDefinitionToCucumberJava.java
new file mode 100644
index 0000000..0807be8
--- /dev/null
+++ b/src/main/java/io/cucumber/migration/CucumberJava8StepDefinitionToCucumberJava.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 io.cucumber.migration;
+
+import lombok.EqualsAndHashCode;
+import lombok.Value;
+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.TypeUtils;
+import org.openrewrite.marker.SearchResult;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Value
+@EqualsAndHashCode(callSuper = true)
+public class CucumberJava8StepDefinitionToCucumberJava extends Recipe {
+
+ private static final String IO_CUCUMBER_JAVA8_STEP_DEFINITION = "io.cucumber.java8.* *(String, ..)";
+ private static final String IO_CUCUMBER_JAVA8_STEP_DEFINITION_BODY = "io.cucumber.java8.StepDefinitionBody";
+ private static final MethodMatcher STEP_DEFINITION_METHOD_MATCHER = new MethodMatcher(
+ IO_CUCUMBER_JAVA8_STEP_DEFINITION);
+
+ @Override
+ protected TreeVisitor, ExecutionContext> getSingleSourceApplicableTest() {
+ return new UsesMethod<>(IO_CUCUMBER_JAVA8_STEP_DEFINITION, true);
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Replace Cucumber-Java8 step definitions with Cucumber-Java";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Replace StepDefinitionBody methods with StepDefinitionAnnotations on new methods with the same body.";
+ }
+
+ @Override
+ public @Nullable Duration getEstimatedEffortPerOccurrence() {
+ return Duration.ofMinutes(10);
+ }
+
+ @Override
+ protected TreeVisitor, ExecutionContext> getVisitor() {
+ return new CucumberStepDefinitionBodyVisitor();
+ }
+
+ static final class CucumberStepDefinitionBodyVisitor extends JavaVisitor {
+ @Override
+ public J visitMethodInvocation(J.MethodInvocation methodInvocation, ExecutionContext p) {
+ J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(methodInvocation, p);
+ if (!STEP_DEFINITION_METHOD_MATCHER.matches(m)) {
+ return m;
+ }
+
+ // Skip any methods not containing a second argument, such as
+ // Scenario.log(String)
+ List arguments = m.getArguments();
+ if (arguments.size() < 2) {
+ return m;
+ }
+
+ // Annotations require a String literal
+ Expression stringExpression = arguments.get(0);
+ if (!(stringExpression instanceof J.Literal)) {
+ return SearchResult.found(m, "TODO Migrate manually");
+ }
+ J.Literal literal = (J.Literal) stringExpression;
+
+ // Extract step definition body, when applicable
+ Expression possibleStepDefinitionBody = arguments.get(1); // Always
+ // available
+ // after a
+ // first
+ // String
+ // argument
+ if (!(possibleStepDefinitionBody instanceof J.Lambda)
+ || !TypeUtils.isAssignableTo(IO_CUCUMBER_JAVA8_STEP_DEFINITION_BODY,
+ possibleStepDefinitionBody.getType())) {
+ return SearchResult.found(m, "TODO Migrate manually");
+ }
+ J.Lambda lambda = (J.Lambda) possibleStepDefinitionBody;
+
+ StepDefinitionArguments stepArguments = new StepDefinitionArguments(
+ m.getSimpleName(), literal, lambda);
+
+ // Determine step definitions class name
+ J.ClassDeclaration parentClass = getCursor()
+ .dropParentUntil(J.ClassDeclaration.class::isInstance)
+ .getValue();
+ if (m.getMethodType() == null) {
+ return m;
+ }
+ String replacementImport = String.format("%s.%s",
+ m.getMethodType().getDeclaringType().getFullyQualifiedName()
+ .replace("java8", "java").toLowerCase(),
+ m.getSimpleName());
+ doAfterVisit(new CucumberJava8ClassVisitor(
+ parentClass.getType(),
+ replacementImport,
+ stepArguments.template(),
+ stepArguments.parameters()));
+
+ // Remove original method invocation; it's replaced in the above
+ // visitor
+ // noinspection DataFlowIssue
+ return null;
+ }
+ }
+
+}
+
+@Value
+class StepDefinitionArguments {
+
+ String annotationName;
+ J.Literal cucumberExpression;
+ J.Lambda lambda;
+
+ String template() {
+ return "@#{}(#{any()})\npublic void #{}(#{}) throws Exception {\n\t#{any()}\n}";
+ }
+
+ private String formatMethodName() {
+ return ((String) cucumberExpression.getValue())
+ .replaceAll("\\s+", "_")
+ .replaceAll("[^A-Za-z0-9_]", "")
+ .toLowerCase();
+ }
+
+ private String formatMethodArguments() {
+ // TODO Type loss here, but my attempts to pass these as J failed:
+ // __P__./*__p0__*/p ()
+ return lambda.getParameters().getParameters().stream()
+ .filter(j -> j instanceof J.VariableDeclarations)
+ .map(j -> (J.VariableDeclarations) j)
+ .map(J.VariableDeclarations::toString)
+ .collect(Collectors.joining(", "));
+ }
+
+ Object[] parameters() {
+ return new Object[] {
+ annotationName,
+ cucumberExpression,
+ formatMethodName(),
+ formatMethodArguments(),
+ lambda.getBody() };
+ }
+
+}
diff --git a/src/main/java/io/cucumber/migration/DropSummaryPrinter.java b/src/main/java/io/cucumber/migration/DropSummaryPrinter.java
new file mode 100644
index 0000000..2db46f8
--- /dev/null
+++ b/src/main/java/io/cucumber/migration/DropSummaryPrinter.java
@@ -0,0 +1,81 @@
+package io.cucumber.migration;
+
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.Recipe;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.internal.ListUtils;
+import org.openrewrite.internal.lang.Nullable;
+import org.openrewrite.java.ChangeType;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.RemoveImport;
+import org.openrewrite.java.search.UsesType;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.TypeUtils;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+public class DropSummaryPrinter extends Recipe {
+
+ private static final String IO_CUCUMBER_PLUGIN_SUMMARY_PRINTER = "io.cucumber.plugin.SummaryPrinter";
+ private static final String IO_CUCUMBER_PLUGIN_PLUGIN = "io.cucumber.plugin.Plugin";
+
+ @Override
+ protected TreeVisitor, ExecutionContext> getSingleSourceApplicableTest() {
+ return new UsesType<>(IO_CUCUMBER_PLUGIN_SUMMARY_PRINTER);
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Drop SummaryPrinter";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Replace SummaryPrinter with Plugin, if not already present.";
+ }
+
+ @Override
+ public @Nullable Duration getEstimatedEffortPerOccurrence() {
+ return Duration.ofMinutes(1);
+ }
+
+ @Override
+ protected TreeVisitor, ExecutionContext> getVisitor() {
+ return new DropSummaryPrinterVisitor();
+ }
+
+ static final class DropSummaryPrinterVisitor extends JavaIsoVisitor {
+ @Override
+ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration cd, ExecutionContext p) {
+ J.ClassDeclaration classDeclaration = super.visitClassDeclaration(cd, p);
+ boolean implementsSummaryPrinter = Stream.of(classDeclaration.getImplements())
+ .filter(Objects::nonNull)
+ .flatMap(Collection::stream)
+ .anyMatch(t -> TypeUtils.isOfClassType(t.getType(), IO_CUCUMBER_PLUGIN_SUMMARY_PRINTER));
+ boolean alreadyImplementsPlugin = Stream.of(classDeclaration.getImplements())
+ .filter(Objects::nonNull)
+ .flatMap(Collection::stream)
+ .anyMatch(t -> TypeUtils.isOfClassType(t.getType(), IO_CUCUMBER_PLUGIN_PLUGIN));
+ if (!implementsSummaryPrinter) {
+ return classDeclaration;
+ }
+ doAfterVisit(new ChangeType(
+ IO_CUCUMBER_PLUGIN_SUMMARY_PRINTER,
+ IO_CUCUMBER_PLUGIN_PLUGIN,
+ true));
+ doAfterVisit(new RemoveImport<>(IO_CUCUMBER_PLUGIN_SUMMARY_PRINTER));
+ return classDeclaration.withImplements(ListUtils.map(classDeclaration.getImplements(), i -> {
+ // Remove duplicate implements
+ if (TypeUtils.isOfClassType(i.getType(), IO_CUCUMBER_PLUGIN_SUMMARY_PRINTER)
+ && alreadyImplementsPlugin) {
+ return null;
+ }
+ return i;
+ }));
+ }
+ }
+
+}
diff --git a/src/main/java/io/cucumber/migration/RegexToCucumberExpression.java b/src/main/java/io/cucumber/migration/RegexToCucumberExpression.java
new file mode 100644
index 0000000..3809202
--- /dev/null
+++ b/src/main/java/io/cucumber/migration/RegexToCucumberExpression.java
@@ -0,0 +1,134 @@
+package io.cucumber.migration;
+
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.Recipe;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.internal.ListUtils;
+import org.openrewrite.internal.lang.Nullable;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.search.UsesType;
+import org.openrewrite.java.tree.Expression;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.JavaType;
+import org.openrewrite.java.tree.TypeUtils;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+public class RegexToCucumberExpression extends Recipe {
+
+ private static final String IO_CUCUMBER_JAVA = "io.cucumber.java";
+ private static final String IO_CUCUMBER_JAVA_STEP_DEFINITION = "io.cucumber.java.*.*";
+
+ @Override
+ protected TreeVisitor, ExecutionContext> getSingleSourceApplicableTest() {
+ return new UsesType<>(IO_CUCUMBER_JAVA_STEP_DEFINITION);
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Replace Cucumber-Java step definition regexes with Cucumber expressions";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Strip regex prefix and suffix from step annotation expressions arguments where possible.";
+ }
+
+ @Override
+ public @Nullable Duration getEstimatedEffortPerOccurrence() {
+ return Duration.ofMinutes(1);
+ }
+
+ @Override
+ protected TreeVisitor, ExecutionContext> getVisitor() {
+ return new CucumberStepDefinitionBodyVisitor();
+ }
+
+ static final class CucumberStepDefinitionBodyVisitor extends JavaIsoVisitor {
+
+ @Override
+ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration m, ExecutionContext p) {
+ J.MethodDeclaration methodDeclaration = super.visitMethodDeclaration(m, p);
+ return methodDeclaration.withLeadingAnnotations(ListUtils.map(methodDeclaration.getLeadingAnnotations(),
+ ann -> replaceRegexWithCucumberExpression(methodDeclaration, ann)));
+ }
+
+ private static J.Annotation replaceRegexWithCucumberExpression(
+ // For when we want to match regexes with method arguments for
+ // replacement cucumber expressions
+ // https://github.com/cucumber/cucumber-expressions#parameter-types
+ J.MethodDeclaration methodDeclaration,
+ J.Annotation annotation
+ ) {
+
+ // Skip if not a cucumber annotation
+ JavaType.FullyQualified annoFqn = TypeUtils.asFullyQualified(annotation.getType());
+ if (annoFqn == null || !annoFqn.getPackageName().startsWith(IO_CUCUMBER_JAVA)) {
+ return annotation;
+ }
+
+ List arguments = annotation.getArguments();
+ Optional possibleExpression = Stream.of(arguments)
+ .filter(Objects::nonNull)
+ .filter(list -> list.size() == 1)
+ .flatMap(Collection::stream)
+ .filter(J.Literal.class::isInstance)
+ .map(e -> (J.Literal) e)
+ .map(l -> (String) l.getValue())
+ // https://github.com/cucumber/cucumber-expressions/blob/main/java/heuristics.adoc
+ .filter(s -> s != null && (s.startsWith("^") || s.endsWith("$") || leadingAndTrailingSlash(s)))
+ .findFirst();
+ if (!possibleExpression.isPresent()) {
+ return annotation;
+ }
+
+ // Strip leading/trailing regex anchors
+ String replacement = stripAnchors(possibleExpression.get());
+
+ // Back off when special characters are encountered in regex
+ if (Stream.of("(", ")", "{", "}", "[", "]", "?", "*", "+", "/", "\\", "^", "|")
+ .anyMatch(replacement::contains)) {
+ return annotation;
+ }
+
+ // Replace regular expression with cucumber expression
+ final String finalReplacement = String.format("\"%s\"", replacement);
+ return annotation.withArguments(ListUtils.map(annotation.getArguments(), arg -> ((J.Literal) arg)
+ .withValue(finalReplacement)
+ .withValueSource(finalReplacement)));
+ }
+
+ private static String stripAnchors(final String initialExpression) {
+ if (leadingAndTrailingSlash(initialExpression)) {
+ return initialExpression.substring(1, initialExpression.length() - 1);
+ }
+
+ // The presence of anchors assumes a Regular Expression, even if
+ // only one of the anchors are present
+ String replacement = initialExpression;
+ if (replacement.startsWith("^")) {
+ replacement = replacement.substring(1);
+ }
+ if (replacement.endsWith("$")) {
+ replacement = replacement.substring(0, replacement.length() - 1);
+ }
+
+ // Prevent conversion of `^/hello world/$` to `/hello world/`
+ if (leadingAndTrailingSlash(replacement)) {
+ return initialExpression;
+ }
+
+ return replacement;
+ }
+
+ private static boolean leadingAndTrailingSlash(final String initialExpression) {
+ return initialExpression.startsWith("/") && initialExpression.endsWith("/");
+ }
+ }
+
+}
diff --git a/src/main/resources/META-INF/rewrite/cucumber.yml b/src/main/resources/META-INF/rewrite/cucumber.yml
new file mode 100755
index 0000000..91d0511
--- /dev/null
+++ b/src/main/resources/META-INF/rewrite/cucumber.yml
@@ -0,0 +1,85 @@
+---
+type: specs.openrewrite.org/v1beta/recipe
+name: io.cucumber.migration.UpgradeCucumber7x
+displayName: Upgrade to Cucumber-JVM 7.x
+description: Upgrade to Cucumber-JVM 7.x from any previous version.
+tags:
+ - testing
+ - cucumber
+recipeList:
+ - io.cucumber.migration.UpgradeCucumber5x
+ - io.cucumber.migration.CucumberJava8ToJava
+ - io.cucumber.migration.DropSummaryPrinter
+ - io.cucumber.migration.RegexToCucumberExpression
+ - io.cucumber.migration.CucumberToJunitPlatformSuite
+ - org.openrewrite.maven.UpgradeDependencyVersion:
+ groupId: io.cucumber
+ artifactId: "*"
+ newVersion: 7.x
+---
+type: specs.openrewrite.org/v1beta/recipe
+name: io.cucumber.migration.UpgradeCucumber5x
+displayName: Upgrade to Cucumber-JVM 5.x
+description: Upgrade to Cucumber-JVM 5.x from any previous version.
+tags:
+ - testing
+ - cucumber
+recipeList:
+ - io.cucumber.migration.UpgradeCucumber2x
+ - org.openrewrite.java.ChangePackage:
+ oldPackageName: cucumber.api
+ newPackageName: io.cucumber
+---
+type: specs.openrewrite.org/v1beta/recipe
+name: io.cucumber.migration.UpgradeCucumber2x
+displayName: Upgrade to Cucumber-JVM 2.x
+description: Upgrade to Cucumber-JVM 2.x from any previous version.
+tags:
+ - testing
+ - cucumber
+recipeList:
+ - org.openrewrite.maven.ChangeDependencyGroupIdAndArtifactId:
+ oldGroupId: info.cukes
+ oldArtifactId: cucumber-java
+ newGroupId: io.cucumber
+ newArtifactId: cucumber-java
+ - org.openrewrite.maven.ChangeDependencyGroupIdAndArtifactId:
+ oldGroupId: info.cukes
+ oldArtifactId: cucumber-java8
+ newGroupId: io.cucumber
+ newArtifactId: cucumber-java8
+---
+type: specs.openrewrite.org/v1beta/recipe
+name: io.cucumber.migration.CucumberJava8ToJava
+displayName: Cucumber-Java8 migration to Cucumber-Java
+description: Migrates Cucumber-Java8 step definitions and LambdaGlue hooks to Cucumber-Java annotated methods.
+tags:
+ - testing
+ - cucumber
+recipeList:
+ - io.cucumber.migration.CucumberJava8HookDefinitionToCucumberJava
+ - io.cucumber.migration.CucumberJava8StepDefinitionToCucumberJava
+ - org.openrewrite.maven.ChangeDependencyGroupIdAndArtifactId:
+ oldGroupId: io.cucumber
+ oldArtifactId: cucumber-java8
+ newGroupId: io.cucumber
+ newArtifactId: cucumber-java
+ - org.openrewrite.java.ChangePackage:
+ oldPackageName: io.cucumber.java8
+ newPackageName: io.cucumber.java
+---
+type: specs.openrewrite.org/v1beta/recipe
+name: io.cucumber.migration.CucumberToJunitPlatformSuite
+displayName: Cucumber to JUnit Test Suites
+description: Migrates Cucumber tests to JUnit Test Suites.
+tags:
+ - testing
+ - cucumber
+recipeList:
+ - io.cucumber.migration.CucumberAnnotationToSuite
+ - org.openrewrite.maven.AddDependency:
+ groupId: org.junit.platform
+ artifactId: junit-platform-suite
+ version: 1.9.x
+ onlyIfUsing: org.junit.platform.suite.api.*
+ acceptTransitive: true
diff --git a/src/test/java/io/cucumber/migration/CucumberAnnotationToSuiteTest.java b/src/test/java/io/cucumber/migration/CucumberAnnotationToSuiteTest.java
new file mode 100644
index 0000000..22110bc
--- /dev/null
+++ b/src/test/java/io/cucumber/migration/CucumberAnnotationToSuiteTest.java
@@ -0,0 +1,47 @@
+package io.cucumber.migration;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.Issue;
+import org.openrewrite.java.JavaParser;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.java.Assertions.java;
+
+@Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/264")
+class CucumberAnnotationToSuiteTest implements RewriteTest {
+
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new CucumberAnnotationToSuite())
+ .parser(JavaParser.fromJavaVersion().classpath("cucumber-junit-platform-engine",
+ "junit-platform-suite-api"));
+ }
+
+ @Test
+ void shouldReplaceCucumberAnnotationWithSuiteWithSelectedClasspathResource() {
+ // language=java
+ rewriteRun(
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.junit.platform.engine.Cucumber;
+
+ @Cucumber
+ public class CucumberJava8Definitions {
+ }
+ """,
+ """
+ package com.example.app;
+
+ import org.junit.platform.suite.api.SelectClasspathResource;
+ import org.junit.platform.suite.api.Suite;
+
+ @Suite
+ @SelectClasspathResource("com/example/app")
+ public class CucumberJava8Definitions {
+ }
+ """));
+ }
+}
diff --git a/src/test/java/io/cucumber/migration/CucumberJava8ToCucumberJavaTest.java b/src/test/java/io/cucumber/migration/CucumberJava8ToCucumberJavaTest.java
new file mode 100644
index 0000000..65f3505
--- /dev/null
+++ b/src/test/java/io/cucumber/migration/CucumberJava8ToCucumberJavaTest.java
@@ -0,0 +1,654 @@
+package io.cucumber.migration;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.openrewrite.Issue;
+import org.openrewrite.java.JavaParser;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.java.Assertions.java;
+import static org.openrewrite.java.Assertions.version;
+
+@Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/259")
+class CucumberJava8ToCucumberJavaTest implements RewriteTest {
+
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe("/META-INF/rewrite/cucumber.yml", "io.cucumber.migration.CucumberJava8ToJava");
+ spec.parser(JavaParser.fromJavaVersion()
+ .logCompilationWarningsAndErrors(true)
+ .classpath("junit-jupiter-api", "cucumber-java", "cucumber-java8"));
+ }
+
+ @SuppressWarnings("CodeBlock2Expr")
+ @Test
+ void cucumberJava8HooksAndSteps() {
+ rewriteRun(
+ version(
+ // language=java
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.java8.En;
+ import io.cucumber.java8.Scenario;
+ import io.cucumber.java8.Status;
+
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+
+ public class CucumberJava8Definitions implements En {
+
+ private int a;
+
+ public CucumberJava8Definitions() {
+ Before(() -> {
+ a = 0;
+ });
+ When("I add {int}", (Integer b) -> {
+ a += b;
+ });
+ Then("I expect {int}", (Integer c) -> assertEquals(c, a));
+
+ After((Scenario scn) -> {
+ if (scn.getStatus() == Status.FAILED) {
+ scn.log("failed");
+ }
+ });
+
+ }
+
+ }""", """
+ package com.example.app;
+
+ import io.cucumber.java.After;
+ import io.cucumber.java.Before;
+ import io.cucumber.java.en.Then;
+ import io.cucumber.java.en.When;
+ import io.cucumber.java.Scenario;
+ import io.cucumber.java.Status;
+
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+
+ public class CucumberJava8Definitions {
+
+ private int a;
+
+ @Before
+ public void before() {
+ a = 0;
+ }
+
+ @After
+ public void after(io.cucumber.java.Scenario scn) {
+ if (scn.getStatus() == Status.FAILED) {
+ scn.log("failed");
+ }
+ }
+
+ @When("I add {int}")
+ public void i_add_int(Integer b) {
+ a += b;
+ }
+
+ @Then("I expect {int}")
+ public void i_expect_int(Integer c) {
+ assertEquals(c, a);
+ }
+
+ }
+ """),
+ 17));
+ }
+
+ @Nested
+ class StepMigration {
+ @SuppressWarnings("CodeBlock2Expr")
+ @Test
+ void cucumberJava8SampleToJavaSample() {
+ rewriteRun(
+ version(
+ // language=java
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.java8.En;
+
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+
+ public class CalculatorStepDefinitions implements En {
+ private RpnCalculator calc;
+
+ public CalculatorStepDefinitions() {
+ Given("a calculator I just turned on", () -> {
+ calc = new RpnCalculator();
+ });
+
+ When("I add {int} and {int}", (Integer arg1, Integer arg2) -> {
+ calc.push(arg1);
+ calc.push(arg2);
+ calc.push("+");
+ });
+
+ Then("the result is {double}", (Double expected) -> assertEquals(expected, calc.value()));
+ }
+
+ static class RpnCalculator {
+ void push(Object string) {
+ }
+
+ public Double value() {
+ return Double.NaN;
+ }
+ }
+ }
+ """,
+ """
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+ import io.cucumber.java.en.Then;
+ import io.cucumber.java.en.When;
+
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+
+ public class CalculatorStepDefinitions {
+ private RpnCalculator calc;
+
+ @Given("a calculator I just turned on")
+ public void a_calculator_i_just_turned_on() {
+ calc = new RpnCalculator();
+ }
+
+ @When("I add {int} and {int}")
+ public void i_add_int_and_int(Integer arg1, Integer arg2) {
+ calc.push(arg1);
+ calc.push(arg2);
+ calc.push("+");
+ }
+
+ @Then("the result is {double}")
+ public void the_result_is_double(Double expected) {
+ assertEquals(expected, calc.value());
+ }
+
+ static class RpnCalculator {
+ void push(Object string) {
+ }
+
+ public Double value() {
+ return Double.NaN;
+ }
+ }
+ }
+ """),
+ 17));
+ }
+
+ @SuppressWarnings("CodeBlock2Expr")
+ @Test
+ void methodInvocationsOutsideConstructor() {
+ rewriteRun(
+ version(
+ // language=java
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.java8.En;
+
+ public class CalculatorStepDefinitions implements En {
+ private int cakes = 0;
+
+ public CalculatorStepDefinitions() {
+ delegated();
+ }
+
+ private void delegated() {
+ Given("{int} cakes", (Integer i) -> {
+ cakes = i;
+ });
+ }
+ }""",
+ """
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+
+ public class CalculatorStepDefinitions {
+ private int cakes = 0;
+
+ public CalculatorStepDefinitions() {
+ delegated();
+ }
+
+ @Given("{int} cakes")
+ public void int_cakes(Integer i) {
+ cakes = i;
+ }
+
+ private void delegated() {
+ }
+ }"""),
+ 17));
+ }
+
+ @Test
+ void retainWhitespaceAndCommentsInLambdaBody() {
+ rewriteRun(
+ version(
+ // language=java
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.java8.En;
+
+ public class CalculatorStepDefinitions implements En {
+ public CalculatorStepDefinitions() {
+ Given("{int} plus {int}", (Integer a, Integer b) -> {
+
+ // Lambda body comment
+ int c = a + b;
+ });
+ }
+ }
+ """,
+ """
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+
+ public class CalculatorStepDefinitions {
+
+ @Given("{int} plus {int}")
+ public void int_plus_int(Integer a, Integer b) {
+
+ // Lambda body comment
+ int c = a + b;
+ }
+ }
+ """),
+ 17));
+ }
+
+ @Test
+ void retainThrowsException() {
+ rewriteRun(
+ version(
+ // language=java
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.java8.En;
+
+ public class CalculatorStepDefinitions implements En {
+ public CalculatorStepDefinitions() {
+ Given("a thrown exception", () -> {
+ throw new Exception();
+ });
+ }
+ }
+ """,
+ """
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+
+ public class CalculatorStepDefinitions {
+
+ @Given("a thrown exception")
+ public void a_thrown_exception() throws Exception {
+ throw new Exception();
+ }
+ }
+ """),
+ 17));
+ }
+
+ @Test
+ void replaceWhenNotUsingStringConstant() {
+ rewriteRun(
+ version(
+ // language=java
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.java8.En;
+
+ public class CalculatorStepDefinitions implements En {
+ public CalculatorStepDefinitions() {
+ String expression = "{int} plus {int}";
+ Given(expression, (Integer a, Integer b) -> {
+ int c = a + b;
+ });
+ }
+ }
+ """,
+ """
+ package com.example.app;
+
+ import io.cucumber.java.En;
+
+ public class CalculatorStepDefinitions implements En {
+ public CalculatorStepDefinitions() {
+ String expression = "{int} plus {int}";
+ /*~~(TODO Migrate manually)~~>*/Given(expression, (Integer a, Integer b) -> {
+ int c = a + b;
+ });
+ }
+ }
+ """),
+ 17));
+ }
+
+ @Test
+ void replaceWhenUsingStringConstant() {
+ // For simplicity, we only replace when using a String literal for
+ // now
+ rewriteRun(
+ version(
+ // language=java
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.java8.En;
+
+ public class CalculatorStepDefinitions implements En {
+ private static final String expression = "{int} plus {int}";
+ public CalculatorStepDefinitions() {
+ Given(expression, (Integer a, Integer b) -> {
+ int c = a + b;
+ });
+ }
+ }
+ """,
+ """
+ package com.example.app;
+
+ import io.cucumber.java.En;
+
+ public class CalculatorStepDefinitions implements En {
+ private static final String expression = "{int} plus {int}";
+ public CalculatorStepDefinitions() {
+ /*~~(TODO Migrate manually)~~>*/Given(expression, (Integer a, Integer b) -> {
+ int c = a + b;
+ });
+ }
+ }
+ """),
+ 17));
+ }
+
+ @Test
+ void replaceMethodReference() {
+ // For simplicity, we only replace when using lambda for now
+ rewriteRun(
+ version(
+ // language=java
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.java8.En;
+
+ public class CalculatorStepDefinitions implements En {
+ public CalculatorStepDefinitions() {
+ Given("{int} plus {int}", Integer::sum);
+ }
+ }""", """
+ package com.example.app;
+
+ import io.cucumber.java.En;
+
+ public class CalculatorStepDefinitions implements En {
+ public CalculatorStepDefinitions() {
+ /*~~(TODO Migrate manually)~~>*/Given("{int} plus {int}", Integer::sum);
+ }
+ }
+ """),
+ 17));
+ }
+
+ }
+
+ @Nested
+ class HookMigration {
+ @SuppressWarnings("CodeBlock2Expr")
+ @Test
+ void cucumberJava8Hooks() {
+ rewriteRun(
+ version(
+ // language=java
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.java8.En;
+ import io.cucumber.java8.Scenario;
+ import io.cucumber.java8.Status;
+
+ public class HookStepDefinitions implements En {
+
+ private int a;
+
+ public HookStepDefinitions() {
+ Before(() -> {
+ a = 0;
+ });
+
+ Before("abc", () -> a = 0);
+
+ Before("not abc", 0, () -> {
+ a = 0;
+ });
+
+ Before(1, () -> {
+ a = 0;
+ });
+
+ Before(2, scn -> {
+ a = 0;
+ });
+
+ After((Scenario scn) -> {
+ if (scn.getStatus() == Status.FAILED) {
+ scn.log("after scenario");
+ }
+ });
+
+ After("abc", (Scenario scn) -> {
+ scn.log("after scenario");
+ });
+
+ AfterStep(scn -> {
+ a = 0;
+ });
+ }
+
+ }
+ """,
+ """
+ package com.example.app;
+
+ import io.cucumber.java.After;
+ import io.cucumber.java.AfterStep;
+ import io.cucumber.java.Before;
+ import io.cucumber.java.Scenario;
+ import io.cucumber.java.Status;
+
+ public class HookStepDefinitions {
+
+ private int a;
+
+ @Before
+ public void before() {
+ a = 0;
+ }
+
+ @Before("abc")
+ public void before_tag_abc() {
+ a = 0;
+ }
+
+ @Before(order = 0, value = "not abc")
+ public void before_tag_not_abc_order_0() {
+ a = 0;
+ }
+
+ @Before(order = 1)
+ public void before_order_1() {
+ a = 0;
+ }
+
+ @Before(order = 2)
+ public void before_order_2(io.cucumber.java.Scenario scn) {
+ a = 0;
+ }
+
+ @After
+ public void after(io.cucumber.java.Scenario scn) {
+ if (scn.getStatus() == Status.FAILED) {
+ scn.log("after scenario");
+ }
+ }
+
+ @After("abc")
+ public void after_tag_abc(io.cucumber.java.Scenario scn) {
+ scn.log("after scenario");
+ }
+
+ @AfterStep
+ public void afterStep(io.cucumber.java.Scenario scn) {
+ a = 0;
+ }
+
+ }
+ """),
+ 17));
+ }
+
+ @Test
+ void convertAnonymousClasses() {
+ // For simplicity, anonymous classes are not converted for now; it's
+ // not how cucumber-java8 usage was intended
+ rewriteRun(
+ spec -> spec.cycles(2),
+ version(
+ // language=java
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.java8.En;
+ import io.cucumber.java8.HookBody;
+ import io.cucumber.java8.HookNoArgsBody;
+ import io.cucumber.java8.Scenario;
+
+ public class HookStepDefinitions implements En {
+
+ private int a;
+
+ public HookStepDefinitions() {
+ Before(new HookNoArgsBody() {
+ @Override
+ public void accept() {
+ a = 0;
+ }
+ });
+
+ Before(new HookBody() {
+ @Override
+ public void accept(Scenario scenario) {
+ a = 0;
+ }
+ });
+ }
+
+ }
+ """,
+ """
+ package com.example.app;
+
+ import io.cucumber.java.En;
+ import io.cucumber.java.HookBody;
+ import io.cucumber.java.HookNoArgsBody;
+ import io.cucumber.java.Scenario;
+
+ public class HookStepDefinitions implements En {
+
+ private int a;
+
+ public HookStepDefinitions() {
+ /*~~(TODO Migrate manually)~~>*/Before(new HookNoArgsBody() {
+ @Override
+ public void accept() {
+ a = 0;
+ }
+ });
+
+ /*~~(TODO Migrate manually)~~>*/Before(new HookBody() {
+ @Override
+ public void accept(Scenario scenario) {
+ a = 0;
+ }
+ });
+ }
+
+ }
+ """),
+ 17));
+ }
+
+ @Test
+ void convertMethodReference() {
+ // Not converted yet; the referred method can potentially be
+ // annotated and be made public
+ rewriteRun(
+ version(
+ // language=java
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.java8.En;
+
+ public class HookStepDefinitions implements En {
+
+ private int a;
+
+ public HookStepDefinitions() {
+ Before(this::connect);
+ }
+
+ private void connect() {
+ }
+
+ }
+ """,
+ """
+ package com.example.app;
+
+ import io.cucumber.java.En;
+
+ public class HookStepDefinitions implements En {
+
+ private int a;
+
+ public HookStepDefinitions() {
+ /*~~(TODO Migrate manually)~~>*/Before(this::connect);
+ }
+
+ private void connect() {
+ }
+
+ }
+ """),
+ 17));
+ }
+ }
+}
diff --git a/src/test/java/io/cucumber/migration/DropSummaryPrinterTest.java b/src/test/java/io/cucumber/migration/DropSummaryPrinterTest.java
new file mode 100644
index 0000000..0a62e39
--- /dev/null
+++ b/src/test/java/io/cucumber/migration/DropSummaryPrinterTest.java
@@ -0,0 +1,69 @@
+package io.cucumber.migration;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.Issue;
+import org.openrewrite.java.JavaParser;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.java.Assertions.java;
+import static org.openrewrite.java.Assertions.version;
+
+@Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/264")
+class DropSummaryPrinterTest implements RewriteTest {
+
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new DropSummaryPrinter())
+ .parser(JavaParser.fromJavaVersion().classpath("cucumber-plugin"));
+ }
+
+ @Test
+ void replaceSummaryPrinterWithPlugin() {
+ rewriteRun(
+ version(
+ // language=java
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.plugin.SummaryPrinter;
+
+ public class CucumberJava8Definitions implements SummaryPrinter {
+ }""", """
+ package com.example.app;
+
+ import io.cucumber.plugin.Plugin;
+
+ public class CucumberJava8Definitions implements Plugin {
+ }
+ """),
+ 17));
+ }
+
+ @Test
+ void dontDuplicatePlugin() {
+ rewriteRun(
+ version(
+ // language=java
+ java(
+ """
+ package com.example.app;
+
+ import io.cucumber.plugin.Plugin;
+ import io.cucumber.plugin.SummaryPrinter;
+
+ public class CucumberJava8Definitions implements Plugin, SummaryPrinter {
+ }
+ """,
+ """
+ package com.example.app;
+
+ import io.cucumber.plugin.Plugin;
+
+ public class CucumberJava8Definitions implements Plugin {
+ }
+ """),
+ 17));
+ }
+}
diff --git a/src/test/java/io/cucumber/migration/RegexToCucumberExpressionTest.java b/src/test/java/io/cucumber/migration/RegexToCucumberExpressionTest.java
new file mode 100644
index 0000000..bdba3e9
--- /dev/null
+++ b/src/test/java/io/cucumber/migration/RegexToCucumberExpressionTest.java
@@ -0,0 +1,249 @@
+package io.cucumber.migration;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.openrewrite.Issue;
+import org.openrewrite.java.JavaParser;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.java.Assertions.java;
+
+@Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/264")
+class RegexToCucumberExpressionTest implements RewriteTest {
+
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new RegexToCucumberExpression())
+ .parser(JavaParser.fromJavaVersion().classpath("junit-jupiter-api", "cucumber-java"));
+ }
+
+ @Test
+ void regexToCucumberExpression() {
+ // language=java
+ rewriteRun(java("""
+ package com.example.app;
+
+ import io.cucumber.java.Before;
+ import io.cucumber.java.en.Given;
+ import io.cucumber.java.en.Then;
+
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+
+ public class ExpressionDefinitions {
+
+ private int a;
+
+ @Before
+ public void before() {
+ a = 0;
+ }
+
+ @Given("^five cukes$")
+ public void five_cukes() {
+ a = 5;
+ }
+
+ @Then("^I expect (\\\\d+)$")
+ public void i_expect_int(Integer c) {
+ assertEquals(c, a);
+ }
+
+ }
+ """, """
+ package com.example.app;
+
+ import io.cucumber.java.Before;
+ import io.cucumber.java.en.Given;
+ import io.cucumber.java.en.Then;
+
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+
+ public class ExpressionDefinitions {
+
+ private int a;
+
+ @Before
+ public void before() {
+ a = 0;
+ }
+
+ @Given("five cukes")
+ public void five_cukes() {
+ a = 5;
+ }
+
+ @Then("^I expect (\\\\d+)$")
+ public void i_expect_int(Integer c) {
+ assertEquals(c, a);
+ }
+
+ }
+ """));
+ }
+
+ @Nested
+ @DisplayName("should convert")
+ class ShouldConvert {
+
+ @Test
+ void only_leading_anchor() {
+ // language=java
+ rewriteRun(java("""
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+
+ public class ExpressionDefinitions {
+ @Given("^five cukes")
+ public void five_cukes() {
+ }
+ }""", """
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+
+ public class ExpressionDefinitions {
+ @Given("five cukes")
+ public void five_cukes() {
+ }
+ }"""));
+ }
+
+ @Test
+ void only_trailing_anchor() {
+ // language=java
+ rewriteRun(java("""
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+
+ public class ExpressionDefinitions {
+ @Given("five cukes$")
+ public void five_cukes() {
+ }
+ }""", """
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+
+ public class ExpressionDefinitions {
+ @Given("five cukes")
+ public void five_cukes() {
+ }
+ }"""));
+ }
+
+ @Test
+ void forward_slashes() {
+ // language=java
+ rewriteRun(java("""
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+
+ public class ExpressionDefinitions {
+ @Given("/five cukes/")
+ public void five_cukes() {
+ }
+ }""", """
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+
+ public class ExpressionDefinitions {
+ @Given("five cukes")
+ public void five_cukes() {
+ }
+ }"""));
+ }
+
+ }
+
+ @Nested
+ @DisplayName("should not convert")
+ class ShouldNotConvert {
+
+ @Test
+ void unrecognized_capturing_groups() {
+ // language=java
+ rewriteRun(java("""
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+
+ public class ExpressionDefinitions {
+ @Given("^some (foo|bar)$")
+ public void five_cukes(String fooOrBar) {
+ }
+ }"""));
+ }
+
+ @Test
+ void integer_matchers() {
+ // language=java
+ rewriteRun(java("""
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+
+ public class ExpressionDefinitions {
+ @Given("^(\\\\d+) cukes$")
+ public void int_cukes(int cukes) {
+ }
+ }"""));
+ }
+
+ @Test
+ void regex_question_mark_optional() {
+ // language=java
+ rewriteRun(java("""
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+
+ public class ExpressionDefinitions {
+ @Given("^cukes?$")
+ public void cukes() {
+ }
+ }"""));
+ }
+
+ @Test
+ void regex_one_or_more() {
+ // language=java
+ rewriteRun(java("""
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+
+ public class ExpressionDefinitions {
+ @Given("^cukes+$")
+ public void cukes() {
+ }
+ }"""));
+ }
+
+ @Test
+ void disabled() {
+ // language=java
+ rewriteRun(java("""
+ package com.example.app;
+
+ import io.cucumber.java.en.Given;
+ import org.junit.jupiter.api.Disabled;
+
+ public class ExpressionDefinitions {
+ @Disabled("/for now/")
+ public void disabled() {
+ }
+ @Given("trigger getSingleSourceApplicableTest")
+ public void trigger() {
+ }
+ }"""));
+ }
+
+ }
+
+}