From fb26ae2b4bfe25d32000fcaabb4067c410846b1c Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sun, 12 Mar 2023 16:25:39 +0100 Subject: [PATCH 01/12] Add basic structure as seen in cucumber-jvm --- .github/FUNDING.yml | 1 + .github/lock.yml | 34 +++ .github/renovate.json | 6 + .github/stale.yml | 17 ++ .github/workflows/release-github.yml | 18 ++ .github/workflows/release-java.yml | 24 ++ .github/workflows/test-java.yml | 33 ++ .gitignore | 32 ++ .spotless/eclipse-formatter-settings.xml | 365 +++++++++++++++++++++++ .spotless/intellij-idea.importorder | 6 + LICENCE | 20 ++ README.md | 45 +++ 12 files changed, 601 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/lock.yml create mode 100644 .github/renovate.json create mode 100644 .github/stale.yml create mode 100644 .github/workflows/release-github.yml create mode 100644 .github/workflows/release-java.yml create mode 100644 .github/workflows/test-java.yml create mode 100644 .gitignore create mode 100644 .spotless/eclipse-formatter-settings.xml create mode 100644 .spotless/intellij-idea.importorder create mode 100644 LICENCE diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e5c98ef --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +open_collective: cucumber diff --git a/.github/lock.yml b/.github/lock.yml new file mode 100644 index 0000000..3a474cb --- /dev/null +++ b/.github/lock.yml @@ -0,0 +1,34 @@ +# Configuration for lock-threads - https://github.com/dessant/lock-threads + +# Number of days of inactivity before a closed issue or pull request is locked +daysUntilLock: 365 + +# Issues and pull requests with these labels will not be locked. Set to `[]` to disable +exemptLabels: [] + +# Label to add before locking, such as `outdated`. Set to `false` to disable +lockLabel: false + +# Comment to post before locking. Set to `false` to disable +lockComment: > + This thread has been automatically locked since there has not been + any recent activity after it was closed. Please open a new issue for + related bugs. + +# Assign `resolved` as the reason for locking. Set to `false` to disable +setLockReason: true + +# Limit to only `issues` or `pulls` +# only: issues + +# Optionally, specify configuration settings just for `issues` or `pulls` +# issues: +# exemptLabels: +# - help-wanted +# lockLabel: outdated + +# pulls: +# daysUntilLock: 30 + +# Repository to extend settings from +# _extends: repo diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..1975114 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "github>cucumber/renovate-config" + ] +} diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..b7a1f32 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 300 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 60 +# Issues with these labels will never be considered stale +exemptLabels: + - ":safety_pin: pinned" +# Label to use when marking an issue as stale +staleLabel: ":hourglass: stale" +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed in two months if no further activity occurs. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: > + This issue has been automatically closed because of inactivity. + You can support the Cucumber core team on [opencollective](https://opencollective.com/cucumber). diff --git a/.github/workflows/release-github.yml b/.github/workflows/release-github.yml new file mode 100644 index 0000000..88b128a --- /dev/null +++ b/.github/workflows/release-github.yml @@ -0,0 +1,18 @@ +name: Release GitHub + +on: + push: + branches: [release/*] + +jobs: + create-github-release: + name: Create GitHub Release and Git tag + runs-on: ubuntu-latest + environment: Release + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + - uses: cucumber/action-create-github-release@v1.1.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-java.yml b/.github/workflows/release-java.yml new file mode 100644 index 0000000..1e3f9f8 --- /dev/null +++ b/.github/workflows/release-java.yml @@ -0,0 +1,24 @@ +name: Release Maven + +on: + push: + branches: [release/*] + +jobs: + publish-mvn: + name: Publish Maven Package + runs-on: ubuntu-latest + environment: Release + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + cache: 'maven' + - uses: cucumber/action-publish-mvn@v2.0.0 + with: + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }} + nexus-username: ${{ secrets.SONATYPE_USERNAME }} + nexus-password: ${{ secrets.SONATYPE_PASSWORD }} diff --git a/.github/workflows/test-java.yml b/.github/workflows/test-java.yml new file mode 100644 index 0000000..d71ba99 --- /dev/null +++ b/.github/workflows/test-java.yml @@ -0,0 +1,33 @@ +name: Test Java + +on: + pull_request: + branches: + - '**' + workflow_call: + push: + branches: + - main + - renovate/** + +jobs: + build: + strategy: + matrix: + os: [ ubuntu-latest, windows-latest ] + version: [ 17, 19 ] + name: 'Build Java ${{ matrix.version }} - ${{ matrix.os }}' + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: ${{ matrix.version }} + cache: 'maven' + - name: Install dependencies + run: mvn install -DskipTests=true -DskipITs=true --batch-mode -D"style.color=always" --show-version + - name: Test + run: mvn verify -D"style.color=always" + env: + CUCUMBER_PUBLISH_TOKEN: ${{ secrets.CUCUMBER_PUBLISH_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2fec83d --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# IDE working files +*.iws +*.ipr +*.iml +.idea/ +.settings +.project +.classpath +lib/ + + +# Build directories +distrib/ +target/ +tmp/ +gen-external-apklibs/ +out/ + +# Build & test droppings +pom.xml.releaseBackup +pom.xml.versionsBackup +release.propertiesF +*.ser +dependency-reduced-pom.xml +*~ +libpeerconnection.log + +# OS generated files +.DS_Store* +ehthumbs.db +Icon? +Thumbs.db diff --git a/.spotless/eclipse-formatter-settings.xml b/.spotless/eclipse-formatter-settings.xml new file mode 100644 index 0000000..e85ae56 --- /dev/null +++ b/.spotless/eclipse-formatter-settings.xml @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.spotless/intellij-idea.importorder b/.spotless/intellij-idea.importorder new file mode 100644 index 0000000..d2fd346 --- /dev/null +++ b/.spotless/intellij-idea.importorder @@ -0,0 +1,6 @@ +# Organize import order using IntelliJ IDEA defaults +# Escaped hashes sort static methods last: https://github.com/diffplug/spotless/issues/306 +1= +2=javax +3=java +4=\# diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..0182d96 --- /dev/null +++ b/LICENCE @@ -0,0 +1,20 @@ +Copyright (c) The Cucumber Organisation + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 4864c38..0c7e4d6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,48 @@ # Cucumber JVM migration Cucumber-JVM migration contains [OpenRewrite](https://docs.openrewrite.org/) recipes for migrating applications using Cucumber-JVM. + +## Running migration recipes +Migration recipes can be run using the [rewrite-maven-plugin](https://docs.openrewrite.org/reference/rewrite-maven-plugin) +or [rewrite-gradle-plugin](https://docs.openrewrite.org/reference/gradle-plugin-configuration). +These can either be added to the build file of the project to be migrated or [run without modifying the build](https://docs.openrewrite.org/running-recipes/running-rewrite-on-a-maven-project-without-modifying-the-build). + + +### Upgrade to Cucumber JVM 7.x +```shell +mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \ + -Drewrite.recipeArtifactCoordinates=io.cucumber:cucumber-jvm-migration:LATEST \ + -DactiveRecipes=io.cucumber.migration.UpgradeCucumber7x +``` + +### Cucumber-Java8 migration to Cucumber-Java +```shell +mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \ + -Drewrite.recipeArtifactCoordinates=io.cucumber:cucumber-jvm-migration:LATEST \ + -DactiveRecipes=io.cucumber.migration.CucumberJava8ToJava +``` + + +## Questions, Problems, Help needed? + +Please ask on + +* [Stack Overflow](https://stackoverflow.com/questions/tagged/cucumber-jvm). +* [CucumberBDD Slack](https://cucumberbdd-slack-invite.herokuapp.com/) [direct link](https://cucumberbdd.slack.com/) + +## Bugs and Feature requests + +You can register bugs and feature requests in the +[GitHub Issue Tracker](https://github.com/cucumber/cucumber-jvm-migration/issues). + +Please bear in mind that this project is almost entirely developed by +volunteers. If you do not provide the implementation yourself (or pay someone +to do it for you), the bug might never get fixed. If it is a serious bug, other +people than you might care enough to provide a fix. + +## Contributing + +If you'd like to contribute to the documentation, checkout +[cucumber/docs.cucumber.io](https://github.com/cucumber/docs.cucumber.io) +otherwise see our +[CONTRIBUTING.md](https://github.com/cucumber/cucumber-jvm/blob/main/CONTRIBUTING.md). From 37e5a1008605f9fca8c5a1fe0b5c12da09b3e5ca Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sun, 12 Mar 2023 16:28:34 +0100 Subject: [PATCH 02/12] Add export of openrewrite/rewrite-testing-frameworks cucumber recipes --- pom.xml | 240 +++++++ .../migration/CucumberAnnotationToSuite.java | 101 +++ .../migration/CucumberJava8ClassVisitor.java | 122 ++++ ...mberJava8HookDefinitionToCucumberJava.java | 204 ++++++ ...mberJava8StepDefinitionToCucumberJava.java | 172 +++++ .../migration/DropSummaryPrinter.java | 81 +++ .../migration/RegexToCucumberExpression.java | 134 ++++ .../resources/META-INF/rewrite/cucumber.yml | 84 +++ .../CucumberAnnotationToSuiteTest.java | 47 ++ .../CucumberJava8ToCucumberJavaTest.java | 654 ++++++++++++++++++ .../migration/DropSummaryPrinterTest.java | 69 ++ .../RegexToCucumberExpressionTest.java | 255 +++++++ 12 files changed, 2163 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/io/cucumber/migration/CucumberAnnotationToSuite.java create mode 100644 src/main/java/io/cucumber/migration/CucumberJava8ClassVisitor.java create mode 100644 src/main/java/io/cucumber/migration/CucumberJava8HookDefinitionToCucumberJava.java create mode 100644 src/main/java/io/cucumber/migration/CucumberJava8StepDefinitionToCucumberJava.java create mode 100644 src/main/java/io/cucumber/migration/DropSummaryPrinter.java create mode 100644 src/main/java/io/cucumber/migration/RegexToCucumberExpression.java create mode 100755 src/main/resources/META-INF/rewrite/cucumber.yml create mode 100644 src/test/java/io/cucumber/migration/CucumberAnnotationToSuiteTest.java create mode 100644 src/test/java/io/cucumber/migration/CucumberJava8ToCucumberJavaTest.java create mode 100644 src/test/java/io/cucumber/migration/DropSummaryPrinterTest.java create mode 100644 src/test/java/io/cucumber/migration/RegexToCucumberExpressionTest.java diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d1aac87 --- /dev/null +++ b/pom.xml @@ -0,0 +1,240 @@ + + + 4.0.0 + + + io.cucumber + cucumber-parent + 4.1.1 + + + + cucumber-jvm-migration + 0.0.1-SNAPSHOT + + Cucumber JVM Migration + Migration module to upgrade projects using cucumber-jvm. + + + 1.8 + 7.11.1 + 1674814830 + io.cucumber.migration + 7.37.3 + + + scm:git:git://github.com/cucumber/cucumber-jvm-migration.git + scm:git:git@github.com:cucumber/cucumber-jvm-migration.git + git://github.com/cucumber/cucumber-jvm-migration.git + HEAD + + + + + io.cucumber + cucumber-java + ${cucumber.version} + + + io.cucumber + cucumber-java8 + ${cucumber.version} + + + io.cucumber + cucumber-plugin + ${cucumber.version} + + + io.cucumber + cucumber-junit-platform-engine + ${cucumber.version} + + + org.junit.platform + junit-platform-suite-api + 1.9.2 + + + + org.openrewrite + rewrite-java + ${rewrite.version} + + + org.openrewrite + rewrite-gradle + ${rewrite.version} + + + org.openrewrite + rewrite-maven + ${rewrite.version} + + + + org.openrewrite + rewrite-java-17 + ${rewrite.version} + test + + + org.openrewrite + rewrite-test + ${rewrite.version} + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.2 + test + + + + org.projectlombok + lombok + 1.18.24 + provided + + + + + + build-local + + true + + + + + + + com.diffplug.spotless + spotless-maven-plugin + + + spotless-apply + compile + + apply + + + + + + + + + + + build-in-ci + + + env.CI + true + + + + + + + + com.diffplug.spotless + spotless-maven-plugin + + + spotless-check + verify + + check + + + + + + + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-maven-3-6-3-plus + + enforce + + + true + + + 3.6.3 + + + + + + enforce-java + + enforce + + + + + [11,) + + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + UTF-8 + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${base.java.version} + ${base.java.version} + + 17 + 17 + + + + + + + + com.diffplug.spotless + spotless-maven-plugin + + + + ${project.basedir}/.spotless/eclipse-formatter-settings.xml + + + ${project.basedir}/.spotless/intellij-idea.importorder + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/io/cucumber/migration/CucumberAnnotationToSuite.java b/src/main/java/io/cucumber/migration/CucumberAnnotationToSuite.java new file mode 100644 index 0000000..23cc8cb --- /dev/null +++ b/src/main/java/io/cucumber/migration/CucumberAnnotationToSuite.java @@ -0,0 +1,101 @@ +package io.cucumber.migration; + +import lombok.SneakyThrows; +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.AnnotationMatcher; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.J.ClassDeclaration; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeUtils; + +import java.text.RuleBasedCollator; +import java.time.Duration; +import java.util.Comparator; +import java.util.function.Supplier; + +public class CucumberAnnotationToSuite extends Recipe { + + private static final String IO_CUCUMBER_JUNIT_PLATFORM_ENGINE_CUCUMBER = "io.cucumber.junit.platform.engine.Cucumber"; + + private static final String SUITE = "org.junit.platform.suite.api.Suite"; + private static final String SELECT_CLASSPATH_RESOURCE = "org.junit.platform.suite.api.SelectClasspathResource"; + + @Override + public String getDisplayName() { + return "Replace @Cucumber with @Suite"; + } + + @Override + public String getDescription() { + return "Replace @Cucumber with @Suite and @SelectClasspathResource(\"cucumber/annotated/class/package\")."; + } + + @Override + public @Nullable Duration getEstimatedEffortPerOccurrence() { + return Duration.ofMinutes(2); + } + + @Override + protected TreeVisitor getSingleSourceApplicableTest() { + return new UsesType<>(IO_CUCUMBER_JUNIT_PLATFORM_ENGINE_CUCUMBER); + } + + @Override + protected JavaIsoVisitor getVisitor() { + final AnnotationMatcher cucumberAnnoMatcher = new AnnotationMatcher( + "@" + IO_CUCUMBER_JUNIT_PLATFORM_ENGINE_CUCUMBER); + + return new JavaIsoVisitor() { + @SneakyThrows + @Override + public J.ClassDeclaration visitClassDeclaration(ClassDeclaration cd, ExecutionContext ctx) { + ClassDeclaration classDecl = super.visitClassDeclaration(cd, ctx); + if (classDecl.getAllAnnotations().stream().noneMatch(cucumberAnnoMatcher::matches)) { + return classDecl; + } + + Supplier javaParserSupplier = () -> JavaParser.fromJavaVersion() + .classpathFromResources(ctx, "junit-platform-suite-api-1.9.2") + .build(); + + JavaType.FullyQualified classFqn = TypeUtils.asFullyQualified(classDecl.getType()); + if (classFqn != null) { + maybeRemoveImport(IO_CUCUMBER_JUNIT_PLATFORM_ENGINE_CUCUMBER); + maybeAddImport(SUITE); + maybeAddImport(SELECT_CLASSPATH_RESOURCE); + + final String classDeclPath = classFqn.getPackageName().replace('.', '/'); + classDecl = classDecl + .withLeadingAnnotations(ListUtils.map(classDecl.getLeadingAnnotations(), ann -> { + if (cucumberAnnoMatcher.matches(ann)) { + String code = "@SelectClasspathResource(\"#{}\")"; + JavaTemplate template = JavaTemplate.builder(this::getCursor, code) + .javaParser(javaParserSupplier) + .imports(SELECT_CLASSPATH_RESOURCE) + .build(); + return ann.withTemplate(template, ann.getCoordinates().replace(), classDeclPath); + } + return ann; + })); + classDecl = classDecl.withTemplate(JavaTemplate.builder(this::getCursor, "@Suite") + .javaParser(javaParserSupplier) + .imports(SUITE) + .build(), + classDecl.getCoordinates().addAnnotation(Comparator.comparing( + J.Annotation::getSimpleName, + new RuleBasedCollator("< SelectClasspathResource")))); + } + return classDecl; + } + }; + } + +} diff --git a/src/main/java/io/cucumber/migration/CucumberJava8ClassVisitor.java b/src/main/java/io/cucumber/migration/CucumberJava8ClassVisitor.java new file mode 100644 index 0000000..fbf48f9 --- /dev/null +++ b/src/main/java/io/cucumber/migration/CucumberJava8ClassVisitor.java @@ -0,0 +1,122 @@ +package io.cucumber.migration; + +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; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +class CucumberJava8ClassVisitor extends JavaIsoVisitor { + + 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 ctx) { + J.ClassDeclaration classDeclaration = super.visitClassDeclaration(cd, ctx); + 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 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() { + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration md, ExecutionContext p) { + J.MethodDeclaration methodDeclaration = super.visitMethodDeclaration(md, p); + if (methodDeclaration.isConstructor() && (methodDeclaration.getBody() == null + || methodDeclaration.getBody().getStatements().isEmpty())) { + // noinspection DataFlowIssue + 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() + .classpathFromResources(ctx, "cucumber-java-7.11.0", "cucumber-java8-7.11.0") + .build()) + .imports(replacementImport) + .build(), + coordinatesForNewMethod(classDeclaration.getBody()), + templateParameters); + } + + /** + * Remove imports & usage of Cucumber-Java8 interfaces. + * + * @return retained implementing interfaces + */ + private List filterImplementingInterfaces(J.ClassDeclaration classDeclaration) { + List 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. + */ + 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 -> ann.getAnnotationType().getType() != null + && ((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())); + } +} diff --git a/src/main/java/io/cucumber/migration/CucumberJava8HookDefinitionToCucumberJava.java b/src/main/java/io/cucumber/migration/CucumberJava8HookDefinitionToCucumberJava.java new file mode 100644 index 0000000..3c52e58 --- /dev/null +++ b/src/main/java/io/cucumber/migration/CucumberJava8HookDefinitionToCucumberJava.java @@ -0,0 +1,204 @@ +package io.cucumber.migration; + +import lombok.EqualsAndHashCode; +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; +import org.openrewrite.marker.SearchResult; + +import java.time.Duration; +import java.util.List; + +@Value +@EqualsAndHashCode(callSuper = true) +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 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 LambdaGlue hook definitions with new annotated methods with the same body."; + } + + @Override + public @Nullable Duration getEstimatedEffortPerOccurrence() { + return Duration.ofMinutes(10); + } + + @Override + protected TreeVisitor getVisitor() { + return new CucumberJava8HooksVisitor(); + } + + static final class CucumberJava8HooksVisitor extends JavaVisitor { + @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 SearchResult.found(methodInvocation, "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 + // noinspection DataFlowIssue + 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; + */ + HookArguments parseHookArguments(String methodName, List 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 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 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 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 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 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 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..75ea0d8 --- /dev/null +++ b/src/main/resources/META-INF/rewrite/cucumber.yml @@ -0,0 +1,84 @@ +--- +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.* 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..9cc4320 --- /dev/null +++ b/src/test/java/io/cucumber/migration/RegexToCucumberExpressionTest.java @@ -0,0 +1,255 @@ +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; +import static org.openrewrite.java.Assertions.version; + +@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() { + rewriteRun( + version( + // language=java + 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); + } + + } + """), + 17)); + } + + @Nested + @DisplayName("should convert") + class ShouldConvert { + + @Test + void only_leading_anchor() { + rewriteRun(version(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() { + } + }"""), + 17)); + } + + @Test + void only_trailing_anchor() { + rewriteRun(version(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() { + } + }"""), + 17)); + } + + @Test + void forward_slashes() { + rewriteRun(version(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() { + } + }"""), + 17)); + } + + } + + @Nested + @DisplayName("should not convert") + class ShouldNotConvert { + + @Test + void unrecognized_capturing_groups() { + rewriteRun(version(java(""" + package com.example.app; + + import io.cucumber.java.en.Given; + + public class ExpressionDefinitions { + @Given("^some (foo|bar)$") + public void five_cukes(String fooOrBar) { + } + }"""), + 17)); + } + + @Test + void integer_matchers() { + rewriteRun(version(java(""" + package com.example.app; + + import io.cucumber.java.en.Given; + + public class ExpressionDefinitions { + @Given("^(\\\\d+) cukes$") + public void int_cukes(int cukes) { + } + }"""), + 17)); + } + + @Test + void regex_question_mark_optional() { + rewriteRun(version(java(""" + package com.example.app; + + import io.cucumber.java.en.Given; + + public class ExpressionDefinitions { + @Given("^cukes?$") + public void cukes() { + } + }"""), + 17)); + } + + @Test + void regex_one_or_more() { + rewriteRun(version(java(""" + package com.example.app; + + import io.cucumber.java.en.Given; + + public class ExpressionDefinitions { + @Given("^cukes+$") + public void cukes() { + } + }"""), + 17)); + } + + @Test + void disabled() { + rewriteRun(version(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() { + } + }"""), + 17)); + } + + } + +} From 058496f585969d891e07fd2b092b8bdd8be1ea1b Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sun, 12 Mar 2023 16:40:49 +0100 Subject: [PATCH 03/12] Add language hints to RegexToCucumberExpressionTest --- .../RegexToCucumberExpressionTest.java | 142 +++++++++--------- 1 file changed, 68 insertions(+), 74 deletions(-) diff --git a/src/test/java/io/cucumber/migration/RegexToCucumberExpressionTest.java b/src/test/java/io/cucumber/migration/RegexToCucumberExpressionTest.java index 9cc4320..bdba3e9 100644 --- a/src/test/java/io/cucumber/migration/RegexToCucumberExpressionTest.java +++ b/src/test/java/io/cucumber/migration/RegexToCucumberExpressionTest.java @@ -9,7 +9,6 @@ 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 RegexToCucumberExpressionTest implements RewriteTest { @@ -22,71 +21,66 @@ public void defaults(RecipeSpec spec) { @Test void regexToCucumberExpression() { - rewriteRun( - version( - // language=java - java( - """ - package com.example.app; + // 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 io.cucumber.java.Before; + import io.cucumber.java.en.Given; + import io.cucumber.java.en.Then; - import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.junit.jupiter.api.Assertions.assertEquals; - public class ExpressionDefinitions { + public class ExpressionDefinitions { - private int a; + private int a; - @Before - public void before() { - a = 0; - } + @Before + public void before() { + a = 0; + } - @Given("^five cukes$") - public void five_cukes() { - a = 5; - } + @Given("^five cukes$") + public void five_cukes() { + a = 5; + } - @Then("^I expect (\\\\d+)$") - public void i_expect_int(Integer c) { - assertEquals(c, a); - } + @Then("^I expect (\\\\d+)$") + public void i_expect_int(Integer c) { + assertEquals(c, a); + } - } - """, - """ - package com.example.app; + } + """, """ + package com.example.app; - import io.cucumber.java.Before; - import io.cucumber.java.en.Given; - import io.cucumber.java.en.Then; + 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; + import static org.junit.jupiter.api.Assertions.assertEquals; - public class ExpressionDefinitions { + public class ExpressionDefinitions { - private int a; + private int a; - @Before - public void before() { - a = 0; - } + @Before + public void before() { + a = 0; + } - @Given("five cukes") - public void five_cukes() { - a = 5; - } + @Given("five cukes") + public void five_cukes() { + a = 5; + } - @Then("^I expect (\\\\d+)$") - public void i_expect_int(Integer c) { - assertEquals(c, a); - } + @Then("^I expect (\\\\d+)$") + public void i_expect_int(Integer c) { + assertEquals(c, a); + } - } - """), - 17)); + } + """)); } @Nested @@ -95,7 +89,8 @@ class ShouldConvert { @Test void only_leading_anchor() { - rewriteRun(version(java(""" + // language=java + rewriteRun(java(""" package com.example.app; import io.cucumber.java.en.Given; @@ -113,13 +108,13 @@ public class ExpressionDefinitions { @Given("five cukes") public void five_cukes() { } - }"""), - 17)); + }""")); } @Test void only_trailing_anchor() { - rewriteRun(version(java(""" + // language=java + rewriteRun(java(""" package com.example.app; import io.cucumber.java.en.Given; @@ -137,13 +132,13 @@ public class ExpressionDefinitions { @Given("five cukes") public void five_cukes() { } - }"""), - 17)); + }""")); } @Test void forward_slashes() { - rewriteRun(version(java(""" + // language=java + rewriteRun(java(""" package com.example.app; import io.cucumber.java.en.Given; @@ -161,8 +156,7 @@ public class ExpressionDefinitions { @Given("five cukes") public void five_cukes() { } - }"""), - 17)); + }""")); } } @@ -173,7 +167,8 @@ class ShouldNotConvert { @Test void unrecognized_capturing_groups() { - rewriteRun(version(java(""" + // language=java + rewriteRun(java(""" package com.example.app; import io.cucumber.java.en.Given; @@ -182,13 +177,13 @@ public class ExpressionDefinitions { @Given("^some (foo|bar)$") public void five_cukes(String fooOrBar) { } - }"""), - 17)); + }""")); } @Test void integer_matchers() { - rewriteRun(version(java(""" + // language=java + rewriteRun(java(""" package com.example.app; import io.cucumber.java.en.Given; @@ -197,13 +192,13 @@ public class ExpressionDefinitions { @Given("^(\\\\d+) cukes$") public void int_cukes(int cukes) { } - }"""), - 17)); + }""")); } @Test void regex_question_mark_optional() { - rewriteRun(version(java(""" + // language=java + rewriteRun(java(""" package com.example.app; import io.cucumber.java.en.Given; @@ -212,13 +207,13 @@ public class ExpressionDefinitions { @Given("^cukes?$") public void cukes() { } - }"""), - 17)); + }""")); } @Test void regex_one_or_more() { - rewriteRun(version(java(""" + // language=java + rewriteRun(java(""" package com.example.app; import io.cucumber.java.en.Given; @@ -227,13 +222,13 @@ public class ExpressionDefinitions { @Given("^cukes+$") public void cukes() { } - }"""), - 17)); + }""")); } @Test void disabled() { - rewriteRun(version(java(""" + // language=java + rewriteRun(java(""" package com.example.app; import io.cucumber.java.en.Given; @@ -246,8 +241,7 @@ public void disabled() { @Given("trigger getSingleSourceApplicableTest") public void trigger() { } - }"""), - 17)); + }""")); } } From ba6556dec8574c58621d46ff0f4657e46969c2fb Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sun, 12 Mar 2023 16:47:29 +0100 Subject: [PATCH 04/12] Set cucumber.version to 7.11.0 to match test classpathFromResources --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d1aac87..e7197cb 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 1.8 - 7.11.1 + 7.11.0 1674814830 io.cucumber.migration 7.37.3 From e11cf7139166fec4fd304f12e3ed4bc89859d54d Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sun, 12 Mar 2023 17:13:04 +0100 Subject: [PATCH 05/12] Drop classpathFromResources --- .../io/cucumber/migration/CucumberJava8ClassVisitor.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/cucumber/migration/CucumberJava8ClassVisitor.java b/src/main/java/io/cucumber/migration/CucumberJava8ClassVisitor.java index fbf48f9..cc0b05c 100644 --- a/src/main/java/io/cucumber/migration/CucumberJava8ClassVisitor.java +++ b/src/main/java/io/cucumber/migration/CucumberJava8ClassVisitor.java @@ -64,9 +64,8 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration md, Execut return classDeclaration .withImplements(retained) .withTemplate(JavaTemplate.builder(this::getCursor, template) - .javaParser(() -> JavaParser.fromJavaVersion() - .classpathFromResources(ctx, "cucumber-java-7.11.0", "cucumber-java8-7.11.0") - .build()) + .javaParser( + () -> JavaParser.fromJavaVersion().classpath("cucumber-java", "cucumber-java8").build()) .imports(replacementImport) .build(), coordinatesForNewMethod(classDeclaration.getBody()), From 4cd5a7135aa9518bcfc0fe3d9282693b03f957cb Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sun, 12 Mar 2023 17:30:49 +0100 Subject: [PATCH 06/12] Drop another classpathFromResources --- .../java/io/cucumber/migration/CucumberAnnotationToSuite.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/cucumber/migration/CucumberAnnotationToSuite.java b/src/main/java/io/cucumber/migration/CucumberAnnotationToSuite.java index 23cc8cb..d33a919 100644 --- a/src/main/java/io/cucumber/migration/CucumberAnnotationToSuite.java +++ b/src/main/java/io/cucumber/migration/CucumberAnnotationToSuite.java @@ -63,7 +63,7 @@ public J.ClassDeclaration visitClassDeclaration(ClassDeclaration cd, ExecutionCo } Supplier javaParserSupplier = () -> JavaParser.fromJavaVersion() - .classpathFromResources(ctx, "junit-platform-suite-api-1.9.2") + .classpath("junit-platform-suite-api") .build(); JavaType.FullyQualified classFqn = TypeUtils.asFullyQualified(classDecl.getType()); From 818ef21566b56cbce60e6399a494b53082531bad Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sat, 18 Mar 2023 13:03:47 +0100 Subject: [PATCH 07/12] Remove stale/lock as suggested Unless I misunderstood --- .github/lock.yml | 34 ---------------------------------- .github/stale.yml | 17 ----------------- 2 files changed, 51 deletions(-) delete mode 100644 .github/lock.yml delete mode 100644 .github/stale.yml diff --git a/.github/lock.yml b/.github/lock.yml deleted file mode 100644 index 3a474cb..0000000 --- a/.github/lock.yml +++ /dev/null @@ -1,34 +0,0 @@ -# Configuration for lock-threads - https://github.com/dessant/lock-threads - -# Number of days of inactivity before a closed issue or pull request is locked -daysUntilLock: 365 - -# Issues and pull requests with these labels will not be locked. Set to `[]` to disable -exemptLabels: [] - -# Label to add before locking, such as `outdated`. Set to `false` to disable -lockLabel: false - -# Comment to post before locking. Set to `false` to disable -lockComment: > - This thread has been automatically locked since there has not been - any recent activity after it was closed. Please open a new issue for - related bugs. - -# Assign `resolved` as the reason for locking. Set to `false` to disable -setLockReason: true - -# Limit to only `issues` or `pulls` -# only: issues - -# Optionally, specify configuration settings just for `issues` or `pulls` -# issues: -# exemptLabels: -# - help-wanted -# lockLabel: outdated - -# pulls: -# daysUntilLock: 30 - -# Repository to extend settings from -# _extends: repo diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index b7a1f32..0000000 --- a/.github/stale.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 300 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 60 -# Issues with these labels will never be considered stale -exemptLabels: - - ":safety_pin: pinned" -# Label to use when marking an issue as stale -staleLabel: ":hourglass: stale" -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed in two months if no further activity occurs. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: > - This issue has been automatically closed because of inactivity. - You can support the Cucumber core team on [opencollective](https://opencollective.com/cucumber). From 815158151c798866edda9e091ca5b2d71a52e454 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sat, 18 Mar 2023 13:04:24 +0100 Subject: [PATCH 08/12] Adopt BOMs for dependency versions; Drop spotless --- pom.xml | 121 ++++++++++++++------------------------------------------ 1 file changed, 30 insertions(+), 91 deletions(-) diff --git a/pom.xml b/pom.xml index e7197cb..5d5dd13 100644 --- a/pom.xml +++ b/pom.xml @@ -20,9 +20,11 @@ 1.8 7.11.0 + 5.9.2 + 1.17.0 + UTF-8 1674814830 io.cucumber.migration - 7.37.3 scm:git:git://github.com/cucumber/cucumber-jvm-migration.git @@ -31,65 +33,80 @@ HEAD + + + + io.cucumber + cucumber-bom + ${cucumber.version} + pom + import + + + org.junit + junit-bom + ${junit.version} + pom + import + + + org.openrewrite.recipe + rewrite-recipe-bom + ${rewrite.version} + pom + import + + + + io.cucumber cucumber-java - ${cucumber.version} io.cucumber cucumber-java8 - ${cucumber.version} io.cucumber cucumber-plugin - ${cucumber.version} io.cucumber cucumber-junit-platform-engine - ${cucumber.version} org.junit.platform junit-platform-suite-api - 1.9.2 org.openrewrite rewrite-java - ${rewrite.version} org.openrewrite rewrite-gradle - ${rewrite.version} org.openrewrite rewrite-maven - ${rewrite.version} org.openrewrite rewrite-java-17 - ${rewrite.version} test org.openrewrite rewrite-test - ${rewrite.version} test org.junit.jupiter junit-jupiter-engine - 5.9.2 test @@ -101,65 +118,6 @@ - - - build-local - - true - - - - - - - com.diffplug.spotless - spotless-maven-plugin - - - spotless-apply - compile - - apply - - - - - - - - - - - build-in-ci - - - env.CI - true - - - - - - - - com.diffplug.spotless - spotless-maven-plugin - - - spotless-check - verify - - check - - - - - - - - - - @@ -200,7 +158,7 @@ org.apache.maven.plugins maven-resources-plugin - UTF-8 + ${resources.encoding} @@ -216,25 +174,6 @@ - - - - - com.diffplug.spotless - spotless-maven-plugin - - - - ${project.basedir}/.spotless/eclipse-formatter-settings.xml - - - ${project.basedir}/.spotless/intellij-idea.importorder - - - - - - \ No newline at end of file From 72c3b47bd7eecc33a2c6bc2bbf56fb29ff61a7be Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sat, 18 Mar 2023 13:15:12 +0100 Subject: [PATCH 09/12] Delete eclipse-formatter-settings.xml --- .spotless/eclipse-formatter-settings.xml | 365 ----------------------- 1 file changed, 365 deletions(-) delete mode 100644 .spotless/eclipse-formatter-settings.xml diff --git a/.spotless/eclipse-formatter-settings.xml b/.spotless/eclipse-formatter-settings.xml deleted file mode 100644 index e85ae56..0000000 --- a/.spotless/eclipse-formatter-settings.xml +++ /dev/null @@ -1,365 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 2d93cc35526f6a788e760c6711e811ff860d124d Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sat, 18 Mar 2023 13:15:28 +0100 Subject: [PATCH 10/12] Delete intellij-idea.importorder --- .spotless/intellij-idea.importorder | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .spotless/intellij-idea.importorder diff --git a/.spotless/intellij-idea.importorder b/.spotless/intellij-idea.importorder deleted file mode 100644 index d2fd346..0000000 --- a/.spotless/intellij-idea.importorder +++ /dev/null @@ -1,6 +0,0 @@ -# Organize import order using IntelliJ IDEA defaults -# Escaped hashes sort static methods last: https://github.com/diffplug/spotless/issues/306 -1= -2=javax -3=java -4=\# From 10dc7061b4fa3d104a38b1b0a08031d486377d41 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sat, 18 Mar 2023 18:24:33 +0100 Subject: [PATCH 11/12] Switch to 0.1.0-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5d5dd13..22b6850 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ cucumber-jvm-migration - 0.0.1-SNAPSHOT + 0.1.0-SNAPSHOT Cucumber JVM Migration Migration module to upgrade projects using cucumber-jvm. From 3650cb32d00b35dd7136ad084d52dbf10dcaadbd Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 21 Mar 2023 23:45:02 +0100 Subject: [PATCH 12/12] acceptTransitive: true on junit-platform-suite As per https://github.com/openrewrite/rewrite-testing-frameworks/pull/323 --- src/main/resources/META-INF/rewrite/cucumber.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/META-INF/rewrite/cucumber.yml b/src/main/resources/META-INF/rewrite/cucumber.yml index 75ea0d8..91d0511 100755 --- a/src/main/resources/META-INF/rewrite/cucumber.yml +++ b/src/main/resources/META-INF/rewrite/cucumber.yml @@ -82,3 +82,4 @@ recipeList: artifactId: junit-platform-suite version: 1.9.x onlyIfUsing: org.junit.platform.suite.api.* + acceptTransitive: true