diff --git a/.github/workflows/analyse.yml b/.github/workflows/analyse.yml new file mode 100644 index 00000000..5a199243 --- /dev/null +++ b/.github/workflows/analyse.yml @@ -0,0 +1,62 @@ +name: SonarCloud +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'zulu' # Alternative distribution options are available. + - name: Cache SonarCloud packages + uses: actions/cache@v4 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + - name: "[Maven] Verify & generate JaCoCo XML" + env: + MAVEN_OPTS: '-Dstyle.color=always' + MAVEN_SWITCHES: >- + --show-version + --no-transfer-progress + --update-snapshots + --fail-at-end + --batch-mode + run: mvn ${{ env.MAVEN_SWITCHES }} -P coverage verify + + - name: Upload maven build result + uses: actions/upload-artifact@v4 + with: + name: maven-build-target-folder + path: target + + - name: List current location + run: find . + + - name: Analyze project + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + MAVEN_OPTS: >- + -Dsonar.projectKey=Philzen_rewrite-recipe-testng-to-junit-jupiter + -Dsonar.organization=philzen + -Dsonar.host.url=https://sonarcloud.io + run: mvn -B org.sonarsource.scanner.maven:sonar-maven-plugin:sonar ${{ env.MAVEN_OPTS }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42aa4d4e..bd294e60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,5 @@ --- -name: ci +name: Build on: push: @@ -36,8 +36,6 @@ jobs: uses: gradle/actions/setup-gradle@v3 with: arguments: ${{ env.GRADLE_SWITCHES }} build test - - name: verify - run: mvn --show-version --no-transfer-progress --update-snapshots --fail-at-end --batch-mode -Dstyle.color=always verify publish-snapshots: needs: [build] diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 00000000..a2702178 --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,3 @@ +gradle=8.8 +java=21.0.3-tem +maven=3.9.7 diff --git a/README.md b/README.md index 35e42253..c39d0071 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,93 @@ -## Rewrite recipe starter +# TestNG to Junit5 recipe   [![Compatible with Java 8](https://img.shields.io/badge/Works%20on%20Java-8-seagreen?logo=openjdk&labelColor=snow&logoColor=black)](#) -This repository serves as a template for building your own recipe JARs and publishing them to a repository where they can be applied on [app.moderne.io](https://app.moderne.io) against all the public OSS code that is included there. +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/philzen/rewrite-recipe-testng-to-junit-jupiter/ci.yml?logo=github)](https://github.com/Philzen/rewrite-recipe-testng-to-junit-jupiter/actions/workflows/ci.yml)   +[![Sonar Coverage](https://img.shields.io/sonar/coverage/Philzen_rewrite-recipe-testng-to-junit-jupiter?server=https%3A%2F%2Fsonarcloud.io&logo=sonarcloud&label=Coverage)](https://sonarcloud.io/summary/new_code?id=Philzen_rewrite-recipe-testng-to-junit-jupiter) -We've provided a sample recipe (NoGuavaListsNewArray) and a sample test class. Both of these exist as placeholders, and they should be replaced by whatever recipe you are interested in writing. +Converts [TestNG](https://testng.org/) test annotations and assertions to +[Junit 5](https://junit.org/junit5/docs/current/user-guide/). -To begin, fork this repository and customize it by: +Inspired by the [Migrate JUnit 4 @Test annotations to JUnit 5](https://docs.openrewrite.org/recipes/java/testing/junit5/updatetestannotation) recipe -1. Changing the root project name in `settings.gradle.kts`. -2. Changing the `group` in `build.gradle.kts`. -3. Changing the package structure from `com.yourorg` to whatever you want. +## Usage -## Getting started +### Prerequisites -Familiarize yourself with the [OpenRewrite documentation](https://docs.openrewrite.org/), in particular the [concepts & explanations](https://docs.openrewrite.org/concepts-explanations) op topics like the [lossless semantic trees](https://docs.openrewrite.org/concepts-explanations/lossless-semantic-trees), [recipes](https://docs.openrewrite.org/concepts-explanations/recipes) and [visitors](https://docs.openrewrite.org/concepts-explanations/visitors). +- cloned the project to your local machine +- if you're an SDKMAN!-Andy, simply run `sdk env` from the project directory + otherwise ensure: + - JDK 17+ + - Maven 3.9.x *or* Gradle 8.x -You might be interested to watch some of the [videos available on OpenRewrite and Moderne](https://www.youtube.com/@moderne-auto-remediation). + are provided on your system. -Once you want to dive into the code there is a [comprehensive getting started guide](https://docs.openrewrite.org/authoring-recipes/recipe-development-environment) -available in the OpenRewrite docs that provides more details than the below README. +### Build & install this recipe to your local repository -## Reference recipes +From the project directory, run one of the following: -* [META-INF/rewrite/stringutils.yml](./src/main/resources/META-INF/rewrite/stringutils.yml) - A declarative YAML recipe that replaces usages of `org.springframework.util.StringUtils` with `org.apache.commons.lang3.StringUtils`. - - [UseApacheStringUtilsTest](src/test/java/org/philzen/oss/UseApacheStringUtilsTest.java) - A test class for the `com.yourorg.UseApacheStringUtils` recipe. -* [NoGuavaListsNewArrayList.java](src/main/java/org/philzen/oss/NoGuavaListsNewArrayList.java) - An imperative Java recipe that replaces usages of `com.google.common.collect.Lists` with `new ArrayList<>()`. - - [NoGuavaListsNewArrayListTest.java](src/test/java/org/philzen/oss/NoGuavaListsNewArrayListTest.java) - A test class for the `NoGuavaListsNewArrayList` recipe. -* [SimplifyTernary](src/main/java/org/philzen/oss/SimplifyTernary.java) - An Refaster style recipe that simplifies ternary expressions. - - [SimplifyTernaryTest](src/test/java/org/philzen/oss/SimplifyTernaryTest.java) - A test class for the `SimplifyTernary` recipe. -* [AssertEqualsToAssertThat](src/main/java/org/philzen/oss/AssertEqualsToAssertThat.java) - An imperative Java recipe that replaces JUnit's `assertEquals` with AssertJ's `assertThat`, to show how to handle classpath dependencies. - - [AssertEqualsToAssertThatTest](src/test/java/org/philzen/oss/AssertEqualsToAssertThatTest.java) - A test class for the `AssertEqualsToAssertThat` recipe. -* [AppendToReleaseNotes](src/main/java/org/philzen/oss/AppendToReleaseNotes.java) - A ScanningRecipe that appends a message to the release notes of a project. - - [AppendToReleaseNotesTest](src/test/java/org/philzen/oss/AppendToReleaseNotesTest.java) - A test class for the `AppendToReleaseNotes` recipe. -* [ClassHierarchy](src/main/java/org/philzen/oss/ClassHierarchy.java) - A recipe that demonstrates how to produce a data table on the class hierarchy of a project. - - [ClassHierarchyTest](src/test/java/org/philzen/oss/ClassHierarchyTest.java) - A test class for the `ClassHierarchy` recipe. -* [UpdateConcoursePipeline](src/main/java/org/philzen/oss/UpdateConcoursePipeline.java) - A recipe that demonstrates how to update a Concourse pipeline, as an example of operating on Yaml files. - - [UpdateConcoursePipelineTest](src/test/java/org/philzen/oss/UpdateConcoursePipelineTest.java) - A test class for the `UpdateConcoursePipeline` recipe. +
Maven +

-## Local Publishing for Testing +```bash +mvn install -DskipTests +``` +

+
+ +
Gradle +

-Before you publish your recipe module to an artifact repository, you may want to try it out locally. -To do this on the command line, run: ```bash ./gradlew publishToMavenLocal # or ./gradlew pTML # or mvn install ``` +

+
+ This will publish to your local maven repository, typically under `~/.m2/repository`. -Replace the groupId, artifactId, recipe name, and version in the below snippets with the ones that correspond to your recipe. +### Migrate a project + +
Maven +

+ +In the `pom.xml` of a different project you wish to run the recipe on, +make it a plugin dependency of rewrite-maven-plugin: -In the pom.xml of a different project you wish to test your recipe out in, make your recipe module a plugin dependency of rewrite-maven-plugin: ```xml - - - - org.openrewrite.maven - rewrite-maven-plugin - RELEASE - - - com.yourorg.NoGuavaListsNewArrayList - - - - - com.yourorg - rewrite-recipe-starter - 0.1.0-SNAPSHOT - - - - - + + + + org.openrewrite.maven + rewrite-maven-plugin + 5.33.0 + + + org.philzen.oss.testng.MigrateToJunit5 + + + + + org.philzen.oss + rewrite-testng-to-junit5 + 1.0.1-SNAPSHOT + + + + + ``` +Now run the recipe via `mvn rewrite:run`. +

+ +
Gradle +

Unlike Maven, Gradle must be explicitly configured to resolve dependencies from Maven local. -The root project of your Gradle build, make your recipe module a dependency of the `rewrite` configuration: +In the root project of a gradle build that you wish to run this recipe on, +make it a dependency of the `rewrite` configuration: ```groovy plugins { @@ -99,45 +109,5 @@ rewrite { } ``` -Now you can run `mvn rewrite:run` or `gradlew rewriteRun` to run your recipe. - -## Publishing to Artifact Repositories - -This project is configured to publish to Moderne's open artifact repository (via the `publishing` task at the bottom of -the `build.gradle.kts` file). If you want to publish elsewhere, you'll want to update that task. -[app.moderne.io](https://app.moderne.io) can draw recipes from the provided repository, as well as from [Maven Central](https://search.maven.org). - -Note: -Running the publish task _will not_ update [app.moderne.io](https://app.moderne.io), as only Moderne employees can -add new recipes. If you want to add your recipe to [app.moderne.io](https://app.moderne.io), please ask the -team in [Slack](https://join.slack.com/t/rewriteoss/shared_invite/zt-nj42n3ea-b~62rIHzb3Vo0E1APKCXEA) or in [Discord](https://discord.gg/xk3ZKrhWAb). - -These other docs might also be useful for you depending on where you want to publish the recipe: - -* Sonatype's instructions for [publishing to Maven Central](https://maven.apache.org/repository/guide-central-repository-upload.html) -* Gradle's instructions on the [Gradle Publishing Plugin](https://docs.gradle.org/current/userguide/publishing\_maven.html). - -### From Github Actions - -The `.github` directory contains a Github action that will push a snapshot on every successful build. - -Run the release action to publish a release version of a recipe. - -### From the command line - -To build a snapshot, run `./gradlew snapshot publish` to build a snapshot and publish it to Moderne's open artifact repository for inclusion at [app.moderne.io](https://app.moderne.io). - -To build a release, run `./gradlew final publish` to tag a release and publish it to Moderne's open artifact repository for inclusion at [app.moderne.io](https://app.moderne.io). - - -## Applying OpenRewrite recipe development best practices - -We maintain a collection of [best practices for writing OpenRewrite recipes](https://docs.openrewrite.org/recipes/recipes/openrewritebestpractices). -You can apply these recommendations to your recipes by running the following command: -```bash -./gradlew rewriteRun -Drewrite.activeRecipe=org.openrewrite.recipes.OpenRewriteBestPractices -``` -or -```bash -mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-recommendations:RELEASE -Drewrite.activeRecipes=org.openrewrite.recipes.OpenRewriteBestPractices -``` \ No newline at end of file +Now run the recipe via `gradlew rewriteRun`. +

diff --git a/build.gradle.kts b/build.gradle.kts index ef930807..13d5e573 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,6 +20,10 @@ plugins { group = "org.philzen.oss" description = "Rewrite recipes." +tasks.named("compileJava") { + options.release.set(8) +} + dependencies { // The bom version can also be set to a specific version // https://github.com/openrewrite/rewrite-recipe-bom/releases @@ -29,6 +33,7 @@ dependencies { implementation("org.openrewrite.recipe:rewrite-java-dependencies") implementation("org.openrewrite:rewrite-yaml") implementation("org.assertj:assertj-core:3.24.2") + runtimeOnly("org.openrewrite:rewrite-java-8") runtimeOnly("org.openrewrite:rewrite-java-17") // Refaster style recipes need the rewrite-templating annotation processor and dependency for generated recipes @@ -44,12 +49,23 @@ dependencies { runtimeOnly("ch.qos.logback:logback-classic:1.2.+") // Our recipe converts Guava's `Lists` type - testRuntimeOnly("com.google.guava:guava:latest.release") + testImplementation("com.google.guava:guava:latest.release") testRuntimeOnly("org.apache.commons:commons-lang3:latest.release") testRuntimeOnly("org.springframework:spring-core:latest.release") // Contains the OpenRewriteBestPractices recipe, which you can apply to your recipes rewrite("org.openrewrite.recipe:rewrite-recommendations:latest.release") + + // ↓ Classpath resource for MigrateTestNg* recipes + implementation("org.testng:testng:7.5.1") // 7.5.x is the last Java 8 compatible version: https://github.com/testng-team/testng/issues/2775 + + // ↓ also make jupiter available for refaster compilation + compileOnly("org.junit.jupiter:junit-jupiter-api:latest.release") + + // ↓ to allow using testing recipes in our recipe list + testRuntimeOnly("org.openrewrite.recipe:rewrite-testing-frameworks:latest.release") { + exclude("org.testcontainers", "testcontainers") + } } signing { diff --git a/docs/NOT-IMPLEMENTED.md b/docs/NOT-IMPLEMENTED.md new file mode 100644 index 00000000..feaf91de --- /dev/null +++ b/docs/NOT-IMPLEMENTED.md @@ -0,0 +1,157 @@ +The following is a list of `@org.testng.annotations.Test` annotation attributes for which +there is no clear plan on how to migrate them yet. + +Any contributions or suggestions for equivalents in a JUnit5 test setup are welcome. + +- ```java + /** + * The list of groups this method depends on. Every method member of one of these groups is + * guaranteed to have been invoked before this method. Furthermore, if any of these methods was + * not a SUCCESS, this test method will not be run and will be flagged as a SKIP. + * + * @return the value + */ + String[] dependsOnGroups() default {}; + ``` + +- ```java + /** + * The list of methods this method depends on. There is no guarantee on the order on which the + * methods depended upon will be run, but you are guaranteed that all these methods will be run + * before the test method that contains this annotation is run. Furthermore, if any of these + * methods was not a SUCCESS, this test method will not be run and will be flagged as a SKIP. + * + *

If some of these methods have been overloaded, all the overloaded versions will be run. + * + * @return the value + */ + String[] dependsOnMethods() default {}; + ``` + +- ```java + /** + * The maximum number of milliseconds that the total number of invocations on this test method + * should take. This annotation will be ignored if the attribute invocationCount is not specified + * on this method. If it hasn't returned after this time, it will be marked as a FAIL. + * + * @return the value (default 0) + */ + long invocationTimeOut() default 0; + ``` + +- ```java + /** + * The number of times this method should be invoked. + * + * @return the value (default 1) + */ + int invocationCount() default 1; + ``` + +- ```java + /** + * The size of the thread pool for this method. The method will be invoked from multiple threads + * as specified by invocationCount. Note: this attribute is ignored if invocationCount is not + * specified + * + * @return the value (default 0) + */ + int threadPoolSize() default 0; + ``` + +- ```java + /** + * The percentage of success expected from this method. + * + * @return the value (default 100) + */ + int successPercentage() default 100; + ``` + +- ```java + /** + * If set to true, this test method will always be run even if it depends on a method that failed. + * This attribute will be ignored if this test doesn't depend on any method or group. + * + * @return the value (default false) + */ + boolean alwaysRun() default false; + ``` + +- ```java + /** + * The name of the suite this test class should be placed in. This attribute is ignore if @Test is + * not at the class level. + * + * @return the value (default empty) + */ + String suiteName() default ""; + ``` + +- ```java + /** + * The name of the test this test class should be placed in. This attribute is ignore if @Test is + * not at the class level. + * + * @return the value (default empty) + */ + String testName() default ""; + ``` + +- ```java + /** + * If set to true, all the methods on this test class are guaranteed to run in the same thread, + * even if the tests are currently being run with parallel="true". + * + *

This attribute can only be used at the class level and will be ignored if used at the method + * level. + * + * @return true if single threaded (default false) + */ + boolean singleThreaded() default false; + ``` + +- ```java + /** + * The name of the class that should be called to test if the test should be retried. + * + * @return String The name of the class that will test if a test method should be retried. + */ + Class retryAnalyzer() default DisabledRetryAnalyzer.class; + ``` + +- ```java + /** + * If true and invocationCount is specified with a value > 1, then all invocations after a + * failure will be marked as a SKIP instead of a FAIL. + * + * @return the value (default false) + */ + boolean skipFailedInvocations() default false; + ``` + +- ```java + /** + * If set to true, this test will run even if the methods it depends on are missing or excluded. + * + * @return the value (default false) + */ + boolean ignoreMissingDependencies() default false; + ``` + +- ```java + /** + * The scheduling priority. Lower priorities will be scheduled first. + * + * @return the value (default 0) + */ + int priority() default 0; + ``` + +- ```java + /** + * @return - An array of {@link CustomAttribute} that represents a set of custom attributes for a + * test method. + */ + CustomAttribute[] attributes() default {}; + ``` diff --git a/pom.xml b/pom.xml index 27f2b448..ea8c1dbe 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,16 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 org.philzen.oss rewrite-testng-to-junit5 0.1-SNAPSHOT + + UTF-8 + + org.projectlombok @@ -49,16 +53,34 @@ rewrite-java-dependencies + + org.openrewrite + rewrite-java-8 + runtime + + org.openrewrite rewrite-test test + + org.openrewrite.recipe + rewrite-testing-frameworks + + + org.testcontainers + testcontainers + + + runtime + + org.junit.jupiter junit-jupiter - test + compile @@ -88,6 +110,16 @@ 6.1.8 test + + + + org.testng + testng + + 7.5.1 + compile + + @@ -147,4 +179,39 @@ + + + + coverage + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + prepare-agent + + prepare-agent + + + + report + + report + + + **/MigrateAssertions.* + + XML + + + + + + + + + diff --git a/src/main/java/io/github/mboegers/openrewrite/testngtojupiter/MigrateAssertions.java b/src/main/java/io/github/mboegers/openrewrite/testngtojupiter/MigrateAssertions.java new file mode 100644 index 00000000..3822d188 --- /dev/null +++ b/src/main/java/io/github/mboegers/openrewrite/testngtojupiter/MigrateAssertions.java @@ -0,0 +1,755 @@ +package io.github.mboegers.openrewrite.testngtojupiter; +/* + * Copyright 2015-2024 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +import com.google.errorprone.refaster.annotation.AfterTemplate; +import com.google.errorprone.refaster.annotation.BeforeTemplate; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.function.Executable; +import org.openrewrite.java.template.RecipeDescriptor; +import org.testng.Assert; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Spliterators; +import java.util.stream.StreamSupport; + +@RecipeDescriptor( + name = "Migrate TestNG Asserts to Jupiter", + description = "Migrate all TestNG Assertions to JUnit Jupiter Assertions." +) +public class MigrateAssertions { + + @RecipeDescriptor( + name = "Migrate `Assert#assertEquals(?, ?)` for array parameters", + description = "Replace `org.testng.Assert#assertEquals(?, ?)` with `org.junit.jupiter.api.Assertions#assertArrayEquals(?, ?)`." + ) + public static class MigrateAssertEqualsArray { + + @BeforeTemplate void before(Object[] actual, Object[] expected) { + Assert.assertEquals(actual, expected); + } + + @AfterTemplate void after(Object[] actual, Object[] expected) { + Assertions.assertArrayEquals(expected, actual); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertEquals(?, ?, String)` for Set, Object", + description = "Replace `org.testng.Assert#assertEquals(?, ?, String)` with `org.junit.jupiter.api.Assertions#assertEquals(?, ?, String)`." + ) + public static class MigrateAssertEqualsArrayWithMsg { + + @BeforeTemplate void before(Object[] actual, Object[] expected, String msg) { + Assert.assertEquals(actual, expected, msg); + } + + @AfterTemplate void after(Object[] actual, Object[] expected, String msg) { + Assertions.assertArrayEquals(expected, actual, msg); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertEquals(double, double, double)`", + description = "Replace `org.testng.Assert#assertEquals(double, double, double)` with `org.junit.jupiter.api.Assertions#assertEquals(double, double, double)`." + ) + public static class MigrateAssertEqualsDoubleDelta { + + @BeforeTemplate void before(double actual, double expected, double delta) { + Assert.assertEquals(actual, expected, delta); + } + + @AfterTemplate + void after(double actual, double expected, double delta) { + Assertions.assertEquals(expected, actual, delta); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertEquals(double, double, double, String)`", + description = "Replace `org.testng.Assert#assertEquals(double, double, double, String)` with `org.junit.jupiter.api.Assertions#assertEquals(double, double, double, String)`." + ) + public static class MigrateAssertEqualsDoubleDeltaWithMsg { + + @BeforeTemplate void before(double actual, double expected, double delta, String msg) { + Assert.assertEquals(actual, expected, delta, msg); + } + + @AfterTemplate + void after(double actual, double expected, double delta, String msg) { + Assertions.assertEquals(expected, actual, delta, msg); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertEquals(float, float, float)`", + description = "Replace `org.testng.Assert#assertEquals(float, float, float)` with `org.junit.jupiter.api.Assertions#assertEquals(float, float, float)`." + ) + public static class MigrateAssertEqualsFloatDelta { + + @BeforeTemplate void before(float actual, float expected, float delta) { + Assert.assertEquals(actual, expected, delta); + } + + @AfterTemplate + void after(float actual, float expected, float delta) { + Assertions.assertEquals(expected, actual, delta); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertEquals(float, float, float, String)`", + description = "Replace `org.testng.Assert#assertEquals(float, float, float, String)` with `org.junit.jupiter.api.Assertions#assertEquals(float, float, float, String)`." + ) + public static class MigrateAssertEqualsFloatDeltaWithMsg { + + @BeforeTemplate void before(float actual, float expected, float delta, String msg) { + Assert.assertEquals(actual, expected, delta, msg); + } + + @AfterTemplate + void after(float actual, float expected, float delta, String msg) { + Assertions.assertEquals(expected, actual, delta, msg); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertEquals(Iterator, Iterator)`", + description = "Migrates `org.testng.Assert#assertEquals(Iterator, Iterator)` " + + "to `org.junit.jupiter.api.Assertions#assertArrayEquals(Object[], Object[])`." + ) + public static class MigrateAssertEqualsIterator { + + @BeforeTemplate void before(Iterator actual, Iterator expected) { + Assert.assertEquals(actual, expected); + } + + @AfterTemplate void after(Iterator actual, Iterator expected) { + Assertions.assertArrayEquals( + StreamSupport.stream(Spliterators.spliteratorUnknownSize(expected, 0), false).toArray(), + StreamSupport.stream(Spliterators.spliteratorUnknownSize(actual, 0), false).toArray() + ); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertEquals(Iterator, Iterator, String)`", + description = "Migrates `org.testng.Assert#assertEquals(Iterator, Iterator, String)` " + + "to `org.junit.jupiter.api.Assertions#assertArrayEquals(Object[], Object[], String)`." + ) + public static class MigrateAssertEqualsIteratorWithMsg { + + @BeforeTemplate void before(Iterator actual, Iterator expected, String msg) { + Assert.assertEquals(actual, expected, msg); + } + + @AfterTemplate void after(Iterator actual, Iterator expected, String msg) { + Assertions.assertArrayEquals( + StreamSupport.stream(Spliterators.spliteratorUnknownSize(expected, 0), false).toArray(), + StreamSupport.stream(Spliterators.spliteratorUnknownSize(actual, 0), false).toArray(), + msg + ); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertEquals(?, ?)` for primitive values, boxed types and other non-array objects", + description = "Replace `org.testng.Assert#assertEquals(?, ?)` with `org.junit.jupiter.api.Assertions#assertEquals(?, ?)`." + + "Always run *after* `MigrateAssertEqualsArrayRecipe` and `MigrateAssertEqualsIteratorRecipe`." + ) + public static class MigrateAssertEquals { + + @BeforeTemplate void before(Object actual, Object expected) { + Assert.assertEquals(actual, expected); + } + + @AfterTemplate void after(Object actual, Object expected) { + Assertions.assertEquals(expected, actual); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertEquals(?, ?, String)` for primitive values, boxed types and other non-array objects", + description = "Replace `org.testng.Assert#assertEquals(?, ?, String)` with `org.junit.jupiter.api.Assertions#assertEquals(?, ?, String)`." + + "Always run *after* `MigrateAssertEqualsArrayRecipe` and `MigrateAssertEqualsIteratorRecipe`." + ) + public static class MigrateAssertEqualsWithMsg { + + @BeforeTemplate void before(Object actual, Object expected, String msg) { + Assert.assertEquals(actual, expected, msg); + } + + @AfterTemplate + void after(Object actual, Object expected, String msg) { + Assertions.assertEquals(expected, actual, msg); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertEqualsNoOrder(Collection, Collection)`", + description = "Migrate `org.testng.Assert#assertEqualsNoOrder(Collection, Collection)` " + + "to `org.junit.jupiter.api.Assertions#assertArrayEquals(Object[], Object[])`, " + + "sorting the collection before applying the assertion." + ) + public static class MigrateAssertEqualsNoOrderCollection { + + @BeforeTemplate void before(Collection actual, Collection expected) { + Assert.assertEqualsNoOrder(actual, expected); + } + + @AfterTemplate void after(Collection actual, Collection expected) { + Assertions.assertArrayEquals( + expected.stream().sorted().toArray(), + actual.stream().sorted().toArray() + ); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertEqualsNoOrder(Collection, Collection, String)`", + description = "Migrate `org.testng.Assert#assertEqualsNoOrder(Collection, Collection, String)` " + + "to `org.junit.jupiter.api.Assertions#assertArrayEquals(Object[], Object[], String)`, " + + "sorting the collection before applying the assertion." + ) + public static class MigrateAssertEqualsNoOrderCollectionWithMessage { + + @BeforeTemplate void before(Collection actual, Collection expected, String message) { + Assert.assertEqualsNoOrder(actual, expected, message); + } + + @AfterTemplate void after(Collection actual, Collection expected, String message) { + Assertions.assertArrayEquals( + expected.stream().sorted().toArray(), + actual.stream().sorted().toArray(), + message + ); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertEqualsNoOrder(Object[], Object[])`", + description = "Migrate `org.testng.Assert#assertEqualsNoOrder(Object[], Object[])` " + + "to `org.junit.jupiter.api.Assertions#assertArrayEquals(Object[], Object[])`, " + + "sorting the arrays before applying the assertion." + ) + public static class MigrateAssertEqualsNoOrderArray { + + @BeforeTemplate void before(Object[] actual, Object[] expected) { + Assert.assertEqualsNoOrder(actual, expected); + } + + @AfterTemplate void after(Object[] actual, Object[] expected) { + Assertions.assertArrayEquals( + Arrays.stream(expected).sorted().toArray(), + Arrays.stream(actual).sorted().toArray() + ); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertEqualsNoOrder(Object[], Object[], String)`", + description = "Migrate `org.testng.Assert#assertEqualsNoOrder(Object[], Object[], String)` " + + "to `org.junit.jupiter.api.Assertions#assertArrayEquals(Object[], Object[], String)`, " + + "sorting the collection before applying the assertion." + ) + public static class MigrateAssertEqualsNoOrderArrayWithMessage { + + @BeforeTemplate void before(Object[] actual, Object[] expected, String message) { + Assert.assertEqualsNoOrder(actual, expected, message); + } + + @AfterTemplate void after(Object[] actual, Object[] expected, String message) { + Assertions.assertArrayEquals( + Arrays.stream(expected).sorted().toArray(), + Arrays.stream(actual).sorted().toArray(), + message + ); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertEqualsNoOrder(Iterator, Iterator)`", + description = "Migrate `org.testng.Assert#assertEqualsNoOrder(Iterator, Iterator)` " + + "to `org.junit.jupiter.api.Assertions#assertArrayEquals(Object[], Object[])`, " + + "sorting the arrays before applying the assertion." + ) + public static class MigrateAssertEqualsNoOrderIterator { + + @BeforeTemplate void before(Iterator actual, Iterator expected) { + Assert.assertEqualsNoOrder(actual, expected); + } + + @AfterTemplate void after(Iterator actual, Iterator expected) { + Assertions.assertArrayEquals( + StreamSupport.stream(Spliterators.spliteratorUnknownSize(expected, 0), false).sorted().toArray(), + StreamSupport.stream(Spliterators.spliteratorUnknownSize(actual, 0), false).sorted().toArray() + ); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertEqualsNoOrder(Iterator, Iterator, String)`", + description = "Migrate `org.testng.Assert#assertEqualsNoOrder(Iterator, Iterator, String)` " + + "to `org.junit.jupiter.api.Assertions#assertArrayEquals(Object[], Object[], String)`, " + + "sorting the arrays before applying the assertion." + ) + public static class MigrateAssertEqualsNoOrderIteratorWithMessage { + + @BeforeTemplate void before(Iterator actual, Iterator expected, String message) { + Assert.assertEqualsNoOrder(actual, expected, message); + } + + @AfterTemplate void after(Iterator actual, Iterator expected, String message) { + Assertions.assertArrayEquals( + StreamSupport.stream(Spliterators.spliteratorUnknownSize(expected, 0), false).sorted().toArray(), + StreamSupport.stream(Spliterators.spliteratorUnknownSize(actual, 0), false).sorted().toArray(), + message + ); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertNotEquals(Object[], Object[])`", + description = "Replace `org.testng.Assert#assertNotEquals(Object[], Object[])` with `org.junit.jupiter.api.Assertions#assertNotEquals(Arrays.toString(Object[], Arrays.toString(Object[]))`." + ) + public static class MigrateAssertNotEqualsArray { + + @BeforeTemplate void before(Object[] actual, Object[] expected) { + Assert.assertNotEquals(actual, expected); + } + + @AfterTemplate void after(Object[] actual, Object[] expected) { + Assertions.assertNotEquals(Arrays.toString(expected), Arrays.toString(actual)); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertNotEquals(Object[], Object[], String)`", + description = "Replace `org.testng.Assert#assertNotEquals(Object[], Object[])` with `org.junit.jupiter.api.Assertions#assertNotEquals(Arrays.toString(Object[], Arrays.toString(Object[]), String)`." + ) + public static class MigrateAssertNotEqualsArrayWithMsg { + + @BeforeTemplate void before(Object[] actual, Object[] expected, String message) { + Assert.assertNotEquals(actual, expected, message); + } + + @AfterTemplate void after(Object[] actual, Object[] expected, String message) { + Assertions.assertNotEquals(Arrays.toString(expected), Arrays.toString(actual), message); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertNotEquals(double, double, double)`", + description = "Replace `org.testng.Assert#assertNotEquals(double, double, double)` with `org.junit.jupiter.api.Assertions#assertNotEquals(double, double, double)`." + ) + public static class MigrateAssertNotEqualsDoubleDelta { + + @BeforeTemplate void before(double actual, double expected, double delta) { + Assert.assertNotEquals(actual, expected, delta); + } + + @AfterTemplate + void after(double actual, double expected, double delta) { + Assertions.assertNotEquals(expected, actual, delta); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertNotEquals(double, double, double, String)`", + description = "Replace `org.testng.Assert#assertNotEquals(double, double, double, String)` with `org.junit.jupiter.api.Assertions#assertNotEquals(double, double, double, String)`." + ) + public static class MigrateAssertNotEqualsDoubleDeltaWithMsg { + + @BeforeTemplate void before(double actual, double expected, double delta, String msg) { + Assert.assertNotEquals(actual, expected, delta, msg); + } + + @AfterTemplate + void after(double actual, double expected, double delta, String msg) { + Assertions.assertNotEquals(expected, actual, delta, msg); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertNotEquals(float, float, float)`", + description = "Replace `org.testng.Assert#assertNotEquals(float, float, float)` with `org.junit.jupiter.api.Assertions#assertNotEquals(float, float, float)`." + ) + public static class MigrateAssertNotEqualsFloatDelta { + + @BeforeTemplate void before(float actual, float expected, float delta) { + Assert.assertNotEquals(actual, expected, delta); + } + + @AfterTemplate + void after(float actual, float expected, float delta) { + Assertions.assertNotEquals(expected, actual, delta); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertNotEquals(float, float, float, String)`", + description = "Replace `org.testng.Assert#assertNotEquals(float, float, float, String)` with `org.junit.jupiter.api.Assertions#assertNotEquals(float, float, float, String)`." + ) + public static class MigrateAssertNotEqualsFloatDeltaWithMsg { + + @BeforeTemplate void before(float actual, float expected, float delta, String msg) { + Assert.assertNotEquals(actual, expected, delta, msg); + } + + @AfterTemplate + void after(float actual, float expected, float delta, String msg) { + Assertions.assertNotEquals(expected, actual, delta, msg); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertNotEquals(Iterator, Iterator)`", + description = "Migrates `org.testng.Assert#assertNotEquals(Iterator, Iterator)` " + + "to `org.junit.jupiter.api.Assertions#assertNotEquals(String, String)` using `Arrays.toString()`." + ) + public static class MigrateAssertNotEqualsIterator { + + @BeforeTemplate void before(Iterator actual, Iterator expected) { + Assert.assertNotEquals(actual, expected); + } + + @AfterTemplate void after(Iterator actual, Iterator expected) { + Assertions.assertNotEquals( + Arrays.toString(StreamSupport.stream(Spliterators.spliteratorUnknownSize(expected, 0), false).toArray()), + Arrays.toString(StreamSupport.stream(Spliterators.spliteratorUnknownSize(actual, 0), false).toArray()) + ); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertEquals(Iterator, Iterator, String)`", + description = "Migrates `org.testng.Assert#assertEquals(Iterator, Iterator, String)` " + + "to `org.junit.jupiter.api.Assertions#assertArrayEquals(Object[], Object[], String)` using `Arrays.toString()`." + ) + public static class MigrateAssertNotEqualsIteratorWithMsg { + + @BeforeTemplate void before(Iterator actual, Iterator expected, String msg) { + Assert.assertNotEquals(actual, expected, msg); + } + + @AfterTemplate void after(Iterator actual, Iterator expected, String msg) { + Assertions.assertNotEquals( + Arrays.toString(StreamSupport.stream(Spliterators.spliteratorUnknownSize(expected, 0), false).toArray()), + Arrays.toString(StreamSupport.stream(Spliterators.spliteratorUnknownSize(actual, 0), false).toArray()), + msg + ); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertNotEquals(?, ?)`", + description = "Replace `org.testng.Assert#assertNotEquals(?, ?)` with `org.junit.jupiter.api.Assertions#assertNotEquals(?, ?)`." + + "Always run *after* `MigrateAssertNotEqualsArrayRecipe` and `MigrateAssertNotEqualsIteratorRecipe`." + ) + public static class MigrateAssertNotEquals { + + @BeforeTemplate void before(Object actual, Object expected) { + Assert.assertNotEquals(actual, expected); + } + + @AfterTemplate void after(Object actual, Object expected) { + Assertions.assertNotEquals(expected, actual); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertNotEquals(?, ?, String)`", + description = "Replace `org.testng.Assert#assertNotEquals(?, ?, String)` with `org.junit.jupiter.api.Assertions#assertNotEquals(?, ?, String)`." + + "Always run *after* `MigrateAssertNotEqualsArrayWithMsgRecipe` and `MigrateAssertNotEqualsIteratorWithMsgRecipe`." + ) + public static class MigrateAssertNotEqualsWithMsg { + + @BeforeTemplate void before(Object actual, Object expected, String msg) { + Assert.assertNotEquals(actual, expected, msg); + } + + @AfterTemplate void after(Object actual, Object expected, String msg) { + Assertions.assertNotEquals(expected, actual, msg); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertNotSame(Object, Object)`", + description = "Migrates `org.testng.Assert#assertNotSame(Object, Object)` to `org.junit.jupiter.api.Assertions#assertNotSame(Object, Object)`." + ) + public static class MigrateAssertNotSame { + + @BeforeTemplate void before(Object actual, Object expected) { + Assert.assertNotSame(actual, expected); + } + + @AfterTemplate void after(Object actual, Object expected) { + Assertions.assertNotSame(expected, actual); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertNotSame(Object, Object, String)`", + description = "Migrates `org.testng.Assert#assertNotSame(Object, Object, String)` to `org.junit.jupiter.api.Assertions#assertNotSame(Object, Object, String)`." + ) + public static class MigrateAssertNotSameWithMsg { + + @BeforeTemplate void before(Object actual, Object expected, String msg) { + Assert.assertNotSame(actual, expected, msg); + } + + @AfterTemplate void after(Object actual, Object expected, String msg) { + Assertions.assertNotSame(expected, actual, msg); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertFalse(boolean)`", + description = "Replace `org.testng.Assert#assertFalse(boolean)` with `org.junit.jupiter.api.Assertions#assertFalse(boolean)`." + ) + public static class MigrateAssertFalse { + + @BeforeTemplate + void before(boolean expr) { + Assert.assertFalse(expr); + } + + @AfterTemplate + void after(boolean expr) { + Assertions.assertFalse(expr); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertFalse(boolean, String)`", + description = "Replace `org.testng.Assert#assertFalse(boolean, String)` with `org.junit.jupiter.api.Assertions#assertFalse(boolean, String)`." + ) + public static class MigrateAssertFalseWithMsg { + + @BeforeTemplate + void before(boolean expr, String msg) { + Assert.assertFalse(expr, msg); + } + + @AfterTemplate + void after(boolean expr, String msg) { + Assertions.assertFalse(expr, msg); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertTrue(boolean)`", + description = "Replace `org.testng.Assert#assertTrue(boolean)` with `org.junit.jupiter.api.Assertions#assertTrue(boolean)`." + ) + public static class MigrateAssertTrue { + + @BeforeTemplate + void before(boolean expr) { + Assert.assertTrue(expr); + } + + @AfterTemplate + void after(boolean expr) { + Assertions.assertTrue(expr); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertTrue(boolean, String)`", + description = "Replace `org.testng.Assert#assertTrue(boolean, String)` with `org.junit.jupiter.api.Assertions#assertTrue(boolean, String)`." + ) + public static class MigrateAssertTrueWithMsg { + + @BeforeTemplate + void before(boolean expr, String msg) { + Assert.assertTrue(expr, msg); + } + + @AfterTemplate + void after(boolean expr, String msg) { + Assertions.assertTrue(expr, msg); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertNull(Object)`", + description = "Replace `org.testng.Assert#assertNull(Object)` with `org.junit.jupiter.api.Assertions#assertNull(Object)`." + ) + public static class MigrateAssertNull { + + @BeforeTemplate void before(Object expr) { + Assert.assertNull(expr); + } + + @AfterTemplate void after(Object expr) { + Assertions.assertNull(expr); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertNull(Object, String)`", + description = "Replace `org.testng.Assert#assertNull(Object, String)` with `org.junit.jupiter.api.Assertions#assertNull(Object, String)`." + ) + public static class MigrateAssertNullWithMsg { + + @BeforeTemplate void before(Object expr, String msg) { + Assert.assertNull(expr, msg); + } + + @AfterTemplate void after(Object expr, String msg) { + Assertions.assertNull(expr, msg); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertNotNull(Object)`", + description = "Replace `org.testng.Assert#assertNotNull(Object)` with `org.junit.jupiter.api.Assertions#assertNotNull(Object)`." + ) + public static class MigrateAssertNotNull { + + @BeforeTemplate void before(Object expr) { + Assert.assertNotNull(expr); + } + + @AfterTemplate void after(Object expr) { + Assertions.assertNotNull(expr); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#assertNotNull(Object, String)`", + description = "Replace `org.testng.Assert#assertNotNull(Object, String)` with `org.junit.jupiter.api.Assertions#assertNotNull(Object, String)`." + ) + public static class MigrateAssertNotNullWithMsg { + + @BeforeTemplate void before(Object expr, String msg) { + Assert.assertNotNull(expr, msg); + } + + @AfterTemplate void after(Object expr, String msg) { + Assertions.assertNotNull(expr, msg); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertSame(Object, Object)`", + description = "Migrates `org.testng.Assert#assertSame(Object, Object)` to `org.junit.jupiter.api.Assertions#assertSame(Object, Object)`." + ) + public static class MigrateAssertSame { + + @BeforeTemplate void before(Object actual, Object expected) { + Assert.assertSame(actual, expected); + } + + @AfterTemplate void after(Object actual, Object expected) { + Assertions.assertSame(expected, actual); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertSame(Object, Object, String)`", + description = "Migrates `org.testng.Assert#assertSame(Object, Object, String)` to `org.junit.jupiter.api.Assertions#assertSame(Object, Object, String)`." + ) + public static class MigrateAssertSameWithMsg { + + @BeforeTemplate void before(Object actual, Object expected, String msg) { + Assert.assertSame(actual, expected, msg); + } + + @AfterTemplate void after(Object actual, Object expected, String msg) { + Assertions.assertSame(expected, actual, msg); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertThrows(Assert.ThrowingRunnable)", + description = "Migrates `org.testng.Assert#assertThrows(Assert.ThrowingRunnable)` to " + + "`org.junit.jupiter.api.Assertions#Assertions.assertThrows(Throwable.class, org.junit.jupiter.api.function.Executable)`." + ) + public static class MigrateAssertThrows { + + @BeforeTemplate void before(Assert.ThrowingRunnable runnable) { + Assert.assertThrows(runnable); + } + + @AfterTemplate void after(Executable executable) { + Assertions.assertThrows(Throwable.class, executable); + } + } + + @RecipeDescriptor( + name = "Migrate `Assert#assertThrows(Class, Assert.ThrowingRunnable) " + + "and `Assert#expectThrows(Class, Assert.ThrowingRunnable)", + description = "Migrates `org.testng.Assert#assertThrows(Class, Assert.ThrowingRunnable)` " + + "and `Assert#expectThrows(Class, Assert.ThrowingRunnable) " + + "to `org.junit.jupiter.api.Assertions#Assertions.assertThrows(Class, org.junit.jupiter.api.function.Executable)`." + ) + public static class MigrateAssertThrowsWithExpectedThrowableType { + + @BeforeTemplate void assertThrows(Class throwableClass, Assert.ThrowingRunnable runnable) { + Assert.assertThrows(throwableClass, runnable); + } + + @BeforeTemplate void expectThrows(Class throwableClass, Assert.ThrowingRunnable runnable) { + Assert.expectThrows(throwableClass, runnable); + } + + @AfterTemplate void after(Class throwableClass, Executable executable) { + Assertions.assertThrows(throwableClass, executable); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#fail()`", + description = "Replace `org.testng.Assert#fail()` with `org.junit.jupiter.api.Assertions#fail()`." + ) + public static class MigrateFailNoArgs { + + @BeforeTemplate void before() { + Assert.fail(); + } + + @AfterTemplate void after() { + Assertions.fail(); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#fail(String)`", + description = "Replace `org.testng.Assert#fail(String)` with `org.junit.jupiter.api.Assertions#fail(String)`." + ) + public static class MigrateFailWithMessage { + + @BeforeTemplate void before(String message) { + Assert.fail(message); + } + + @AfterTemplate void after(String message) { + Assertions.fail(message); + } + } + + @RecipeDescriptor( + name = "Replace `Assert#fail(String)`", + description = "Replace `org.testng.Assert#fail(String)` with `org.junit.jupiter.api.Assertions#fail(String)`." + ) + public static class MigrateFailWithMessageAndCause { + + @BeforeTemplate void before(String message, Throwable cause) { + Assert.fail(message, cause); + } + + @AfterTemplate void after(String message, Throwable cause) { + Assertions.fail(message, cause); + } + } +} diff --git a/src/main/java/org/philzen/oss/AppendToReleaseNotes.java b/src/main/java/org/philzen/oss/AppendToReleaseNotes.java deleted file mode 100644 index 623d4e1a..00000000 --- a/src/main/java/org/philzen/oss/AppendToReleaseNotes.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -import lombok.EqualsAndHashCode; -import lombok.Value; -import org.openrewrite.*; -import org.openrewrite.internal.lang.Nullable; -import org.openrewrite.text.PlainText; -import org.openrewrite.text.PlainTextParser; -import org.openrewrite.text.PlainTextVisitor; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collection; -import java.util.Collections; -import java.util.stream.Collectors; - -@Value -@EqualsAndHashCode(callSuper = false) -public class AppendToReleaseNotes extends ScanningRecipe { - - @Override - public String getDisplayName() { - return "Append to release notes"; - } - - @Override - public String getDescription() { - return "Adds the specified line to RELEASE.md."; - } - - @Option(displayName = "Message", - description = "Message to append to the bottom of RELEASE.md.", - example = "## 1.0.0\n\n- New feature") - String message; - - // The shared state between the scanner and the visitor. The custom class ensures we can easily extend the recipe. - public static class Accumulator { - boolean found; - } - - @Override - public Accumulator getInitialValue(ExecutionContext ctx) { - return new Accumulator(); - } - - @Override - public TreeVisitor getScanner(Accumulator acc) { - return new TreeVisitor() { - @Override - public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { - if (tree instanceof SourceFile) { - Path sourcePath = ((SourceFile) tree).getSourcePath(); - acc.found |= "RELEASE.md".equals(sourcePath.toString()); - } - return tree; - } - }; - } - - @Override - public Collection generate(Accumulator acc, ExecutionContext ctx) { - if (acc.found) { - return Collections.emptyList(); - } - // If the file was not found, create it - return PlainTextParser.builder().build() - // We start with an empty string that we then append to in the visitor - .parse("") - // Be sure to set the source path for any generated file, so that the visitor can find it - .map(it -> (SourceFile) it.withSourcePath(Paths.get("RELEASE.md"))) - .collect(Collectors.toList()); - } - - @Override - public TreeVisitor getVisitor(Accumulator acc) { - return new PlainTextVisitor() { - @Override - public PlainText visitText(PlainText text, ExecutionContext ctx) { - PlainText t = super.visitText(text, ctx); - // If the file is not RELEASE.md, don't modify it - if (!"RELEASE.md".equals(t.getSourcePath().toString())) { - return t; - } - // If the file already contains the message, don't append it again - if (t.getText().contains(message)) { - return t; - } - // Append the message to the end of the file - return t.withText(t.getText() + "\n" + message); - } - }; - } -} diff --git a/src/main/java/org/philzen/oss/AssertEqualsToAssertThat.java b/src/main/java/org/philzen/oss/AssertEqualsToAssertThat.java deleted file mode 100644 index bae3df57..00000000 --- a/src/main/java/org/philzen/oss/AssertEqualsToAssertThat.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -import lombok.EqualsAndHashCode; -import lombok.Value; -import org.openrewrite.ExecutionContext; -import org.openrewrite.Preconditions; -import org.openrewrite.Recipe; -import org.openrewrite.TreeVisitor; -import org.openrewrite.java.JavaIsoVisitor; -import org.openrewrite.java.JavaParser; -import org.openrewrite.java.JavaTemplate; -import org.openrewrite.java.MethodMatcher; -import org.openrewrite.java.search.UsesType; -import org.openrewrite.java.tree.Expression; -import org.openrewrite.java.tree.J; - -import java.util.List; - -@Value -@EqualsAndHashCode(callSuper = false) -public class AssertEqualsToAssertThat extends Recipe { - @Override - public String getDisplayName() { - // language=markdown - return "JUnit `assertEquals()` to Assertj `assertThat()`"; - } - - @Override - public String getDescription() { - return "Use AssertJ assertThat instead of JUnit assertEquals()."; - } - - private static MethodMatcher MATCHER = new MethodMatcher("org.junit.jupiter.api.Assertions assertEquals(..)"); - - @Override - public TreeVisitor getVisitor() { - return Preconditions.check(new UsesType<>("org.junit.jupiter.api.Assertions", null), - new JavaIsoVisitor() { - @Override - public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { - J.MethodInvocation m = super.visitMethodInvocation(method, ctx); - if (!MATCHER.matches(m)) { - return m; - } - List arguments = m.getArguments(); - maybeAddImport("org.assertj.core.api.Assertions"); - maybeRemoveImport("org.junit.jupiter.api.Assertions"); - if (arguments.size() == 2) { - Expression expected = arguments.get(0); - Expression actual = arguments.get(1); - - m = JavaTemplate.builder("Assertions.assertThat(#{any()}).isEqualTo(#{any()})") - .imports("org.assertj.core.api.Assertions") - .javaParser(JavaParser.fromJavaVersion() - .classpath("assertj-core")) - .build() - .apply(getCursor(), m.getCoordinates().replace(), actual, expected); - } else if (arguments.size() == 3) { - Expression expected = arguments.get(0); - Expression actual = arguments.get(1); - Expression description = arguments.get(2); - - m = JavaTemplate.builder("Assertions.assertThat(#{any()}).as(#{any()}).isEqualTo(#{any()})") - .imports("org.assertj.core.api.Assertions") - .javaParser(JavaParser.fromJavaVersion() - .classpath("assertj-core")) - .build() - .apply(getCursor(), m.getCoordinates().replace(), actual, description, expected); - } - return m; - } - }); - } -} diff --git a/src/main/java/org/philzen/oss/ClassHierarchy.java b/src/main/java/org/philzen/oss/ClassHierarchy.java deleted file mode 100644 index bcb540e3..00000000 --- a/src/main/java/org/philzen/oss/ClassHierarchy.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -import org.philzen.oss.table.ClassHierarchyReport; -import lombok.EqualsAndHashCode; -import lombok.Value; -import org.openrewrite.ExecutionContext; -import org.openrewrite.Recipe; -import org.openrewrite.TreeVisitor; -import org.openrewrite.java.JavaIsoVisitor; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.JavaType; - -@Value -@EqualsAndHashCode(callSuper = false) -public class ClassHierarchy extends Recipe { - - transient ClassHierarchyReport report = new ClassHierarchyReport(this); - - @Override - public String getDisplayName() { - return "Class hierarchy"; - } - - @Override - public String getDescription() { - return "Produces a data table showing inheritance relationships between classes."; - } - - @Override - public TreeVisitor getVisitor() { - return new JavaIsoVisitor() { - - @Override - public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { - JavaType.FullyQualified type = classDecl.getType(); - // Capture all classes, which all extend java.lang.Object - if (type instanceof JavaType.Class && type.getSupertype() != null) { - JavaType.FullyQualified supertype = type.getSupertype(); - // Capture the direct superclass - report.insertRow(ctx, new ClassHierarchyReport.Row( - type.getFullyQualifiedName(), - ClassHierarchyReport.Relationship.EXTENDS, - supertype.getFullyQualifiedName())); - - // Capture all interfaces - for (JavaType.FullyQualified anInterface : type.getInterfaces()) { - report.insertRow(ctx, new ClassHierarchyReport.Row( - type.getFullyQualifiedName(), - ClassHierarchyReport.Relationship.IMPLEMENTS, - anInterface.getFullyQualifiedName() - )); - } - } - return super.visitClassDeclaration(classDecl, ctx); - } - }; - } -} diff --git a/src/main/java/org/philzen/oss/NoGuavaListsNewArrayList.java b/src/main/java/org/philzen/oss/NoGuavaListsNewArrayList.java deleted file mode 100644 index d243c6e9..00000000 --- a/src/main/java/org/philzen/oss/NoGuavaListsNewArrayList.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2021 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 org.philzen.oss; - -import lombok.EqualsAndHashCode; -import lombok.Value; -import org.openrewrite.ExecutionContext; -import org.openrewrite.Preconditions; -import org.openrewrite.Recipe; -import org.openrewrite.TreeVisitor; -import org.openrewrite.java.JavaTemplate; -import org.openrewrite.java.JavaVisitor; -import org.openrewrite.java.MethodMatcher; -import org.openrewrite.java.TreeVisitingPrinter; -import org.openrewrite.java.search.UsesMethod; -import org.openrewrite.java.tree.J; - -@Value -@EqualsAndHashCode(callSuper = false) -public class NoGuavaListsNewArrayList extends Recipe { - // These matchers use a syntax described on https://docs.openrewrite.org/reference/method-patterns - private static final MethodMatcher NEW_ARRAY_LIST = new MethodMatcher("com.google.common.collect.Lists newArrayList()"); - private static final MethodMatcher NEW_ARRAY_LIST_ITERABLE = new MethodMatcher("com.google.common.collect.Lists newArrayList(java.lang.Iterable)"); - private static final MethodMatcher NEW_ARRAY_LIST_CAPACITY = new MethodMatcher("com.google.common.collect.Lists newArrayListWithCapacity(int)"); - - @Override - public String getDisplayName() { - //language=markdown - return "Use `new ArrayList<>()` instead of Guava"; - } - - @Override - public String getDescription() { - //language=markdown - return "Prefer the Java standard library over third-party usage of Guava in simple cases like this."; - } - - @Override - public TreeVisitor getVisitor() { - return Preconditions.check( - // Any change to the AST made by the preconditions check will lead to the visitor returned by Recipe - // .getVisitor() being applied - // No changes made by the preconditions check will be kept - Preconditions.or(new UsesMethod<>(NEW_ARRAY_LIST), - new UsesMethod<>(NEW_ARRAY_LIST_ITERABLE), - new UsesMethod<>(NEW_ARRAY_LIST_CAPACITY)), - // To avoid stale state persisting between cycles, getVisitor() should always return a new instance of - // its visitor - new JavaVisitor() { - private final JavaTemplate newArrayList = JavaTemplate.builder("new ArrayList<>()") - .imports("java.util.ArrayList") - .build(); - - private final JavaTemplate newArrayListIterable = - JavaTemplate.builder("new ArrayList<>(#{any(java.util.Collection)})") - .imports("java.util.ArrayList") - .build(); - - private final JavaTemplate newArrayListCapacity = - JavaTemplate.builder("new ArrayList<>(#{any(int)})") - .imports("java.util.ArrayList") - .build(); - - // This method override is only here to show how to print the AST for debugging purposes. - // You can remove this method if you don't need it. - @Override - public J visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) { - // This is a useful debugging tool if you're ever unsure what the visitor is visiting - String printed = TreeVisitingPrinter.printTree(cu); - System.out.printf(printed); - // You must always delegate to the super method to ensure the visitor continues to visit deeper - return super.visitCompilationUnit(cu, ctx); - } - - // Visit any method invocation, and replace matches with the new ArrayList instantiation. - @Override - public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { - if (NEW_ARRAY_LIST.matches(method)) { - maybeRemoveImport("com.google.common.collect.Lists"); - maybeAddImport("java.util.ArrayList"); - return newArrayList.apply(getCursor(), method.getCoordinates().replace()); - } else if (NEW_ARRAY_LIST_ITERABLE.matches(method)) { - maybeRemoveImport("com.google.common.collect.Lists"); - maybeAddImport("java.util.ArrayList"); - return newArrayListIterable.apply(getCursor(), method.getCoordinates().replace(), - method.getArguments().get(0)); - } else if (NEW_ARRAY_LIST_CAPACITY.matches(method)) { - maybeRemoveImport("com.google.common.collect.Lists"); - maybeAddImport("java.util.ArrayList"); - return newArrayListCapacity.apply(getCursor(), method.getCoordinates().replace(), - method.getArguments().get(0)); - } - return super.visitMethodInvocation(method, ctx); - } - } - ); - } -} diff --git a/src/main/java/org/philzen/oss/SimplifyTernary.java b/src/main/java/org/philzen/oss/SimplifyTernary.java deleted file mode 100644 index d609b305..00000000 --- a/src/main/java/org/philzen/oss/SimplifyTernary.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -import com.google.errorprone.refaster.annotation.AfterTemplate; -import com.google.errorprone.refaster.annotation.BeforeTemplate; -import org.openrewrite.java.template.RecipeDescriptor; - -@SuppressWarnings({"SimplifiableConditionalExpression", "unused"}) -@RecipeDescriptor( - name = "Simplify ternary expressions", - description = "Simplifies various types of ternary expressions to improve code readability." -) -public class SimplifyTernary { - - @RecipeDescriptor( - name = "Replace `booleanExpression ? true : false` with `booleanExpression`", - description = "Replace ternary expressions like `booleanExpression ? true : false` with `booleanExpression`." - ) - public static class SimplifyTernaryTrueFalse { - - @BeforeTemplate - boolean before(boolean expr) { - return expr ? true : false; - } - - @AfterTemplate - boolean after(boolean expr) { - return expr; - } - } - - @RecipeDescriptor( - name = "Replace `booleanExpression ? false : true` with `!booleanExpression`", - description = "Replace ternary expressions like `booleanExpression ? false : true` with `!booleanExpression`." - ) - public static class SimplifyTernaryFalseTrue { - - @BeforeTemplate - boolean before(boolean expr) { - return expr ? false : true; - } - - @AfterTemplate - boolean after(boolean expr) { - // We wrap the expression in parentheses as the input expression might be a complex expression - return !(expr); - } - } -} diff --git a/src/main/java/org/philzen/oss/StringIsEmpty.java b/src/main/java/org/philzen/oss/StringIsEmpty.java deleted file mode 100644 index a5659acd..00000000 --- a/src/main/java/org/philzen/oss/StringIsEmpty.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -// TODO - This is a placeholder for a Refaster recipe. Implement the recipe by adding before and after annotated methods. -// The rule should replace calls to `String.length() == 0` with `String.isEmpty()`, as well as similar variants. -// You're done when all the tests in `StringIsEmptyTest` passes. -public class StringIsEmpty { -} diff --git a/src/main/java/org/philzen/oss/UpdateConcoursePipeline.java b/src/main/java/org/philzen/oss/UpdateConcoursePipeline.java deleted file mode 100644 index 5290f84f..00000000 --- a/src/main/java/org/philzen/oss/UpdateConcoursePipeline.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -import lombok.EqualsAndHashCode; -import lombok.Value; -import org.openrewrite.*; -import org.openrewrite.yaml.ChangePropertyValue; -import org.openrewrite.yaml.YamlIsoVisitor; -import org.openrewrite.yaml.tree.Yaml; - -@Value -@EqualsAndHashCode(callSuper = false) -public class UpdateConcoursePipeline extends Recipe { - @Override - public String getDisplayName() { - return "Update concourse pipeline"; - } - - @Override - public String getDescription() { - return "Update the tag filter on concourse pipelines."; - } - - @Option(displayName = "New tag filter version", - description = "tag filter version.", - example = "8.2.0") - String version; - - @Override - public TreeVisitor getVisitor() { - return Preconditions.check( - Preconditions.or( - new FindSourceFiles("ci/pipeline*.yml").getVisitor(), - new FindSourceFiles("ci/pipeline*.yaml").getVisitor()), - new YamlIsoVisitor() { - - @Override - public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { - Yaml.Mapping.Entry e = super.visitMappingEntry(entry, ctx); - if ("source".equals(e.getKey().getValue())) { - Yaml.Block value = e.getValue(); - if (!(value instanceof Yaml.Mapping)) { - return e; - } - Yaml.Mapping mapping = (Yaml.Mapping) value; - Yaml.Mapping.Entry uriEntry = null; - Yaml.Mapping.Entry tagFilter = null; - for (Yaml.Mapping.Entry mappingEntry : mapping.getEntries()) { - if ("uri".equals(mappingEntry.getKey().getValue())) { - uriEntry = mappingEntry; - } else if ("tag_filter".equals(mappingEntry.getKey().getValue())) { - tagFilter = mappingEntry; - } - } - if (uriEntry == null || tagFilter == null) { - return e; - } - if (!(uriEntry.getValue() instanceof Yaml.Scalar) || !(tagFilter.getValue() instanceof Yaml.Scalar)) { - return e; - } - Yaml.Scalar uriValue = (Yaml.Scalar) uriEntry.getValue(); - if (!uriValue.getValue().contains(".git")) { - return e; - } - Yaml.Scalar tagFilterValue = (Yaml.Scalar) tagFilter.getValue(); - if (version.equals(tagFilterValue.getValue())) { - return e; - } - return (Yaml.Mapping.Entry) new ChangePropertyValue("source.tag_filter", version, null, null, null) - .getVisitor() - .visitNonNull(e, ctx); - } - return e; - } - } - ); - } -} diff --git a/src/main/java/org/philzen/oss/table/ClassHierarchyReport.java b/src/main/java/org/philzen/oss/table/ClassHierarchyReport.java deleted file mode 100644 index 55bef165..00000000 --- a/src/main/java/org/philzen/oss/table/ClassHierarchyReport.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss.table; - -import lombok.Value; -import org.openrewrite.Column; -import org.openrewrite.DataTable; -import org.openrewrite.Recipe; - -public class ClassHierarchyReport extends DataTable { - - public ClassHierarchyReport(Recipe recipe) { - super(recipe, - "Class hierarchy report", - "Records inheritance relationships between classes."); - } - - @Value - public static class Row { - @Column(displayName = "Class name", - description = "Fully qualified name of the class.") - String className; - - @Column(displayName = "Relationship", - description = "Whether the class implements a super interface or extends a superclass.") - Relationship relationship; - - @Column(displayName = "Super class name", - description = "Fully qualified name of the superclass.") - String superClassName; - } - - public enum Relationship { - EXTENDS, - IMPLEMENTS - } -} diff --git a/src/main/java/org/philzen/oss/testng/MigrateMismatchedAssertions.java b/src/main/java/org/philzen/oss/testng/MigrateMismatchedAssertions.java new file mode 100644 index 00000000..40f42731 --- /dev/null +++ b/src/main/java/org/philzen/oss/testng/MigrateMismatchedAssertions.java @@ -0,0 +1,97 @@ +package org.philzen.oss.testng; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.lang.NonNullApi; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.search.UsesMethod; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.J; +import org.philzen.oss.utils.Parser; + +import java.util.function.Function; + +@NonNullApi +public class MigrateMismatchedAssertions extends Recipe { + + @Override + public String getDisplayName() { + return "Replace `Assert#assertEquals(actual[], expected[], delta [, message])` for float and double inputs"; + } + + @Override + public String getDescription() { + return "Replaces `org.testng.Assert#assertEquals(actual[], expected[], delta [, message])` with custom `org.junit.jupiter.api.Assertions#assertAll(() -> {})`."; + } + + @Override + public TreeVisitor getVisitor() { + JavaVisitor javaVisitor = new JavaVisitor() { + + final Function before = (type) -> JavaTemplate + .builder("org.testng.Assert.assertEquals(#{actual:anyArray(%s)}, #{expected:anyArray(%s)}, #{delta:any(%s)});".replace("%s", type)) + .javaParser(Parser.runtime()).build(); + + final Function beforeWithMsg = (type) -> JavaTemplate + .builder("org.testng.Assert.assertEquals(#{actual:anyArray(%s)}, #{expected:anyArray(%s)}, #{delta:any(%s)}, #{message:any(java.lang.String)});".replace("%s", type)) + .javaParser(Parser.runtime()).build(); + + final JavaTemplate after = JavaTemplate + .builder("Assertions.assertAll(()->{\n Assertions.assertEquals(#{expected:anyArray(float)}.length, #{actual:anyArray(float)}.length, \"Arrays don't have the same size.\");\n for (int i = 0; i < #{actual}.length; i++) {\n Assertions.assertEquals(#{expected}[i], #{actual}[i], #{delta:any(float)});\n }\n});") + .imports("org.junit.jupiter.api.Assertions") + .javaParser(Parser.jupiter()).build(); + + final JavaTemplate afterWithMsg = JavaTemplate + .builder("Assertions.assertAll(()->{\n Assertions.assertEquals(#{expected:anyArray(float)}.length, #{actual:anyArray(float)}.length, \"Arrays don't have the same size.\");\n for (int i = 0; i < #{actual}.length; i++) {\n Assertions.assertEquals(#{expected}[i], #{actual}[i], #{delta:any(float)}, #{message:any(String)});\n }\n});") + .imports("org.junit.jupiter.api.Assertions") + .javaParser(Parser.jupiter()).build(); + + @Override + public J visitMethodInvocation(J.MethodInvocation elem, ExecutionContext ctx) { + JavaTemplate.Matcher matcher; + if ((matcher = before.apply("float").matcher(getCursor())).find() + || (matcher = before.apply("double").matcher(getCursor())).find()) + { + imports(); + return after.apply( + getCursor(), + elem.getCoordinates().replace(), + matcher.parameter(1), + matcher.parameter(0), + matcher.parameter(2) + ); + } else if ((matcher = beforeWithMsg.apply("float").matcher(getCursor())).find() + || (matcher = beforeWithMsg.apply("double").matcher(getCursor())).find()) + { + imports(); + return afterWithMsg.apply( + getCursor(), + elem.getCoordinates().replace(), + matcher.parameter(1), + matcher.parameter(0), + matcher.parameter(2), + matcher.parameter(3) + ); + } + + return super.visitMethodInvocation(elem, ctx); + } + + private void imports() { + maybeRemoveImport("org.testng.Assert"); + maybeAddImport("org.junit.jupiter.api.Assertions"); + } + }; + + return Preconditions.check( + Preconditions.and( + new UsesType<>("org.testng.Assert", true), + new UsesMethod<>("org.testng.Assert assertEquals(..)") + ), + javaVisitor + ); + } +} diff --git a/src/main/java/org/philzen/oss/testng/UpdateTestAnnotationToJunit5.java b/src/main/java/org/philzen/oss/testng/UpdateTestAnnotationToJunit5.java new file mode 100644 index 00000000..588f5d5d --- /dev/null +++ b/src/main/java/org/philzen/oss/testng/UpdateTestAnnotationToJunit5.java @@ -0,0 +1,237 @@ +package org.philzen.oss.testng; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.internal.lang.NonNullApi; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.AnnotationMatcher; +import org.openrewrite.java.ChangeType; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.search.FindImports; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.TypeUtils; +import org.philzen.oss.utils.Class; +import org.philzen.oss.utils.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +@Value +@NonNullApi +@EqualsAndHashCode(callSuper = true) +public class UpdateTestAnnotationToJunit5 extends Recipe { + + @Override + public String getDisplayName() { + return "Migrate TestNG `@Test` annotations to JUnit 5"; + } + + @Override + public String getDescription() { + return String.format( + "Update usages of TestNG's `@%s` annotation to JUnit 5's `@%s` annotation.", TESTNG_TYPE, JUPITER_TYPE + ); + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(Preconditions.or( + new UsesType<>(TESTNG_TYPE, false), + new FindImports(TESTNG_TYPE, null).getVisitor() + ), new UpdateTestAnnotationToJunit5Visitor()); + } + + public static final String TESTNG_TYPE = "org.testng.annotations.Test"; + public static final String JUPITER_API_NAMESPACE = "org.junit.jupiter.api"; + public static final String JUPITER_TYPE = JUPITER_API_NAMESPACE + ".Test"; + public static final String JUPITER_ASSERTIONS_TYPE = JUPITER_API_NAMESPACE + ".Assertions"; + + // inspired by https://github.com/openrewrite/rewrite-testing-frameworks/blob/4e8ba68b2a28a180f84de7bab9eb12b4643e342e/src/main/java/org/openrewrite/java/testing/junit5/UpdateTestAnnotation.java# + private static class UpdateTestAnnotationToJunit5Visitor extends JavaIsoVisitor { + + private static final AnnotationMatcher TESTNG_TEST = new AnnotationMatcher("@org.testng.annotations.Test"); + + private final JavaTemplate displayNameAnnotation = JavaTemplate + .builder("@DisplayName(#{any(java.lang.String)})") + .imports(JUPITER_API_NAMESPACE + ".DisplayName") + .javaParser(Parser.jupiter()).build(); + + private final JavaTemplate disabledAnnotation = JavaTemplate + .builder("@Disabled") + .imports(JUPITER_API_NAMESPACE + ".Disabled") + .javaParser(Parser.jupiter()).build(); + + private final JavaTemplate junitExecutable = JavaTemplate + .builder(JUPITER_API_NAMESPACE + ".function.Executable o = () -> #{};") + .javaParser(Parser.jupiter()).build(); + + private final JavaTemplate tagAnnotation = JavaTemplate + .builder("@Tag(#{any(java.lang.String)})") + .imports(JUPITER_API_NAMESPACE + ".Tag") + .javaParser(Parser.jupiter()).build(); + + private final JavaTemplate timeoutAnnotation = JavaTemplate + .builder("@Timeout(value = #{any(long)}, unit = TimeUnit.MILLISECONDS)") + .imports(JUPITER_API_NAMESPACE + ".Timeout", "java.util.concurrent.TimeUnit") + .javaParser(Parser.jupiter()).build(); + + @Override + public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) { + J.CompilationUnit c = super.visitCompilationUnit(cu, ctx); + if (!c.findType(TESTNG_TYPE).isEmpty()) { + // Update other references like `Test.class`. + c = (J.CompilationUnit) new ChangeType(TESTNG_TYPE, JUPITER_TYPE, true) + .getVisitor().visitNonNull(c, ctx); + maybeRemoveImport(TESTNG_TYPE); + } + + return c; + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, + ExecutionContext executionContext) { + + final J.Annotation testAnnotation = Class.getAnnotation(classDecl, TESTNG_TEST); + if (testAnnotation != null) { + classDecl = Cleanup.removeAnnotation(classDecl, testAnnotation); + + getCursor().putMessage( + // don't know a good way to determine if annotation is fully qualified, therefore determining + // it from the toString() method and passing on a code template for the JavaTemplate.Builder + "ADD_TO_ALL_METHODS", "@" + (testAnnotation.toString().contains(".") ? JUPITER_TYPE : "Test") + ); + } + + return super.visitClassDeclaration(classDecl, executionContext); + } + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + final ProcessAnnotationAttributes cta = new ProcessAnnotationAttributes(); + J.MethodDeclaration m = (J.MethodDeclaration) cta.visitNonNull(method, ctx, getCursor().getParentOrThrow()); + + // method identity changes when `@Test` annotation was found and migrated by ChangeTestAnnotation + if (m == method) { + final String neededOnAllMethods = getCursor().getNearestMessage("ADD_TO_ALL_METHODS"); + final boolean isContainedInInnerClass = Boolean.TRUE.equals(Method.isContainedInInnerClass(m)); + if (neededOnAllMethods == null || !Method.isPublic(m) || isContainedInInnerClass) { + return m; + } + + return JavaTemplate.builder(neededOnAllMethods).javaParser(Parser.jupiter()) + .imports(JUPITER_TYPE).build() + .apply(getCursor(), m.getCoordinates().addAnnotation(Sort.BELOW)); + } + + if (cta.description != null && !J.Literal.isLiteralValue(cta.description, "")) { + maybeAddImport(JUPITER_API_NAMESPACE + ".DisplayName"); + m = displayNameAnnotation.apply( + updateCursor(m), m.getCoordinates().addAnnotation(Sort.BELOW), cta.description + ); + } + + if (J.Literal.isLiteralValue(cta.enabled, Boolean.FALSE)) { + maybeAddImport(JUPITER_API_NAMESPACE + ".Disabled"); + m = disabledAnnotation.apply(updateCursor(m), m.getCoordinates().addAnnotation(Sort.BELOW)); + } + + if (cta.expectedException instanceof J.FieldAccess + // TestNG actually allows any type of Class here, however anything but a Throwable doesn't make sense + && TypeUtils.isAssignableTo("java.lang.Throwable", ((J.FieldAccess) cta.expectedException).getTarget().getType())) + { + m = junitExecutable.apply(updateCursor(m), m.getCoordinates().replaceBody(), m.getBody()); + + maybeAddImport(JUPITER_ASSERTIONS_TYPE); + final List parameters = Arrays.asList(cta.expectedException, Method.getFirstStatementLambdaAssignment(m)); + final String code = "Assertions.assertThrows(#{any(java.lang.Class)}, #{any(org.junit.jupiter.api.function.Executable)});"; + if (!(cta.expectedExceptionMessageRegExp instanceof J.Literal)) { + m = JavaTemplate.builder(code).javaParser(Parser.jupiter()) + .imports(JUPITER_ASSERTIONS_TYPE).build() + .apply(updateCursor(m), m.getCoordinates().replaceBody(), parameters.toArray()); + } else { + m = JavaTemplate.builder( + "final Throwable thrown = " + code + System.lineSeparator() + + "Assertions.assertTrue(thrown.getMessage().matches(#{any(java.lang.String)}));" + ).javaParser(Parser.jupiter()).imports(JUPITER_ASSERTIONS_TYPE).build() + .apply( + updateCursor(m), + m.getCoordinates().replaceBody(), + ListUtils.concat(parameters, cta.expectedExceptionMessageRegExp).toArray() + ); + } + } + + if (cta.groups != null) { + maybeAddImport(JUPITER_API_NAMESPACE + ".Tag"); + if (cta.groups instanceof J.Literal && !J.Literal.isLiteralValue(cta.groups, "")) { + m = tagAnnotation.apply(updateCursor(m), m.getCoordinates().addAnnotation(Sort.BELOW), cta.groups); + } else if (cta.groups instanceof J.NewArray && ((J.NewArray) cta.groups).getInitializer() != null) { + final List groups = ((J.NewArray) cta.groups).getInitializer(); + for (Expression group : groups) { + if (group instanceof J.Empty) continue; + m = tagAnnotation.apply(updateCursor(m), m.getCoordinates().addAnnotation(Sort.BELOW), group); + } + } + } + + if (cta.timeout != null) { + maybeAddImport("java.util.concurrent.TimeUnit"); + maybeAddImport(JUPITER_API_NAMESPACE + ".Timeout"); + m = timeoutAnnotation.apply(updateCursor(m), m.getCoordinates().addAnnotation(Sort.ABOVE), cta.timeout); + } + + return m; + } + + /** + * Parses all annotation arguments, retains all that are migratable + * and removes them from the visited @Test-annotation + */ + private static class ProcessAnnotationAttributes extends JavaIsoVisitor { + + @Nullable + Expression description, enabled, expectedException, expectedExceptionMessageRegExp, groups, timeout; + + @Override + public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext ctx) { + if (a.getArguments() == null || !TESTNG_TEST.matches(a)) { + return a; + } + + for (Expression arg : a.getArguments()) { + final J.Assignment assign = (J.Assignment) arg; + final String assignParamName = ((J.Identifier) assign.getVariable()).getSimpleName(); + final Expression e = assign.getAssignment(); + if ("description".equals(assignParamName)) { + description = e; + } else if ("enabled".equals(assignParamName)) { + enabled = e; + } else if ("expectedExceptions".equals(assignParamName)) { + // if attribute was given in { array form }, pick the first element (null is not allowed) + expectedException = !(e instanceof J.NewArray) + ? e : Objects.requireNonNull(((J.NewArray) e).getInitializer()).get(0); + } else if ("expectedExceptionsMessageRegExp".equals(assignParamName)) { + expectedExceptionMessageRegExp = e; + } else if ("groups".equals(assignParamName)) { + groups = e; + } else if ("timeOut".equals(assignParamName)) { + timeout = e; + } + } + + // remove all attribute arguments (JUnit 5 @Test annotation doesn't allow any) + return a.withArguments(null); + } + } + } +} diff --git a/src/main/java/org/philzen/oss/utils/Class.java b/src/main/java/org/philzen/oss/utils/Class.java new file mode 100644 index 00000000..2601a057 --- /dev/null +++ b/src/main/java/org/philzen/oss/utils/Class.java @@ -0,0 +1,20 @@ +package org.philzen.oss.utils; + +import org.openrewrite.internal.lang.NonNullApi; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.AnnotationMatcher; +import org.openrewrite.java.tree.J; + +import java.util.Optional; + +@NonNullApi +public enum Class {; + + @Nullable + public static J.Annotation getAnnotation(J.ClassDeclaration classDeclaration, AnnotationMatcher annotation) { + final Optional maybeAnnotation = classDeclaration.getLeadingAnnotations() + .stream().filter(annotation::matches).findFirst(); + + return maybeAnnotation.orElse(null); + } +} diff --git a/src/main/java/org/philzen/oss/utils/Cleanup.java b/src/main/java/org/philzen/oss/utils/Cleanup.java new file mode 100644 index 00000000..1d3d19d3 --- /dev/null +++ b/src/main/java/org/philzen/oss/utils/Cleanup.java @@ -0,0 +1,44 @@ +package org.philzen.oss.utils; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JContainer; +import org.openrewrite.java.tree.Space; + +import java.util.ArrayList; +import java.util.List; + +public enum Cleanup {; + + /** + * Removes an annotation and cleans the space that it occupied. + * Same could be achieved with {@link org.openrewrite.java.RemoveAnnotationVisitor}, however + * that would also traverse the whole LST underneath the class, yielding suboptimal performance.

+ * + * Space cleaning algorithm borrowed from {@link org.openrewrite.java.RemoveAnnotationVisitor#visitClassDeclaration(J.ClassDeclaration, ExecutionContext)} + */ + public static J.ClassDeclaration removeAnnotation(J.ClassDeclaration classDeclaration, J.Annotation a) { + + classDeclaration.getLeadingAnnotations().remove(a); + if (!classDeclaration.getLeadingAnnotations().isEmpty()) { + final List newLeadingAnnotations = new ArrayList<>(); + for (final J.Annotation other : classDeclaration.getLeadingAnnotations()) { + newLeadingAnnotations.add(other.withPrefix(other.getPrefix().withWhitespace(""))); + } + return classDeclaration.withLeadingAnnotations(newLeadingAnnotations); + } + + final List modifiers = classDeclaration.getModifiers(); + if (!modifiers.isEmpty()) { + return classDeclaration.withModifiers(Space.formatFirstPrefix(modifiers, Space.firstPrefix(modifiers).withWhitespace(""))); + } + + final JContainer typeParameters = classDeclaration.getPadding().getTypeParameters(); + if (typeParameters != null) { + return classDeclaration.getPadding().withTypeParameters(typeParameters.withBefore(typeParameters.getBefore().withWhitespace(""))); + } + + final J.ClassDeclaration.Padding padding = classDeclaration.getPadding(); + return padding.withKind(padding.getKind().withPrefix(padding.getKind().getPrefix().withWhitespace(""))); + } +} diff --git a/src/main/java/org/philzen/oss/utils/Method.java b/src/main/java/org/philzen/oss/utils/Method.java new file mode 100644 index 00000000..42f0c5d3 --- /dev/null +++ b/src/main/java/org/philzen/oss/utils/Method.java @@ -0,0 +1,50 @@ +package org.philzen.oss.utils; + +import org.openrewrite.internal.lang.NonNullApi; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +@NonNullApi +public enum Method {; + + /** + * Whether this method is declared in a nested class and not on the main class scope + * @return Returns null in the fringe case that {@link J.MethodDeclaration#getMethodType()} is + * null as it's not possible to query the parent scope information then (respectively + * it's not clear what {@link J.MethodDeclaration#getMethodType()} == null means) + */ + @Nullable + public static Boolean isContainedInInnerClass(J.MethodDeclaration method) { + final JavaType.Method methodType = method.getMethodType(); + if (methodType == null) { + return null; + } + + return methodType.getDeclaringType().getOwningClass() != null; + } + + public static boolean isPublic(J.MethodDeclaration method) { + return method.getModifiers().stream().anyMatch(mod -> mod.toString().equals("public")); + } + + /** + * Suppose you have a method that looks like this: + *

+     *     void method() {
+     *         Supplier x = () -> { return "x" };
+     *     }
+     * 

+ * Then this method will return the {@link J.Lambda} that represents () -> { return "x" }. + * @return The lambda or null, if no such expression exists on the first statement of the method body + */ + @Nullable + public static J.Lambda getFirstStatementLambdaAssignment(J.MethodDeclaration method) { + final J.Block body = method.getBody(); + if (body == null) { + return null; + } + + return (J.Lambda) ((J.VariableDeclarations) body.getStatements().get(0)).getVariables().get(0).getInitializer(); + } +} diff --git a/src/main/java/org/philzen/oss/utils/Parser.java b/src/main/java/org/philzen/oss/utils/Parser.java new file mode 100644 index 00000000..5c78336e --- /dev/null +++ b/src/main/java/org/philzen/oss/utils/Parser.java @@ -0,0 +1,28 @@ +package org.philzen.oss.utils; + +import org.openrewrite.java.JavaParser; + +public enum Parser {; + + private static final class JavaParserHolder { + static final JavaParser.Builder jupiter = + JavaParser.fromJavaVersion().classpath("junit-jupiter-api"); + + static final JavaParser.Builder runtimeClasspath = + JavaParser.fromJavaVersion().classpath(JavaParser.runtimeClasspath()); + } + + /** + * Get a {@link JavaParser.Builder} with junit-jupiter-api added to the classpath + */ + public static JavaParser.Builder jupiter() { + return JavaParserHolder.jupiter; + } + + /** + * Get a {@link JavaParser.Builder} for the full runtime classpath + */ + public static JavaParser.Builder runtime() { + return JavaParserHolder.runtimeClasspath; + } +} diff --git a/src/main/java/org/philzen/oss/utils/Sort.java b/src/main/java/org/philzen/oss/utils/Sort.java new file mode 100644 index 00000000..be28ae4d --- /dev/null +++ b/src/main/java/org/philzen/oss/utils/Sort.java @@ -0,0 +1,10 @@ +package org.philzen.oss.utils; + +import org.openrewrite.java.tree.J; + +import java.util.Comparator; + +public enum Sort {; + public static final Comparator ABOVE = java.util.Comparator.comparing(J.Annotation::getSimpleName); + public static final Comparator BELOW = java.util.Comparator.comparing(J.Annotation::getSimpleName).reversed(); +} diff --git a/src/main/resources/META-INF/rewrite/rewrite.yml b/src/main/resources/META-INF/rewrite/rewrite.yml index 9bf81b0c..fed10d41 100644 --- a/src/main/resources/META-INF/rewrite/rewrite.yml +++ b/src/main/resources/META-INF/rewrite/rewrite.yml @@ -1,32 +1,14 @@ -# -# Copyright 2021 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. -# - -# Include any Declarative YAML format recipes here, as per: -# https://docs.openrewrite.org/reference/yaml-format-reference -# These are most easily composed through the Yaml recipe builder at: -# https://app.moderne.io/recipes/builder - -# Notice how we can have multiple recipes in the same file, separated by `---` -# You can also have multiple files in `src/main/resources/META-INF/rewrite`, each containing one or more recipes. ---- type: specs.openrewrite.org/v1beta/recipe -name: org.philzen.oss.UseOpenRewriteNullable -displayName: Prefer OpenRewrite Nullable -description: Replaces JetBrains Nullable with OpenRewrite Nullable. +name: org.philzen.oss.testng.MigrateToJunit5 +tags: [TestNG, JUnit5, Jupiter, JUnit] +displayName: JUnit Jupiter migration from TestNG +description: Migrates TestNG tests to JUnit Jupiter. +estimatedEffortPerOccurrence: PT20S +preconditions: +- org.openrewrite.FindSourceFiles: + filePattern: "**/*.java" recipeList: - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: org.jetbrains.annotations.Nullable - newFullyQualifiedTypeName: org.openrewrite.internal.lang.Nullable +- org.philzen.oss.testng.UpdateTestAnnotationToJunit5 +- org.philzen.oss.testng.MigrateMismatchedAssertions +- org.openrewrite.java.testing.junit5.AddMissingNested +- io.github.mboegers.openrewrite.testngtojupiter.MigrateAssertionsRecipes diff --git a/src/main/resources/META-INF/rewrite/stringutils.yml b/src/main/resources/META-INF/rewrite/stringutils.yml deleted file mode 100644 index 4b98316c..00000000 --- a/src/main/resources/META-INF/rewrite/stringutils.yml +++ /dev/null @@ -1,31 +0,0 @@ -# -# Copyright 2024 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. -# - ---- -type: specs.openrewrite.org/v1beta/recipe -name: org.philzen.oss.UseApacheStringUtils -displayName: Use Apache `StringUtils` -description: Replace Spring string utilities with Apache string utilities -recipeList: - - org.openrewrite.java.dependencies.AddDependency: - groupId: org.apache.commons - artifactId: commons-lang3 - version: latest.release - onlyIfUsing: org.springframework.util.StringUtils - configuration: implementation - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: org.springframework.util.StringUtils - newFullyQualifiedTypeName: org.apache.commons.lang3.StringUtils diff --git a/src/test/java/io/github/mboegers/openrewrite/testngtojupiter/MigrateAssertionsTests.java b/src/test/java/io/github/mboegers/openrewrite/testngtojupiter/MigrateAssertionsTests.java new file mode 100644 index 00000000..f8a488bc --- /dev/null +++ b/src/test/java/io/github/mboegers/openrewrite/testngtojupiter/MigrateAssertionsTests.java @@ -0,0 +1,1377 @@ +/* + * Copyright 2015-2024 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ +package io.github.mboegers.openrewrite.testngtojupiter; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.openrewrite.java.Assertions.java; + +class MigrateAssertionsTests implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.parser(JavaParser.fromJavaVersion() + .logCompilationWarningsAndErrors(true) + .classpath("junit-jupiter-api", "testng")) + .recipe(new MigrateAssertionsRecipes()); + } + + static Supplier> assertEqualsWithMessageArgumentStream = () -> Stream.of( + arguments("boolean", "boolean"), + arguments("boolean", "java.lang.Boolean"), + arguments("java.lang.Boolean", "boolean"), + arguments("java.lang.Boolean", "java.lang.Boolean"), + + arguments("byte", "byte"), + arguments("byte", "java.lang.Byte"), + arguments("java.lang.Byte", "byte"), + arguments("java.lang.Byte", "java.lang.Byte"), + + arguments("char", "char"), + arguments("char", "java.lang.Character"), + arguments("java.lang.Character", "char"), + arguments("java.lang.Character", "java.lang.Character"), + + arguments("double", "double"), + arguments("double", "java.lang.Double"), + arguments("java.lang.Double", "double"), + arguments("java.lang.Double", "java.lang.Double"), + + arguments("float", "float"), + arguments("float", "java.lang.Float"), + arguments("java.lang.Float", "float"), + arguments("java.lang.Float", "java.lang.Float"), + + arguments("java.lang.Short", "java.lang.Short"), + arguments("java.lang.Short", "short"), + arguments("short", "java.lang.Short"), + arguments("short", "short"), + + arguments("int", "int"), + arguments("int", "java.lang.Integer"), + arguments("java.lang.Integer", "int"), + arguments("java.lang.Integer", "java.lang.Integer"), + + arguments("java.lang.Long", "java.lang.Long"), + arguments("java.lang.Long", "long"), + arguments("long", "long"), + + arguments("java.lang.String", "java.lang.String") + ); + + static Supplier> assertArrayArgumentStream = () -> Stream.of( + arguments("boolean[]", "boolean[]"), + arguments("byte[]", "byte[]"), + arguments("char[]", "char[]"), + arguments("double[]", "double[]"), + arguments("float[]", "float[]"), + arguments("short[]", "short[]"), + arguments("int[]", "int[]"), + arguments("long[]", "long[]"), + arguments("Object[]", "Object[]") + ); + + static Stream assertEqualsArgumentsWithMessage() { + return assertEqualsWithMessageArgumentStream.get(); + } + + static Stream assertEqualsArgumentsWithoutMessage() { + return Stream.concat( + assertEqualsWithMessageArgumentStream.get(), + // ↓ there is no overload for this one with a message argument in TestNG + Stream.of(arguments("long", "java.lang.Long")) + ); + } + + static Stream toAssertArguments_shorterList() { + return Stream.of( + arguments("java.lang.Boolean", "java.lang.Boolean"), + arguments("java.lang.Character", "java.lang.Character"), + arguments("double", "double"), + arguments("java.lang.Short", "short"), + arguments("long", "java.lang.Long"), + arguments("java.lang.String", "java.lang.String") + ); + } + + static Stream toAssertArrayArguments() { + return assertArrayArgumentStream.get(); + } + + @Nested class MigrateAssertEquals { + + @Nested class WithErrorMessage { + + @MethodSource("io.github.mboegers.openrewrite.testngtojupiter.MigrateAssertionsTests#assertEqualsArgumentsWithMessage") + @ParameterizedTest void becomesAssertEquals_forPrimitiveAndBoxedArguments(String actual, String expected) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertEquals(actual, expected, "Test failed badly"); + } + } + """.formatted(actual, expected), + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertEquals(expected, actual, "Test failed badly"); + } + } + """.formatted(actual, expected) + )); + } + + @MethodSource("io.github.mboegers.openrewrite.testngtojupiter.MigrateAssertionsTests#toAssertArrayArguments") + @ParameterizedTest void becomesAssertArrayEquals_forArrays(String actual, String expected) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertEquals(actual, expected, "Test failed badly"); + } + } + """.formatted(actual, expected), + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertArrayEquals(expected, actual, "Test failed badly"); + } + } + """.formatted(actual, expected) + )); + } + + @Test void becomesSpecialAssertArrayEquals_forIterators() { + // language=java + rewriteRun(java( + """ + import java.util.Iterator; + import java.util.List; + import org.testng.Assert; + + class MyTest { + void testMethod() { + Iterator actual = List.of("a", "b").iterator(); + Iterator expected = List.of("b", "a").iterator(); + + Assert.assertEquals(actual, expected, "Kaboom."); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + import java.util.Iterator; + import java.util.List; + import java.util.Spliterators; + import java.util.stream.StreamSupport; + + class MyTest { + void testMethod() { + Iterator actual = List.of("a", "b").iterator(); + Iterator expected = List.of("b", "a").iterator(); + + Assertions.assertArrayEquals(StreamSupport.stream(Spliterators.spliteratorUnknownSize(expected, 0), false).toArray(), StreamSupport.stream(Spliterators.spliteratorUnknownSize(actual, 0), false).toArray(), "Kaboom."); + } + } + """ + )); + } + + @ValueSource(strings = {"float", "double"}) + @ParameterizedTest void deltaFunctionIsMigrated(String type) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertEquals(actual, expected, %s, "Test failed badly"); + } + } + """.formatted(type, type, type.equals("float") ? "0.1f" : "0.2d"), + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertEquals(expected, actual, %s, "Test failed badly"); + } + } + """.formatted(type, type, type.equals("float") ? "0.1f" : "0.2d") + )); + } + } + + @Nested class WithoutErrorMessage { + + @MethodSource("io.github.mboegers.openrewrite.testngtojupiter.MigrateAssertionsTests#assertEqualsArgumentsWithoutMessage") + @ParameterizedTest void becomesAssertEquals_forPrimitiveAndBoxedArguments(String actual, String expected) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertEquals(actual, expected); + } + } + """.formatted(actual, expected), + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertEquals(expected, actual); + } + } + """.formatted(actual, expected) + )); + } + + @MethodSource("io.github.mboegers.openrewrite.testngtojupiter.MigrateAssertionsTests#toAssertArrayArguments") + @ParameterizedTest void becomesAssertArrayEquals_forArrays(String actual, String expected) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertEquals(actual, expected); + } + } + """.formatted(actual, expected), + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertArrayEquals(expected, actual); + } + } + """.formatted(actual, expected) + )); + } + + @Test void becomesSpecialAssertArrayEquals_forIterators() { + // language=java + rewriteRun(java( + """ + import java.util.Iterator; + import java.util.List; + import org.testng.Assert; + + class MyTest { + void testMethod() { + Iterator actual = List.of("a", "b").iterator(); + Iterator expected = List.of("b", "a").iterator(); + + Assert.assertEquals(actual, expected); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + import java.util.Iterator; + import java.util.List; + import java.util.Spliterators; + import java.util.stream.StreamSupport; + + class MyTest { + void testMethod() { + Iterator actual = List.of("a", "b").iterator(); + Iterator expected = List.of("b", "a").iterator(); + + Assertions.assertArrayEquals(StreamSupport.stream(Spliterators.spliteratorUnknownSize(expected, 0), false).toArray(), StreamSupport.stream(Spliterators.spliteratorUnknownSize(actual, 0), false).toArray()); + } + } + """ + )); + } + + @ValueSource(strings = {"float", "double"}) + @ParameterizedTest void deltaFunctionIsMigrated(String type) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertEquals(actual, expected, %s); + } + } + """.formatted(type, type, type.equals("float") ? "0.1f" : "0.2d"), + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertEquals(expected, actual, %s); + } + } + """.formatted(type, type, type.equals("float") ? "0.1f" : "0.2d") + )); + } + } + } + + @Nested class MigrateAssertEqualsNoOrder { + + @SuppressWarnings("SimplifyStreamApiCallChains") + @Nested class WithErrorMessage { + + @Test void migratesCollectionToAssertArrayEquals() { + // language=java + rewriteRun(java( + """ + import java.util.Arrays; + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.assertEqualsNoOrder(Arrays.asList("a", "b"), Arrays.asList("b", "a"), "Should contain the same elements"); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + import java.util.Arrays; + + class MyTest { + void testMethod() { + Assertions.assertArrayEquals(Arrays.asList("b", "a").stream().sorted().toArray(), Arrays.asList("a", "b").stream().sorted().toArray(), "Should contain the same elements"); + } + } + """ + )); + } + + @Test void migratesArrayToAssertArrayEquals() { + // language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.assertEqualsNoOrder(new String[] {"actual"}, new String[] {"expected"}, "Should contain the same elements"); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + import java.util.Arrays; + + class MyTest { + void testMethod() { + Assertions.assertArrayEquals(Arrays.stream(new String[]{"expected"}).sorted().toArray(), Arrays.stream(new String[]{"actual"}).sorted().toArray(), "Should contain the same elements"); + } + } + """ + )); + } + + @Test void migratesIteratorToAssertArrayEquals() { + // language=java + rewriteRun(java( + """ + import java.util.Iterator; + import java.util.List; + import org.testng.Assert; + + class MyTest { + void testMethod() { + Iterator actual = List.of("a", "b").iterator(); + Iterator expected = List.of("b", "a").iterator(); + + Assert.assertEqualsNoOrder(actual, expected, "Should contain the same elements"); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + import java.util.Iterator; + import java.util.List; + import java.util.Spliterators; + import java.util.stream.StreamSupport; + + class MyTest { + void testMethod() { + Iterator actual = List.of("a", "b").iterator(); + Iterator expected = List.of("b", "a").iterator(); + + Assertions.assertArrayEquals(StreamSupport.stream(Spliterators.spliteratorUnknownSize(expected, 0), false).sorted().toArray(), StreamSupport.stream(Spliterators.spliteratorUnknownSize(actual, 0), false).sorted().toArray(), "Should contain the same elements"); + } + } + """ + )); + } + } + + @SuppressWarnings("SimplifyStreamApiCallChains") + @Nested class WithoutErrorMessage { + + @Test void migratesCollectionToAssertArrayEquals() { + // language=java + rewriteRun(java( + """ + import java.util.Arrays; + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.assertEqualsNoOrder(Arrays.asList("a", "b"), Arrays.asList("b", "a")); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + import java.util.Arrays; + + class MyTest { + void testMethod() { + Assertions.assertArrayEquals(Arrays.asList("b", "a").stream().sorted().toArray(), Arrays.asList("a", "b").stream().sorted().toArray()); + } + } + """ + )); + } + + @Test void migratesArrayToAssertArrayEquals() { + // language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.assertEqualsNoOrder(new String[] {"actual"}, new String[] {"expected"}); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + import java.util.Arrays; + + class MyTest { + void testMethod() { + Assertions.assertArrayEquals(Arrays.stream(new String[]{"expected"}).sorted().toArray(), Arrays.stream(new String[]{"actual"}).sorted().toArray()); + } + } + """ + )); + } + + @Test void migratesIteratorToAssertArrayEquals() { + // language=java + rewriteRun(java( + """ + import java.util.Iterator; + import java.util.List; + import org.testng.Assert; + + class MyTest { + void testMethod() { + Iterator actual = List.of("a", "b").iterator(); + Iterator expected = List.of("b", "a").iterator(); + + Assert.assertEqualsNoOrder(actual, expected); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + import java.util.Iterator; + import java.util.List; + import java.util.Spliterators; + import java.util.stream.StreamSupport; + + class MyTest { + void testMethod() { + Iterator actual = List.of("a", "b").iterator(); + Iterator expected = List.of("b", "a").iterator(); + + Assertions.assertArrayEquals(StreamSupport.stream(Spliterators.spliteratorUnknownSize(expected, 0), false).sorted().toArray(), StreamSupport.stream(Spliterators.spliteratorUnknownSize(actual, 0), false).sorted().toArray()); + } + } + """ + )); + } + } + } + + @Nested class MigrateAssertNotEquals { + + @Nested class WithErrorMessage { + + @MethodSource("io.github.mboegers.openrewrite.testngtojupiter.MigrateAssertionsTests#toAssertArguments_shorterList") + @ParameterizedTest void isMigratedToJupiterEquivalent(String actual, String expected) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertNotEquals(actual, expected, "Test failed badly"); + } + } + """.formatted(actual, expected), """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertNotEquals(expected, actual, "Test failed badly"); + } + } + """.formatted(actual, expected) + )); + } + + @MethodSource("io.github.mboegers.openrewrite.testngtojupiter.MigrateAssertionsTests#toAssertArrayArguments") + @ParameterizedTest void isMigratedToJupiterEquivalent_wrappedInArrayToString_forArrays(String actual, String expected) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertNotEquals(actual, expected, "Test failed badly"); + } + } + """.formatted(actual, expected), + """ + import org.junit.jupiter.api.Assertions; + + import java.util.Arrays; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertNotEquals(Arrays.toString(expected), Arrays.toString(actual), "Test failed badly"); + } + } + """.formatted(actual, expected) + )); + } + + @Test void becomesSpecialAssertNotEquals_forIterators() { + // language=java + rewriteRun(java( + """ + import java.util.Iterator; + import java.util.List; + import org.testng.Assert; + + class MyTest { + void testMethod() { + Iterator actual = List.of("a", "b").iterator(); + Iterator expected = List.of("b", "a").iterator(); + + Assert.assertNotEquals(actual, expected, "Kaboom."); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + import java.util.Arrays; + import java.util.Iterator; + import java.util.List; + import java.util.Spliterators; + import java.util.stream.StreamSupport; + + class MyTest { + void testMethod() { + Iterator actual = List.of("a", "b").iterator(); + Iterator expected = List.of("b", "a").iterator(); + + Assertions.assertNotEquals(Arrays.toString(StreamSupport.stream(Spliterators.spliteratorUnknownSize(expected, 0), false).toArray()), Arrays.toString(StreamSupport.stream(Spliterators.spliteratorUnknownSize(actual, 0), false).toArray()), "Kaboom."); + } + } + """ + )); + } + + @ValueSource(strings = {"float", "double"}) + @ParameterizedTest void deltaFunctionIsMigrated(String type) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertNotEquals(actual, expected, %s, "Test failed badly"); + } + } + """.formatted(type, type, type.equals("float") ? "0.1f" : "0.2d"), + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertNotEquals(expected, actual, %s, "Test failed badly"); + } + } + """.formatted(type, type, type.equals("float") ? "0.1f" : "0.2d") + )); + } + } + + @Nested class WithoutErrorMessage { + + @MethodSource("io.github.mboegers.openrewrite.testngtojupiter.MigrateAssertionsTests#toAssertArguments_shorterList") + @ParameterizedTest void withoutErrorMessage(String actual, String expected) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertNotEquals(actual, expected); + } + } + """.formatted(actual, expected), + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertNotEquals(expected, actual); + } + } + """.formatted(actual, expected) + )); + } + + @MethodSource("io.github.mboegers.openrewrite.testngtojupiter.MigrateAssertionsTests#toAssertArrayArguments") + @ParameterizedTest void isMigratedToJupiterEquivalent_wrappedInArrayToString_forArrays(String actual, String expected) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertNotEquals(actual, expected); + } + } + """.formatted(actual, expected), + """ + import org.junit.jupiter.api.Assertions; + + import java.util.Arrays; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertNotEquals(Arrays.toString(expected), Arrays.toString(actual)); + } + } + """.formatted(actual, expected) + )); + } + + @Test void becomesSpecialAssertNotEquals_forIterators() { + // language=java + rewriteRun(java( + """ + import java.util.Iterator; + import java.util.List; + import org.testng.Assert; + + class MyTest { + void testMethod() { + Iterator actual = List.of("a", "b").iterator(); + Iterator expected = List.of("b", "a").iterator(); + + Assert.assertNotEquals(actual, expected); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + import java.util.Arrays; + import java.util.Iterator; + import java.util.List; + import java.util.Spliterators; + import java.util.stream.StreamSupport; + + class MyTest { + void testMethod() { + Iterator actual = List.of("a", "b").iterator(); + Iterator expected = List.of("b", "a").iterator(); + + Assertions.assertNotEquals(Arrays.toString(StreamSupport.stream(Spliterators.spliteratorUnknownSize(expected, 0), false).toArray()), Arrays.toString(StreamSupport.stream(Spliterators.spliteratorUnknownSize(actual, 0), false).toArray())); + } + } + """ + )); + } + + @ValueSource(strings = {"float", "double"}) + @ParameterizedTest void deltaFunctionIsMigrated(String type) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertNotEquals(actual, expected, %s); + } + } + """.formatted(type, type, type.equals("float") ? "0.1f" : "0.2d"), + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertNotEquals(expected, actual, %s); + } + } + """.formatted(type, type, type.equals("float") ? "0.1f" : "0.2d") + )); + } + } + } + + @Nested class SkipAssertEqualsDeep { + + @ValueSource(strings = {"java.util.Map", "java.util.Set"}) + @ParameterizedTest void withErrorMessage(String type) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertEqualsDeep(actual, expected, "Test failed badly"); + } + } + """.formatted(type, type) + )); + } + + @Test + void withoutErrorMessage() { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + java.util.Map actual; + java.util.Map expected; + + Assert.assertEqualsDeep(actual, expected); + } + } + """ + )); + } + } + + @Nested class MigrateAssertFalse { + + @ValueSource(strings = {"boolean", "Boolean"}) + @ParameterizedTest void withErrorMessage(String type) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s expr; + + Assert.assertFalse(expr, "Test failed badly"); + } + } + """.formatted(type), """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s expr; + + Assertions.assertFalse(expr, "Test failed badly"); + } + } + """.formatted(type) + )); + } + + @ValueSource(strings = {"boolean", "Boolean"}) + @ParameterizedTest void withoutErrorMessage(String type) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s expr; + + Assert.assertFalse(expr); + } + } + """.formatted(type), """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s expr; + + Assertions.assertFalse(expr); + } + } + """.formatted(type) + )); + } + } + + @Nested class MigrateAssertTrue { + + @ValueSource(strings = {"boolean", "Boolean"}) + @ParameterizedTest void withErrorMessage(String type) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s expr; + + Assert.assertTrue(expr, "Test failed badly"); + } + } + """.formatted(type), """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s expr; + + Assertions.assertTrue(expr, "Test failed badly"); + } + } + """.formatted(type) + )); + } + + @ValueSource(strings = {"boolean", "Boolean"}) + @ParameterizedTest void withoutErrorMessage(String type) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s expr; + + Assert.assertTrue(expr); + } + } + """.formatted(type), """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s expr; + + Assertions.assertTrue(expr); + } + } + """.formatted(type) + )); + } + } + + @SuppressWarnings({"DataFlowIssue", "ObviousNullCheck"}) + @Nested class MigrateAssertNull { + + @Test void withErrorMessage() { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.assertNull(null, "Near-missed the billion dollar mistake."); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Assertions.assertNull(null, "Near-missed the billion dollar mistake."); + } + } + """ + )); + } + + @Test void withoutErrorMessage() { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.assertNull("Not null"); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Assertions.assertNull("Not null"); + } + } + """ + )); + } + } + + @SuppressWarnings({"DataFlowIssue", "ObviousNullCheck"}) + @Nested class MigrateAssertNotNull { + + @Test void withErrorMessage() { + // language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.assertNotNull(null, "The billion dollar mistake hit again."); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Assertions.assertNotNull(null, "The billion dollar mistake hit again."); + } + } + """ + )); + } + + @Test void withoutErrorMessage() { + // language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.assertNotNull("Not null"); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Assertions.assertNotNull("Not null"); + } + } + """ + )); + } + } + + @Nested class MigrateAssertNotSame { + + @Test void withMessage() { + // language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.assertNotSame(MyTest.class, Object.class, "Should not be the same object instance"); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Assertions.assertNotSame(Object.class, MyTest.class, "Should not be the same object instance"); + } + } + """ + )); + } + + @Test void withoutMessage() { + // language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.assertNotSame(MyTest.class, Object.class); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Assertions.assertNotSame(Object.class, MyTest.class); + } + } + """ + )); + } + } + + @SuppressWarnings("EqualsWithItself") + @Nested class MigrateAssertSame { + + @Test void withMessage() { + // language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Class actual = MyTest.class; + Assert.assertSame(MyTest.class, actual, "Should be the same object instance"); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Class actual = MyTest.class; + Assertions.assertSame(actual, MyTest.class, "Should be the same object instance"); + } + } + """ + )); + } + + @Test void withoutMessage() { + // language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.assertSame(MyTest.class, MyTest.class); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Assertions.assertSame(MyTest.class, MyTest.class); + } + } + """ + )); + } + } + + @Nested class MigrateAssertThrows { + + @Test void runnableWithNoExpectedType() { + // language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.assertThrows(() -> { throw new RuntimeException(); }); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Assertions.assertThrows(Throwable.class, () -> { + throw new RuntimeException(); + }); + } + } + """ + )); + } + + @Test void runnableWithNoExpectedThrowableType() { + // language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.assertThrows(Exception.class, () -> { throw new RuntimeException(); }); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Assertions.assertThrows(Exception.class, () -> { + throw new RuntimeException(); + }); + } + } + """ + )); + } + } + + @Test void migrateExpectThrows() { + // language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.expectThrows(Exception.class, () -> { throw new RuntimeException(); }); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Assertions.assertThrows(Exception.class, () -> { + throw new RuntimeException(); + }); + } + } + """ + )); + } + + @Nested class MigrateFail { + + @Test void withNoArguments() { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.fail(); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Assertions.fail(); + } + } + """ + )); + } + + @Test void withMessage() { + // language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.fail("Boom!"); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Assertions.fail("Boom!"); + } + } + """ + )); + } + + @Test void withMessageAndException() { + rewriteRun( + // language=java + java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + Assert.fail("Boom!", new Exception("Halted and caught fire.")); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + Assertions.fail("Boom!", new Exception("Halted and caught fire.")); + } + } + """ + )); + } + } +} diff --git a/src/test/java/org/philzen/oss/AppendToReleaseNotesTest.java b/src/test/java/org/philzen/oss/AppendToReleaseNotesTest.java deleted file mode 100644 index e66ade6b..00000000 --- a/src/test/java/org/philzen/oss/AppendToReleaseNotesTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -import org.junit.jupiter.api.Test; -import org.openrewrite.DocumentExample; -import org.openrewrite.test.RecipeSpec; -import org.openrewrite.test.RewriteTest; - -import java.nio.file.Paths; - -import static org.openrewrite.test.SourceSpecs.text; - -class AppendToReleaseNotesTest implements RewriteTest { - @Override - public void defaults(RecipeSpec spec) { - spec.recipe(new AppendToReleaseNotes("Hello world")); - } - - @Test - void createNewReleaseNotes() { - // Notice how the before text is null, indicating that the file does not exist yet. - // The after text is the content of the file after the recipe is applied. - rewriteRun( - text( - null, - """ - Hello world - """, - spec -> spec.path(Paths.get("RELEASE.md") - ) - ) - ); - } - - @DocumentExample - @Test - void editExistingReleaseNotes() { - // When the file does already exist, we assert the content is modified as expected. - rewriteRun( - text( - """ - You say goodbye, I say - """, - """ - You say goodbye, I say - Hello world - """, - spec -> spec.path(Paths.get("RELEASE.md") - ) - ) - ); - } -} diff --git a/src/test/java/org/philzen/oss/AssertEqualsToAssertThatTest.java b/src/test/java/org/philzen/oss/AssertEqualsToAssertThatTest.java deleted file mode 100644 index 53fb16e3..00000000 --- a/src/test/java/org/philzen/oss/AssertEqualsToAssertThatTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -import org.junit.jupiter.api.Test; -import org.openrewrite.DocumentExample; -import org.openrewrite.java.JavaParser; -import org.openrewrite.test.RecipeSpec; -import org.openrewrite.test.RewriteTest; - -import static org.openrewrite.java.Assertions.java; - -class AssertEqualsToAssertThatTest implements RewriteTest { - - @Override - public void defaults(RecipeSpec spec) { - spec.recipe(new AssertEqualsToAssertThat()) - .parser(JavaParser.fromJavaVersion() - .classpath("junit-jupiter-api")); - } - - @DocumentExample - @Test - void twoArgument() { - rewriteRun( - //language=java - java( - """ - import org.junit.jupiter.api.Assertions; - - class A { - void foo() { - Assertions.assertEquals(1, 2); - } - } - """, - """ - import org.assertj.core.api.Assertions; - - class A { - void foo() { - Assertions.assertThat(2).isEqualTo(1); - } - } - """ - ) - ); - } - - @Test - void withDescription() { - rewriteRun( - //language=java - java( - """ - import org.junit.jupiter.api.Assertions; - - class A { - void foo() { - Assertions.assertEquals(1, 2, "one equals two, everyone knows that"); - } - } - """, - """ - import org.assertj.core.api.Assertions; - - class A { - void foo() { - Assertions.assertThat(2).as("one equals two, everyone knows that").isEqualTo(1); - } - } - """ - ) - ); - } -} diff --git a/src/test/java/org/philzen/oss/ClassHierarchyTest.java b/src/test/java/org/philzen/oss/ClassHierarchyTest.java deleted file mode 100644 index 0c3b124e..00000000 --- a/src/test/java/org/philzen/oss/ClassHierarchyTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -import org.philzen.oss.table.ClassHierarchyReport; -import org.junit.jupiter.api.Test; -import org.openrewrite.test.RecipeSpec; -import org.openrewrite.test.RewriteTest; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.openrewrite.java.Assertions.java; - -class ClassHierarchyTest implements RewriteTest { - - @Override - public void defaults(RecipeSpec spec) { - spec.recipe(new ClassHierarchy()); - } - - @Test - void basic() { - rewriteRun( - spec -> spec.dataTable(ClassHierarchyReport.Row.class, rows -> { - assertThat(rows).containsExactly(new ClassHierarchyReport.Row("A", ClassHierarchyReport.Relationship.EXTENDS, "java.lang.Object")); - }), - //language=java - java( - """ - class A {} - """ - ) - ); - } - - @Test - void bExtendsA() { - rewriteRun( - spec -> spec.dataTable(ClassHierarchyReport.Row.class, rows -> { - assertThat(rows).containsExactly( - new ClassHierarchyReport.Row("A", ClassHierarchyReport.Relationship.EXTENDS, "java.lang.Object"), - new ClassHierarchyReport.Row("B", ClassHierarchyReport.Relationship.EXTENDS, "A")); - }), - //language=java - java( - """ - class A {} - """ - ), - //language=java - java( - """ - class B extends A {} - """ - ) - ); - } - - @Test - void interfaceRelationship() { - rewriteRun( - spec -> spec.dataTable(ClassHierarchyReport.Row.class, rows -> { - assertThat(rows).containsExactly( - new ClassHierarchyReport.Row("A", ClassHierarchyReport.Relationship.EXTENDS, "java.lang.Object"), - new ClassHierarchyReport.Row("A", ClassHierarchyReport.Relationship.IMPLEMENTS, "java.io.Serializable")); - }), - // language=java - java( - """ - import java.io.Serializable; - class A implements Serializable {} - """ - ) - ); - } -} diff --git a/src/test/java/org/philzen/oss/NoGuavaListsNewArrayListTest.java b/src/test/java/org/philzen/oss/NoGuavaListsNewArrayListTest.java deleted file mode 100644 index 71d34b3b..00000000 --- a/src/test/java/org/philzen/oss/NoGuavaListsNewArrayListTest.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2021 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 org.philzen.oss; - -import org.junit.jupiter.api.Test; -import org.openrewrite.DocumentExample; -import org.openrewrite.java.JavaParser; -import org.openrewrite.test.RecipeSpec; -import org.openrewrite.test.RewriteTest; - -import static org.openrewrite.java.Assertions.java; - -// This is a test for the NoGuavaListsNewArrayList recipe, as an example of how to write a test for an imperative recipe. -class NoGuavaListsNewArrayListTest implements RewriteTest { - - // Note, you can define defaults for the RecipeSpec and these defaults will be used for all tests. - // In this case, the recipe and the parser are common. See below, on how the defaults can be overridden - // per test. - @Override - public void defaults(RecipeSpec spec) { - // Note how we directly instantiate the recipe class here - spec.recipe(new NoGuavaListsNewArrayList()) - .parser(JavaParser.fromJavaVersion() - .logCompilationWarningsAndErrors(true) - // The before/after examples are using Guava classes, so we need to add the Guava library to the classpath - .classpath("guava")); - } - - @DocumentExample - @Test - void replaceWithNewArrayList() { - rewriteRun( - // There is an overloaded version or rewriteRun that allows the RecipeSpec to be customized specifically - // for a given test. In this case, the parser for this test is configured to not log compilation warnings. - spec -> spec - .parser(JavaParser.fromJavaVersion() - .logCompilationWarningsAndErrors(false) - .classpath("guava")), - // language=java - java( - """ - import com.google.common.collect.*; - - import java.util.List; - - class Test { - List cardinalsWorldSeries = Lists.newArrayList(); - } - """, - """ - import java.util.ArrayList; - import java.util.List; - - class Test { - List cardinalsWorldSeries = new ArrayList<>(); - } - """ - ) - ); - } - - @Test - void replaceWithNewArrayListIterable() { - rewriteRun( - // language=java - java( - """ - import com.google.common.collect.*; - - import java.util.Collections; - import java.util.List; - - class Test { - List l = Collections.emptyList(); - List cardinalsWorldSeries = Lists.newArrayList(l); - } - """, - """ - import java.util.ArrayList; - import java.util.Collections; - import java.util.List; - - class Test { - List l = Collections.emptyList(); - List cardinalsWorldSeries = new ArrayList<>(l); - } - """ - ) - ); - } - - @Test - void replaceWithNewArrayListWithCapacity() { - rewriteRun( - // language=java - java( - """ - import com.google.common.collect.*; - - import java.util.ArrayList; - import java.util.List; - - class Test { - List cardinalsWorldSeries = Lists.newArrayListWithCapacity(2); - } - """, - """ - import java.util.ArrayList; - import java.util.List; - - class Test { - List cardinalsWorldSeries = new ArrayList<>(2); - } - """) - ); - } - - // This test is to show that the `super.visitMethodInvocation` is needed to ensure that nested method invocations are visited. - @Test - void showNeedForSuperVisitMethodInvocation() { - rewriteRun( - //language=java - java( - """ - import com.google.common.collect.*; - - import java.util.Collections; - import java.util.List; - - class Test { - List cardinalsWorldSeries = Collections.unmodifiableList(Lists.newArrayList()); - } - """, - """ - import java.util.ArrayList; - import java.util.Collections; - import java.util.List; - - class Test { - List cardinalsWorldSeries = Collections.unmodifiableList(new ArrayList<>()); - } - """ - ) - ); - } - - // Often you want to make sure no changes are made when the target state is already achieved. - // To do so only passs in a before state and no after state to the rewriteRun method SourceSpecs. - @Test - void noChangeNecessary() { - rewriteRun( - //language=java - java( - """ - import java.util.ArrayList; - import java.util.Collections; - import java.util.List; - - class Test { - List cardinalsWorldSeries = Collections.unmodifiableList(new ArrayList<>()); - } - """ - ) - ); - } -} diff --git a/src/test/java/org/philzen/oss/SimplifyTernaryTest.java b/src/test/java/org/philzen/oss/SimplifyTernaryTest.java deleted file mode 100644 index 024cd88f..00000000 --- a/src/test/java/org/philzen/oss/SimplifyTernaryTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -import org.junit.jupiter.api.Test; -import org.openrewrite.DocumentExample; -import org.openrewrite.test.RecipeSpec; -import org.openrewrite.test.RewriteTest; - -import static org.openrewrite.java.Assertions.java; - -// This is a test for the SimplifyTernary recipe, as an example of how to write a test for a Refaster style recipe. -class SimplifyTernaryTest implements RewriteTest { - - @Override - public void defaults(RecipeSpec spec) { - // Note that we instantiate a generated class here, with `Recipes` appended to the Refaster class name - spec.recipe(new SimplifyTernaryRecipes()); - } - - @Test - @DocumentExample - void simplified() { - rewriteRun( - //language=java - java( - """ - class Test { - boolean trueCondition1 = true ? true : false; - boolean trueCondition2 = false ? false : true; - boolean trueCondition3 = booleanExpression() ? true : false; - boolean trueCondition4 = trueCondition1 && trueCondition2 ? true : false; - boolean trueCondition5 = !true ? false : true; - boolean trueCondition6 = !false ? true : false; - - boolean falseCondition1 = true ? false : true; - boolean falseCondition2 = !false ? false : true; - boolean falseCondition3 = booleanExpression() ? false : true; - boolean falseCondition4 = trueCondition1 && trueCondition2 ? false : true; - boolean falseCondition5 = !false ? false : true; - boolean falseCondition6 = !true ? true : false; - - boolean binary1 = booleanExpression() && booleanExpression() ? true : false; - boolean binary2 = booleanExpression() && booleanExpression() ? false : true; - boolean binary3 = booleanExpression() || booleanExpression() ? true : false; - boolean binary4 = booleanExpression() || booleanExpression() ? false : true; - - boolean booleanExpression() { - return true; - } - } - """, - """ - class Test { - boolean trueCondition1 = true; - boolean trueCondition2 = true; - boolean trueCondition3 = booleanExpression(); - boolean trueCondition4 = trueCondition1 && trueCondition2; - boolean trueCondition5 = true; - boolean trueCondition6 = true; - - boolean falseCondition1 = false; - boolean falseCondition2 = false; - boolean falseCondition3 = !booleanExpression(); - boolean falseCondition4 = !(trueCondition1 && trueCondition2); - boolean falseCondition5 = false; - boolean falseCondition6 = false; - - boolean binary1 = booleanExpression() && booleanExpression(); - boolean binary2 = !(booleanExpression() && booleanExpression()); - boolean binary3 = booleanExpression() || booleanExpression(); - boolean binary4 = !(booleanExpression() || booleanExpression()); - - boolean booleanExpression() { - return true; - } - } - """ - ) - ); - } - - // It's good practice to also include a test that verifies that the recipe doesn't change anything when it shouldn't. - @Test - void unchanged() { - rewriteRun( - //language=java - java( - """ - class Test { - boolean unchanged1 = booleanExpression() ? booleanExpression() : !booleanExpression(); - boolean unchanged2 = booleanExpression() ? true : !booleanExpression(); - boolean unchanged3 = booleanExpression() ? booleanExpression() : false; - - boolean booleanExpression() { - return true; - } - } - """ - ) - ); - } -} diff --git a/src/test/java/org/philzen/oss/StringIsEmptyTest.java b/src/test/java/org/philzen/oss/StringIsEmptyTest.java deleted file mode 100644 index 823bda6e..00000000 --- a/src/test/java/org/philzen/oss/StringIsEmptyTest.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.openrewrite.DocumentExample; -import org.openrewrite.Recipe; -import org.openrewrite.test.RecipeSpec; -import org.openrewrite.test.RewriteTest; - -import static org.openrewrite.java.Assertions.java; - -@Disabled("Remove this annotation to run the tests once you implement the recipe") -class StringIsEmptyTest implements RewriteTest { - - @Override - public void defaults(RecipeSpec spec) { - // Note that we instantiate a generated class here, with `Recipes` appended to the Refaster class name - // You might need to trigger an explicit build of your project to generate this class with Ctrl + F9 - - // TODO: Uncomment the line below once you have implemented the recipe - //spec.recipe(new StringIsEmptyRecipe()); - } - - @DocumentExample - @Test - void standardizeStringIsEmpty() { - // Notice how we pass in both the "before" and "after" code snippets - // This indicates that we expect the recipe to transform the "before" code snippet into the "after" code snippet - // If the recipe does not do this, the test will fail, and a diff will be shown - rewriteRun( - //language=java - java( - """ - class A { - void test(String s, boolean b) { - b = s.length() == 0; - b = 0 == s.length(); - b = s.length() < 1; - b = 1 > s.length(); - b = s.equals(""); - b = "".equals(s); - b = s.isEmpty(); - } - } - """, - """ - class A { - void test(String s, boolean b) { - b = s.isEmpty(); - b = s.isEmpty(); - b = s.isEmpty(); - b = s.isEmpty(); - b = s.isEmpty(); - b = s.isEmpty(); - b = s.isEmpty(); - } - } - """ - ) - ); - } - - @Test - void showStringTypeMatchAndSimplification() { - // Notice how the recipe will match anything that is of type String, not just local variables - // Take a closer look at the last two replacements to `true` and `false`. - // Open up the generated recipe and see if you can work out why those are replaced with booleans! - rewriteRun( - //language=java - java( - """ - class A { - String field; - - String methodCall() { - return "Hello World"; - } - - void test(String argument) { - boolean bool1 = field.length() == 0; - boolean bool2 = methodCall().length() == 0; - boolean bool3 = argument.length() == 0; - boolean bool4 = "".length() == 0; - boolean bool5 = "literal".length() == 0; - } - } - """, - """ - class A { - String field; - - String methodCall() { - return "Hello World"; - } - - void test(String argument) { - boolean bool1 = field.isEmpty(); - boolean bool2 = methodCall().isEmpty(); - boolean bool3 = argument.isEmpty(); - boolean bool4 = true; - boolean bool5 = false; - } - } - """ - ) - ); - } - - @Test - void doNothingForStringIsEmpty() { - // Notice how we only pass in the "before" code snippet, and not the "after" code snippet - // That indicates that we expect the recipe to do nothing in this case, and will fail if it does anything - rewriteRun( - //language=java - java( - """ - class A { - void test(String s, boolean b) { - b = s.isEmpty(); - } - } - """ - ) - ); - } - - @Test - void doNothingForCharSequence() { - // When a different type is used, the recipe should do nothing - // See if you can modify the recipe to handle CharSequence as well, or create a separate recipe for it - rewriteRun( - //language=java - java( - """ - class A { - void test(CharSequence s, boolean b) { - b = s.length() == 0; - } - } - """ - ) - ); - } - - @Test - void recipeDocumentation() { - // This is a test to validate the correctness of the documentation in the recipe - // By default you get generated documentation, but you can customize it through the RecipeDescriptor annotation - Recipe recipe = null; // TODO: = new StringIsEmptyRecipe(); - String displayName = recipe.getDisplayName(); - String description = recipe.getDescription(); - assert "Standardize empty String checks".equals(displayName) : displayName; - assert "Replace calls to `String.length() == 0` with `String.isEmpty()`.".equals(description) : description; - } -} diff --git a/src/test/java/org/philzen/oss/UpdateConcoursePipelineTest.java b/src/test/java/org/philzen/oss/UpdateConcoursePipelineTest.java deleted file mode 100644 index 7e07d4ce..00000000 --- a/src/test/java/org/philzen/oss/UpdateConcoursePipelineTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -import org.junit.jupiter.api.Test; -import org.openrewrite.DocumentExample; -import org.openrewrite.test.RewriteTest; - -import java.nio.file.Paths; - -import static org.openrewrite.yaml.Assertions.yaml; - -class UpdateConcoursePipelineTest implements RewriteTest { - - @DocumentExample - @Test - void updateTagFilter() { - rewriteRun( - spec -> spec.recipe(new UpdateConcoursePipeline("8.2.0")), - //language=yaml - yaml( - """ - --- - resources: - - name: tasks - type: git - source: - uri: git@github.com:Example/concourse-tasks.git - tag_filter: 8.1.0 - """, - """ - --- - resources: - - name: tasks - type: git - source: - uri: git@github.com:Example/concourse-tasks.git - tag_filter: 8.2.0 - """, - spec -> spec.path(Paths.get("ci/pipeline.yml")) - ) - ); - } -} diff --git a/src/test/java/org/philzen/oss/UseApacheStringUtilsTest.java b/src/test/java/org/philzen/oss/UseApacheStringUtilsTest.java deleted file mode 100644 index d144163b..00000000 --- a/src/test/java/org/philzen/oss/UseApacheStringUtilsTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -import org.junit.jupiter.api.Test; -import org.openrewrite.DocumentExample; -import org.openrewrite.java.JavaParser; -import org.openrewrite.test.RecipeSpec; -import org.openrewrite.test.RewriteTest; - -import static org.openrewrite.java.Assertions.java; - -class UseApacheStringUtilsTest implements RewriteTest { - @Override - public void defaults(RecipeSpec spec) { - spec.recipeFromResources("org.philzen.oss.UseApacheStringUtils") - .parser(JavaParser.fromJavaVersion().classpath("commons-lang3", "spring-core")); - } - - @DocumentExample - @Test - void replacesStringEquals() { - rewriteRun( - //language=java - java( - """ - import org.springframework.util.StringUtils; - - class A { - boolean test(String s) { - return StringUtils.containsWhitespace(s); - } - } - """, - """ - import org.apache.commons.lang3.StringUtils; - - class A { - boolean test(String s) { - return StringUtils.containsWhitespace(s); - } - } - """ - ) - ); - } -} diff --git a/src/test/java/org/philzen/oss/UseOpenRewriteNullableTest.java b/src/test/java/org/philzen/oss/UseOpenRewriteNullableTest.java deleted file mode 100644 index 154beada..00000000 --- a/src/test/java/org/philzen/oss/UseOpenRewriteNullableTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2024 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 org.philzen.oss; - -import org.junit.jupiter.api.Test; -import org.openrewrite.DocumentExample; -import org.openrewrite.java.JavaParser; -import org.openrewrite.test.RecipeSpec; -import org.openrewrite.test.RewriteTest; - -import static org.openrewrite.java.Assertions.java; - -// This is a test for the UseOpenRewriteNullable recipe, as an example of how to write a test for a declarative recipe. -class UseOpenRewriteNullableTest implements RewriteTest { - @Override - public void defaults(RecipeSpec spec) { - spec - // Use the fully qualified class name of the recipe defined in src/main/resources/META-INF/rewrite/rewrite.yml - .recipeFromResources("org.philzen.oss.UseOpenRewriteNullable") - // The before and after text blocks contain references to annotations from these two classpath entries - .parser(JavaParser.fromJavaVersion().classpath("annotations", "rewrite-core")); - } - - @DocumentExample - @Test - void replacesNullableAnnotation() { - rewriteRun( - // Composite recipes are a hierarchy of recipes that can be applied in a single pass. - // To view what the composite recipe does, you can use the RecipePrinter to print the recipe to the console. - spec -> spec.printRecipe(() -> System.out::println), - //language=java - java( - """ - import org.jetbrains.annotations.Nullable; - - class A { - @Nullable - String s; - } - """, - """ - import org.openrewrite.internal.lang.Nullable; - - class A { - @Nullable - String s; - } - """ - ) - ); - } -} diff --git a/src/test/java/org/philzen/oss/research/AssertionsComparisonTest.java b/src/test/java/org/philzen/oss/research/AssertionsComparisonTest.java new file mode 100644 index 00000000..ae3e99c4 --- /dev/null +++ b/src/test/java/org/philzen/oss/research/AssertionsComparisonTest.java @@ -0,0 +1,405 @@ +package org.philzen.oss.research; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.assertj.core.api.ThrowableAssert; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.testng.Assert; + +import java.util.*; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; + +class AssertionsComparisonTest { + + static final Collection ABC_list = ImmutableList.of("a", "b", "c"); + static final Collection CBA_list = ImmutableList.of("c", "b", "a"); + + static final String[] ABC_array = {"a", "b", "c"}; + static final String[] CBA_array = {"c", "b", "a"}; + + static final Map ASC_numMap = ImmutableMap.of(42, "Meaning of Life", 1312, "All Computers Are Broken"); + static final Map DESC_numMap = ImmutableMap.of(1312, "All Computers Are Broken", 42, "Meaning of Life"); + static final Map OTHER_map = ImmutableMap.of("foo", "far", "baz", "qux"); + + static final Set ABC_set = ImmutableSet.of("a", "b", "c"); + static final Set CBA_set = ImmutableSet.of("c", "b", "a"); + static final Set XYZ_set = ImmutableSet.of("x", "y", "z"); + + @SuppressWarnings("AssertBetweenInconvertibleTypes") + @Nested class assertEquals { + + @Tag("mismatch") + @Test void array() { + thisWillFail(() -> Assert.assertEquals(ABC_array, CBA_array)); + thisWillFail(() -> Assertions.assertEquals(CBA_array, ABC_array)); + + thisWillPass(() -> Assert.assertEquals(ABC_array.clone(), ABC_array)); + thisWillFail(() -> Assertions.assertEquals(ABC_array, ABC_array.clone())); + + // possible migration + thisWillPass(() -> Assertions.assertEquals(Arrays.toString(ABC_array), Arrays.toString(ABC_array.clone()))); + } + + @Tag("mismatch") + @Test void iterator() { + thisWillFail(() -> Assert.assertEquals(ABC_list.iterator(), CBA_list.iterator())); + thisWillFail(() -> Assertions.assertEquals(CBA_list.iterator(), ABC_list.iterator())); + + thisWillPass(() -> Assert.assertEquals(ABC_list.iterator(), ABC_list.iterator())); + thisWillFail(() -> Assertions.assertEquals(ABC_list.iterator(), ABC_list.iterator())); + + // possible migration + thisWillPass(() -> Assertions.assertArrayEquals( + StreamSupport.stream(Spliterators.spliteratorUnknownSize(ABC_list.iterator(), 0), false).toArray(), + StreamSupport.stream(Spliterators.spliteratorUnknownSize(ABC_list.iterator(), 0), false).toArray() + )); + + thisWillFail(() -> Assertions.assertArrayEquals( + StreamSupport.stream(Spliterators.spliteratorUnknownSize(ABC_list.iterator(), 0), false).toArray(), + StreamSupport.stream(Spliterators.spliteratorUnknownSize(CBA_list.iterator(), 0), false).toArray() + )); + } + + @Test void collection() { + Collection actual = new ArrayList<>(ABC_list); + + thisWillPass(() -> Assert.assertEquals(actual, new ArrayList<>(ABC_list))); + thisWillPass(() -> Assertions.assertEquals(new ArrayList<>(ABC_list), actual)); + + thisWillFail(() -> Assert.assertEquals(actual, CBA_list)); + thisWillFail(() -> Assertions.assertEquals(CBA_list, actual)); + } + + @Test void doubleDelta() { + final double actual = 1d; + + thisWillPass(() -> Assert.assertEquals(actual, 2d, 1d)); + thisWillPass(() -> Assertions.assertEquals(2d, actual, 1d)); + + thisWillFail(() -> Assert.assertEquals(actual, 2d, .999d)); + thisWillFail(() -> Assertions.assertEquals(2d, actual, .999d)); + } + + @Tag("missing") + @Test void doubleArrayDelta() { + final double[] expected = new double[] {0d, 10d}; + final double[] actual = new double[] {1d, 9d}; + thisWillPass(() -> Assert.assertEquals(actual, expected, 1d)); + // there is no equivalent in Jupiter :/ + + thisWillFail(() -> Assert.assertEquals(actual, expected, .999d)); + // there is no equivalent in Jupiter :/ + + // possible migration equivalent + thisWillPass(() -> Assertions.assertAll(() -> { + Assertions.assertEquals(expected.length, actual.length, "Arrays don't have the same size."); + for (int i = 0; i < actual.length; i++) { + Assertions.assertEquals(expected[i], actual[i], 1d); + } + })); + thisWillFail(() -> Assertions.assertAll(() -> { + Assertions.assertEquals(expected.length, actual.length, "Arrays don't have the same size."); + for (int i = 0; i < actual.length; i++) { + Assertions.assertEquals(expected[i], actual[i], .999d); + } + })); + } + + @Test void floatDelta() { + final float actual = 1f; + + thisWillPass(() -> Assert.assertEquals(actual, 2f, 1f)); + thisWillPass(() -> Assertions.assertEquals(2f, actual, 1f)); + + thisWillFail(() -> Assert.assertEquals(actual, 2f, .999f)); + thisWillFail(() -> Assertions.assertEquals(2f, actual, .999f)); + } + + @Tag("missing") + @Test void floatArrayDelta() { + final double[] expected = new double[] {0d, 10f}; + final double[] actual = new double[] {1f, 9f}; + thisWillPass(() -> Assert.assertEquals(actual, expected, 1f)); + // there is no equivalent in Jupiter :/ + + thisWillFail(() -> Assert.assertEquals(actual, expected, .999f)); + // there is no equivalent in Jupiter :/ + + // possible migration equivalent + thisWillPass(() -> Assertions.assertAll(() -> { + Assertions.assertEquals(expected.length, actual.length, "Arrays don't have the same size."); + for (int i = 0; i < actual.length; i++) { + Assertions.assertEquals(expected[i], actual[i], 1f); + } + })); + thisWillFail(() -> Assertions.assertAll(() -> { + Assertions.assertEquals(expected.length, actual.length, "Arrays don't have the same size."); + for (int i = 0; i < actual.length; i++) { + Assertions.assertEquals(expected[i], actual[i], .999f); + } + })); + } + + @Test void iterable() { + final Iterable actual = ABC_list; + final Iterable expected = new ArrayList<>(ABC_list); + + thisWillPass(() -> Assert.assertEquals(actual, expected)); + thisWillPass(() -> Assertions.assertEquals(expected, actual)); + + thisWillFail(() -> Assert.assertEquals(actual, CBA_list)); + thisWillFail(() -> Assertions.assertEquals(CBA_list, actual)); + } + + @Test void map() { + final Map actual = ASC_numMap; + + thisWillPass(() -> Assert.assertEquals(actual, new LinkedHashMap<>(ASC_numMap))); + thisWillPass(() -> Assertions.assertEquals(new LinkedHashMap<>(ASC_numMap), actual)); + + // order does not matter + thisWillPass(() -> Assert.assertEquals(actual, DESC_numMap)); + thisWillPass(() -> Assertions.assertEquals(DESC_numMap, actual)); + + thisWillFail(() -> Assert.assertEquals(actual, OTHER_map)); + thisWillFail(() -> Assertions.assertEquals(OTHER_map, actual)); + } + + @Test void set() { + final Set actual = ASC_numMap.keySet(); + + // order does not matter + thisWillPass(() -> Assert.assertEquals(actual, DESC_numMap.keySet())); + thisWillPass(() -> Assertions.assertEquals(DESC_numMap.keySet(), actual)); + + thisWillFail(() -> Assert.assertEquals(actual, OTHER_map.keySet())); + thisWillFail(() -> Assertions.assertEquals(OTHER_map.keySet(), actual)); + } + } + + @Tag("missing") + @Nested class assertEqualsNoOrder { // there is no equivalent in Jupiter + + @Test void collection() { + final Collection expected = ABC_list; + + thisWillPass(() -> Assert.assertEqualsNoOrder(CBA_list, expected)); + thisWillFail(() -> Assert.assertEqualsNoOrder(List.of("x", "y"), expected)); + + // possible migration (string only) + thisWillPass(() -> Assertions.assertLinesMatch(expected.stream().sorted(), CBA_list.stream().sorted())); + thisWillFail(() -> Assertions.assertLinesMatch(expected.stream().sorted(), Stream.of("x", "y").sorted())); + + // possible migration (any type) + thisWillPass(() -> Assertions.assertArrayEquals( + expected.stream().sorted().toArray(), CBA_list.stream().sorted().toArray() + )); + thisWillFail(() -> Assertions.assertArrayEquals( + expected.stream().sorted().toArray(), Stream.of("x", "y").sorted().toArray() + )); + } + + @Test void iterator() { + thisWillPass(() -> Assert.assertEqualsNoOrder(CBA_list.iterator(), ABC_list.iterator())); + thisWillFail(() -> Assert.assertEqualsNoOrder(List.of("x", "y").iterator(), ABC_list.iterator())); + + // possible migration + thisWillPass(() -> Assertions.assertArrayEquals( + StreamSupport.stream(Spliterators.spliteratorUnknownSize(CBA_list.iterator(), 0), false).sorted().toArray(), + StreamSupport.stream(Spliterators.spliteratorUnknownSize(ABC_list.iterator(), 0), false).sorted().toArray() + )); + thisWillFail(() -> Assertions.assertArrayEquals( + StreamSupport.stream(Spliterators.spliteratorUnknownSize(CBA_list.iterator(), 0), false).sorted().toArray(), + StreamSupport.stream(Spliterators.spliteratorUnknownSize(List.of("x", "y").iterator(), 0), false).sorted().toArray() + )); + } + + @Test void objectArray() { + final Object[] expected = new String[]{"a", "b", "c", "d"}; + + thisWillPass(() -> Assert.assertEqualsNoOrder(new String[]{"b", "a", "d", "c"}, expected)); + + thisWillFail(() -> Assert.assertEqualsNoOrder(new String[]{"b", "b", "a", "d", "c"}, expected)); + + // possible migration + thisWillPass(() -> Assertions.assertArrayEquals( + Arrays.stream(expected).sorted().toArray(), + Arrays.stream(new String[]{"b", "a", "d", "c"}).sorted().toArray() + )); + + // possible migration + thisWillFail(() -> Assertions.assertEquals( + Arrays.stream(expected).sorted(), + Arrays.stream(new String[]{"b", "b", "a", "d", "c"}).sorted() + )); + } + } + + @Nested class assertNotEquals { + + @Tag("mismatch") + @Test void array() { + thisWillPass(() -> Assert.assertNotEquals(ABC_array, CBA_array)); + thisWillPass(() -> Assertions.assertNotEquals(CBA_array, ABC_array)); + + thisWillFail(() -> Assert.assertNotEquals(ABC_array.clone(), ABC_array)); + thisWillPass(() -> Assertions.assertNotEquals(ABC_array, ABC_array.clone())); + + // possible migration + thisWillFail(() -> Assertions.assertNotEquals(Arrays.toString(ABC_array), Arrays.toString(ABC_array.clone()))); + } + + @Test void collection() { + thisWillPass(() -> Assert.assertNotEquals(ABC_list, CBA_list)); + thisWillPass(() -> Assertions.assertNotEquals(CBA_list, ABC_list)); + + thisWillFail(() -> Assert.assertNotEquals(ImmutableList.copyOf(ABC_list), ABC_list)); + thisWillFail(() -> Assertions.assertNotEquals(ABC_list, ImmutableList.copyOf(ABC_list))); + } + + @Tag("mismatch") + @Test void iterator() { + thisWillPass(() -> Assert.assertNotEquals(ABC_list.iterator(), CBA_list.iterator())); + thisWillPass(() -> Assertions.assertNotEquals(CBA_list.iterator(), ABC_list.iterator())); + + thisWillFail(() -> Assert.assertNotEquals(ABC_list.iterator(), ABC_list.iterator())); + thisWillPass(() -> Assertions.assertNotEquals(ABC_list.iterator(), ABC_list.iterator())); + + // possible migration + thisWillPass(() -> Assertions.assertNotEquals( + Arrays.toString(StreamSupport.stream(Spliterators.spliteratorUnknownSize(ABC_list.iterator(), 0), false).toArray()), + Arrays.toString(StreamSupport.stream(Spliterators.spliteratorUnknownSize(CBA_list.iterator(), 0), false).toArray()) + )); + thisWillFail(() -> Assertions.assertNotEquals( + Arrays.toString(StreamSupport.stream(Spliterators.spliteratorUnknownSize(ABC_list.iterator(), 0), false).toArray()), + Arrays.toString(StreamSupport.stream(Spliterators.spliteratorUnknownSize(ABC_list.iterator(), 0), false).toArray()) + )); + } + + @Test void map() { + thisWillPass(() -> Assert.assertNotEquals(ASC_numMap, OTHER_map)); + thisWillPass(() -> Assertions.assertNotEquals(OTHER_map, ASC_numMap)); + + // order does not matter + thisWillFail(() -> Assert.assertNotEquals(ASC_numMap, DESC_numMap)); + thisWillFail(() -> Assertions.assertNotEquals(DESC_numMap, ASC_numMap)); + + thisWillFail(() -> Assert.assertNotEquals(ASC_numMap, ASC_numMap)); + thisWillFail(() -> Assertions.assertNotEquals(ASC_numMap, ASC_numMap)); + } + + @Test void set() { + thisWillPass(() -> Assert.assertNotEquals(ABC_set, XYZ_set)); + thisWillPass(() -> Assertions.assertNotEquals(XYZ_set, ABC_set)); + + // order does not matter + thisWillFail(() -> Assert.assertNotEquals(ABC_set, CBA_set)); + thisWillFail(() -> Assertions.assertNotEquals(CBA_set, ABC_set)); + + thisWillFail(() -> Assert.assertNotEquals(ABC_set, ABC_set)); + thisWillFail(() -> Assertions.assertNotEquals(ABC_set, ABC_set)); + } + + @Test void doubleDelta() { + final double actual = 1d; + thisWillPass(() -> Assert.assertNotEquals(actual, 2d, .999d)); + thisWillPass(() -> Assertions.assertNotEquals(2d, actual, .999d)); + + thisWillFail(() -> Assert.assertNotEquals(actual, 2d, 1d)); + thisWillFail(() -> Assertions.assertNotEquals(2d, actual, 1d)); + } + + @Test void floatDelta() { + final float actual = 1f; + + thisWillPass(() -> Assert.assertNotEquals(actual, 2f, .999f)); + thisWillPass(() -> Assertions.assertNotEquals(2f, actual, .999f)); + + thisWillFail(() -> Assert.assertNotEquals(actual, 2f, 1f)); + thisWillFail(() -> Assertions.assertNotEquals(2f, actual, 1f)); + } + } + + @Test void assertNotSame() { + final Collection expected = ABC_list; + + thisWillPass(() -> Assertions.assertNotSame(new ArrayList<>(ABC_list), expected)); + thisWillPass(() -> Assert.assertNotSame(expected, new ArrayList<>(ABC_list))); + + thisWillFail(() -> Assert.assertNotSame(ABC_list, expected)); + thisWillFail(() -> Assertions.assertNotSame(expected, ABC_list)); + } + + @Test void assertSame() { + final Collection expected = ABC_list; + + thisWillPass(() -> Assert.assertSame(ABC_list, expected)); + thisWillPass(() -> Assertions.assertSame(expected, ABC_list)); + + thisWillFail(() -> Assert.assertSame(new ArrayList<>(ABC_list), expected)); + thisWillFail(() -> Assertions.assertSame(expected, new ArrayList<>(ABC_list))); + } + + @SuppressWarnings("unused") + @Nested class assertThrows { + + @Test void onlyRunnable() { + thisWillPass(() -> Assert.assertThrows(() -> { throw new RuntimeException(); })); + thisWillFail(() -> Assert.assertThrows(() -> { final var meaningful = 42; })); + + // not a 1:1 equivalent in terms of arguments, but functionally completely equivalent + thisWillPass(() -> Assertions.assertThrows(Throwable.class, () -> { throw new RuntimeException(); })); + thisWillFail(() -> Assertions.assertThrows(Throwable.class, () -> { final var meaningful = 42; })); + } + + @Test void expectedExceptionClassCheck() { + thisWillPass(() -> Assert.assertThrows(RuntimeException.class, () -> { throw new RuntimeException(); })); + thisWillFail(() -> Assert.assertThrows(Error.class, () -> { throw new RuntimeException(); })); + + thisWillPass(() -> Assertions.assertThrows(RuntimeException.class, () -> { throw new RuntimeException(); })); + thisWillFail(() -> Assertions.assertThrows(Error.class, () -> { throw new RuntimeException(); })); + + // additional test to show that they both do NOT check for the exact type, "instance of"-truthyness is enough + thisWillPass(() -> Assert.assertThrows(Exception.class, () -> { throw new RuntimeException(); })); + thisWillFail(() -> Assert.assertThrows(Error.class, () -> { throw new RuntimeException(); })); + + thisWillPass(() -> Assertions.assertThrows(Exception.class, () -> { throw new RuntimeException(); })); + thisWillFail(() -> Assertions.assertThrows(Error.class, () -> { throw new RuntimeException(); })); + } + + /** + * This is the actual function being called by {@link Assert#assertThrows(Assert.ThrowingRunnable)} + * and {@link Assert#assertThrows(Class, Assert.ThrowingRunnable)}, so these migrations are identical + */ + @Test void expectThrows() { + thisWillPass(() -> Assert.expectThrows(RuntimeException.class, () -> { throw new RuntimeException(); })); + thisWillFail(() -> Assert.expectThrows(Error.class, () -> { throw new RuntimeException(); })); + + thisWillPass(() -> Assertions.assertThrows(RuntimeException.class, () -> { throw new RuntimeException(); })); + thisWillFail(() -> Assertions.assertThrows(Error.class, () -> { throw new RuntimeException(); })); + + // additional test to show that they both do NOT check for the exact type, "instance of"-truthyness is enough + thisWillPass(() -> Assert.expectThrows(Exception.class, () -> { throw new RuntimeException(); })); + thisWillFail(() -> Assert.expectThrows(Error.class, () -> { throw new RuntimeException(); })); + + thisWillPass(() -> Assertions.assertThrows(Exception.class, () -> { throw new RuntimeException(); })); + thisWillFail(() -> Assertions.assertThrows(Error.class, () -> { throw new RuntimeException(); })); + } + } + + void thisWillPass(final ThrowableAssert.ThrowingCallable code) { + assertThatNoException().isThrownBy(code); + } + + void thisWillFail(final ThrowableAssert.ThrowingCallable code) { + assertThatExceptionOfType(AssertionError.class).isThrownBy(code); + } +} diff --git a/src/test/java/org/philzen/oss/testng/MigrateMismatchedAssertionsTest.java b/src/test/java/org/philzen/oss/testng/MigrateMismatchedAssertionsTest.java new file mode 100644 index 00000000..627e4f9e --- /dev/null +++ b/src/test/java/org/philzen/oss/testng/MigrateMismatchedAssertionsTest.java @@ -0,0 +1,89 @@ +package org.philzen.oss.testng; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class MigrateMismatchedAssertionsTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new MigrateMismatchedAssertions()); + } + + @ValueSource(strings = {"float[]", "double[]"}) + @ParameterizedTest + void deltaFunctionForArraysIsMigrated(String type) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertEquals(actual, expected, %s); + } + } + """.formatted(type, type, type.equals("float[]") ? "0.1f" : "0.2d"), + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertAll(() -> { + Assertions.assertEquals(expected.length, actual.length, "Arrays don't have the same size."); + for (int i = 0; i < actual.length; i++) { + Assertions.assertEquals(expected[i], actual[i], %s); + } + }); + } + } + """.formatted(type, type, type.equals("float[]") ? "0.1f" : "0.2d") + )); + } + + @ValueSource(strings = {"float[]", "double[]"}) + @ParameterizedTest + void deltaFunctionForArraysIsMigratedWithMessage(String type) { + //language=java + rewriteRun(java( + """ + import org.testng.Assert; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assert.assertEquals(actual, expected, %s, "Those values are way off."); + } + } + """.formatted(type, type, type.equals("float[]") ? "0.1f" : "0.2d"), + """ + import org.junit.jupiter.api.Assertions; + + class MyTest { + void testMethod() { + %s actual; + %s expected; + + Assertions.assertAll(() -> { + Assertions.assertEquals(expected.length, actual.length, "Arrays don't have the same size."); + for (int i = 0; i < actual.length; i++) { + Assertions.assertEquals(expected[i], actual[i], %s, "Those values are way off."); + } + }); + } + } + """.formatted(type, type, type.equals("float[]") ? "0.1f" : "0.2d") + )); + } +} diff --git a/src/test/java/org/philzen/oss/testng/UpdateTestAnnotationToJunit5Test.java b/src/test/java/org/philzen/oss/testng/UpdateTestAnnotationToJunit5Test.java new file mode 100644 index 00000000..e3d203a3 --- /dev/null +++ b/src/test/java/org/philzen/oss/testng/UpdateTestAnnotationToJunit5Test.java @@ -0,0 +1,1268 @@ +package org.philzen.oss.testng; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +@SuppressWarnings({"groupsTestNG", "NewClassNamingConvention"}) +class UpdateTestAnnotationToJunit5Test implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new UpdateTestAnnotationToJunit5()); + } + + @Nested class NoAttributes { + + @Nested class onClass { + + @Test void isMigratedToMethods() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + @Test + public class BazTest { + + public void shouldDoStuff() { + // + } + + public void shouldDoMoreStuff() { + // + } + } + """, + """ + import org.junit.jupiter.api.Test; + + public class BazTest { + + @Test + public void shouldDoStuff() { + // + } + + @Test + public void shouldDoMoreStuff() { + // + } + } + """ + )); + } + + @Test void isMigratedToMethods_preservingOtherAnnotations() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + @Deprecated @Test + public class BazTest { + + public void shouldDoStuff() { + // + } + } + """, + """ + import org.junit.jupiter.api.Test; + + @Deprecated + public class BazTest { + + @Test + public void shouldDoStuff() { + // + } + } + """ + )); + } + + /** + * Non-public method are executed only if they are explicitly annotated with `@Test` + */ + @Test void isMigratedOnlyToPublicMethods() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + @Test + public class BazTest { + + public void shouldDoStuff() { + // + } + + void thisAintNoTest() { + // + } + } + """, + """ + import org.junit.jupiter.api.Test; + + public class BazTest { + + @Test + public void shouldDoStuff() { + // + } + + void thisAintNoTest() { + // + } + } + """ + )); + } + + @Test void isMigratedToMethods_whenClassIsTyped() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + @Test + class BazTest { + + public void shouldDoStuff() { + // + } + + public void shouldDoMoreStuff() { + // + } + } + """, + """ + import org.junit.jupiter.api.Test; + + class BazTest { + + @Test + public void shouldDoStuff() { + // + } + + @Test + public void shouldDoMoreStuff() { + // + } + } + """ + )); + } + + @Test void isMigratedToMethods_whenFullyQualified() { + // language=java + rewriteRun(java( + """ + package de.foo.bar; + + @org.testng.annotations.Test + public class BazTest { + + public void shouldDoStuff() { + // + } + + public void shouldDoMoreStuff() { + // + } + } + """, + """ + package de.foo.bar; + + public class BazTest { + + @org.junit.jupiter.api.Test + public void shouldDoStuff() { + // + } + + @org.junit.jupiter.api.Test + public void shouldDoMoreStuff() { + // + } + } + """ + )); + } + + @Test void migrationPreservesOtherAnnotations() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + @Test + @Deprecated + class BazTest { + + public void shouldDoStuff() { + // + } + } + """, + """ + import org.junit.jupiter.api.Test; + + @Deprecated + class BazTest { + + @Test + public void shouldDoStuff() { + // + } + } + """ + )); + } + + @Test void doesNotOverwriteMethodLevelTestAnnotations() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + @Test + class BazTest { + + public void shouldDoStuff() { } + + @Test(enabled = false) + public void shouldDoMoreStuff() { } + } + """, + """ + import org.junit.jupiter.api.Disabled; + import org.junit.jupiter.api.Test; + + class BazTest { + + @Test + public void shouldDoStuff() { } + + @Test + @Disabled + public void shouldDoMoreStuff() { } + } + """ + )); + } + + /** + * Inner class methods are executed by the TestNG runner only when they are explicitly annotated with @Test, + * class-level annotation will not make them execute. + */ + @Test void doesNotAnnotateInnerClassMethods() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + @Test + public class BazTest { + public void test() { } + + public static class Inner { + public void noTest() { } + } + } + """, + """ + import org.junit.jupiter.api.Test; + + public class BazTest { + @Test + public void test() { } + + public static class Inner { + public void noTest() { } + } + } + """ + )); + } + + @Test void isRemoved_WhenOnlyInnerClassMethods() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + @Test + public class BazTest { + public static class Inner { + public void noTest() { } + } + } + """, + """ + public class BazTest { + public static class Inner { + public void noTest() { } + } + } + """ + )); + } + + @Test void noChangeOnOtherAnnotations() { + // language=java + rewriteRun(java( + """ + @Deprecated + public class BazTest { + } + """ + )); + } + } + + @Test void isMigratedToJunitTestAnnotationWithoutParameters() { + // language=java + rewriteRun(java( + """ + package de.foo.bar; + + import org.testng.annotations.Test; + + public class BazTest { + + @Test + public void shouldDoStuff() { + // + } + } + """, + """ + package de.foo.bar; + + import org.junit.jupiter.api.Test; + + public class BazTest { + + @Test + public void shouldDoStuff() { + // + } + } + """ + )); + } + + @Test void isMigratedPreservingOtherAnnotationsAndComments() { + // language=java + rewriteRun(java( + """ + package org.openrewrite; + public @interface Issue { + String value(); + } + """ + ), java( + """ + import org.testng.annotations.Test; + import org.openrewrite.Issue; + + public class MyTest { + + // some comments + @Issue("some issue") + @Test + public void test() { + } + + // some more comments + @Test + public void test1() { + } + + @Test + // even more comments + public void test2() { + } + } + """, + """ + import org.junit.jupiter.api.Test; + import org.openrewrite.Issue; + + public class MyTest { + + // some comments + @Issue("some issue") + @Test + public void test() { + } + + // some more comments + @Test + public void test1() { + } + + @Test + // even more comments + public void test2() { + } + } + """ + )); + } + + @Test void isMigratedWhenReferencedAsVariable() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + public class MyTest { + Object o = Test.class; + } + """, + """ + import org.junit.jupiter.api.Test; + + public class MyTest { + Object o = Test.class; + } + """ + )); + } + + @Test void isMigrated_whenUsedInJavadoc() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + /** @see org.testng.annotations.Test */ + public class MyTest { + @Test + public void test() { + } + } + """, + """ + import org.junit.jupiter.api.Test; + + /** @see org.junit.jupiter.api.Test */ + public class MyTest { + @Test + public void test() { + } + } + """ + )); + } + + @Test void noTestAnnotationValues_sameLine_multipleImports() { + // language=java + rewriteRun(java( + """ + package de.foo.bar; + + import java.util.List; + import org.testng.annotations.Test; + import static org.assertj.core.api.Assertions.assertThat; + + public class Baz { + + @Test public void shouldDoStuff() { + // + } + } + """, + """ + package de.foo.bar; + + import org.junit.jupiter.api.Test; + + import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; + + public class Baz { + + @Test public void shouldDoStuff() { + // + } + } + """ + )); + } + + @Test void fullyQualified() { + // language=java + rewriteRun(java( + """ + package de.foo.bar; + + class Baz { + + @org.testng.annotations.Test + public void shouldDoStuff() { + // + } + } + """, + """ + package de.foo.bar; + + class Baz { + + @org.junit.jupiter.api.Test + public void shouldDoStuff() { + // + } + } + """ + )); + } + + @Test void fullyQualified_sameLineAsMethodDeclaration() { + // language=java + rewriteRun(java( + """ + package de.foo.bar; + + class Baz { + + @org.testng.annotations.Test public void shouldDoStuff() { + // + } + } + """, + """ + package de.foo.bar; + + class Baz { + + @org.junit.jupiter.api.Test public void shouldDoStuff() { + // + } + } + """ + )); + } + + @Test void mixedFullyQualifiedAndNot() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + public class MyTest { + @org.testng.annotations.Test + public void feature1() { + } + + @Test + public void feature2() { + } + } + """, + """ + import org.junit.jupiter.api.Test; + + public class MyTest { + @org.junit.jupiter.api.Test + public void feature1() { + } + + @Test + public void feature2() { + } + } + """ + )); + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @Test void nestedClass() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + class Baz { + + @Test public void shouldDoStuff() { + // + } + + public class NestedGroupedTests { + + @Test public void shouldDoStuff() { + // + } + } + } + """, + """ + import org.junit.jupiter.api.Test; + + class Baz { + + @Test public void shouldDoStuff() { + // + } + + public class NestedGroupedTests { + + @Test public void shouldDoStuff() { + // + } + } + } + """ + )); + } + + @Test void noChangeNecessary() { + // language=java + rewriteRun(java( + """ + package de.foo.bar; + + import org.junit.jupiter.api.Test; + + class Baz { + + @Test public void shouldDoStuff() { + // + } + } + """ + )); + } + + @Test void noChangeNecessary_fullyQualified() { + // language=java + rewriteRun(java( + """ + package de.foo.bar; + + class Baz { + + @org.junit.jupiter.api.Test public void shouldDoStuff() { + // + } + } + """ + )); + } + + @Test void noChangeNecessary_nestedClass() { + // language=java + rewriteRun(java( + """ + package de.foo.bar; + + import org.junit.jupiter.api.Nested; + import org.junit.jupiter.api.Test; + + class Baz { + + @Test public void shouldDoStuff() { + // + } + + @Nested class NestedGroupedTests { + + @Test public void shouldDoStuff() { + // + } + } + } + """ + )); + } + } + + @Nested class Attribute_description { + + @Test void isMigratedToDisplayNameAnnotation_whenNotEmpty() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(description = "A test that tests something") + public void test() { + // some content + } + } + """, + """ + import org.junit.jupiter.api.DisplayName; + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + @DisplayName("A test that tests something") + public void test() { + // some content + } + } + """ + )); + } + + @SuppressWarnings("DefaultAnnotationParam") + @Test void isIgnored_whenEmpty() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(description = "") + public void test() { + // some content + } + } + """, + """ + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + public void test() { + // some content + } + } + """ + )); + } + } + + @Nested class Attribute_enabled { + + @Test void isMigratedToDisabledAnnotation_whenFalse() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(enabled = false) + public void test() { + // some content + } + } + """, + """ + import org.junit.jupiter.api.Disabled; + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + @Disabled + public void test() { + // some content + } + } + """ + )); + } + } + + @Nested class Attribute_expectedExceptions { + + @Test void isMigratedToBodyWrappedInAssertThrows_forLiteralJavaExceptionThrown() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(expectedExceptions = IllegalArgumentException.class) + public void test() { + throw new IllegalArgumentException("boom"); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + public void test() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + throw new IllegalArgumentException("boom"); + }); + } + } + """ + )); + } + + @Test void Attribute_expectedExceptionsMessageRegExp() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "boom.*!") + public void test() { + throw new IllegalArgumentException("boom !"); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + public void test() { + final Throwable thrown = Assertions.assertThrows(IllegalArgumentException.class, () -> { + throw new IllegalArgumentException("boom !"); + }); + Assertions.assertTrue(thrown.getMessage().matches("boom.*!")); + } + } + """ + )); + } + + @Test void isMigratedToBodyWrappedInAssertThrows_forLiteralCustomExceptionThrown() { + // language=java + rewriteRun(java( + """ + package com.abc; + public class MyException extends Exception { + public MyException(String message) { + super(message); + } + } + """ + ), java( + """ + import com.abc.MyException; + import org.testng.annotations.Test; + + public class MyTest { + + @Test(expectedExceptions = MyException.class) + public void test() { + throw new MyException("my exception"); + } + } + """, + """ + import com.abc.MyException; + import org.junit.jupiter.api.Assertions; + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + public void test() { + Assertions.assertThrows(MyException.class, () -> { + throw new MyException("my exception"); + }); + } + } + """ + )); + } + + @SuppressWarnings("ConstantConditions") + @Test void isMigratedToBodyWrappedInAssertThrows_forSingleStatement() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(expectedExceptions = IndexOutOfBoundsException.class) + public void test() { + int arr = new int[]{}[0]; + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + public void test() { + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> { + int arr = new int[]{}[0]; + }); + } + } + """ + )); + } + + @Test void isMigratedToBodyWrappedInAssertThrows() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(expectedExceptions = IllegalArgumentException.class) + public void test() { + String foo = "foo"; + throw new IllegalArgumentException("boom"); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + public void test() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + String foo = "foo"; + throw new IllegalArgumentException("boom"); + }); + } + } + """ + )); + } + + @Nested class Array { + + @Test void extractsFirstElementOfMultiple() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(expectedExceptions = { RuntimeException.class, IllegalAccessError.class, UnknownError.class } ) + public void test() { + throw new RuntimeException("Whooopsie!"); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + public void test() { + Assertions.assertThrows(RuntimeException.class, () -> { + throw new RuntimeException("Whooopsie!"); + }); + } + } + """ + ) + ); + } + @SuppressWarnings("DefaultAnnotationParam") + @Test void doesNotAddAssert_ifEmpty() { + // language=java + rewriteRun( + java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(expectedExceptions = { } ) + public void test() { + throw new RuntimeException("Not really caught nor tested"); + } + } + """, + """ + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + public void test() { + throw new RuntimeException("Not really caught nor tested"); + } + } + """ + ) + ); + } + } + } + + @Nested class Attribute_groups { + + @Test void isMigratedToSingleTagAnnotation_whenLiteralValue() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(groups = "Fast") + public void test() { + } + } + """, + """ + import org.junit.jupiter.api.Tag; + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + @Tag("Fast") + public void test() { + } + } + """ + )); + } + + @Test void isMigratedToSingleTagAnnotation_whenSingleArrayValue() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(groups = { "Fast" }) + public void test() { + } + } + """, + """ + import org.junit.jupiter.api.Tag; + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + @Tag("Fast") + public void test() { + } + } + """ + )); + } + + @Test void isMigratedToMultipleTagAnnotations_forEveryArrayValue() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(groups = { "Fast", "Integration", "Regression-1312" }) + public void test() { + } + } + """, + """ + import org.junit.jupiter.api.Tag; + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + @Tag("Fast") + @Tag("Integration") + @Tag("Regression-1312") + public void test() { + } + } + """ + )); + } + + @SuppressWarnings("DefaultAnnotationParam") + @Test void isIgnored_forEmptyArray() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(groups = { }) + public void test() { + } + } + """, + """ + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + public void test() { + } + } + """ + )); + } + + @Test void isIgnored_forEmptyString() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(groups = "") + public void test() { + } + } + """, + """ + import org.junit.jupiter.api.Test; + + public class MyTest { + + @Test + public void test() { + } + } + """ + )); + } + } + + @Nested class Attribute_timeOut { + + @Test void isMigratedToTimeoutAnnotation() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(timeOut = 500) + public void test() { + } + } + """, + """ + import org.junit.jupiter.api.Test; + import org.junit.jupiter.api.Timeout; + + import java.util.concurrent.TimeUnit; + + public class MyTest { + + @Test + @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) + public void test() { + } + } + """ + )); + } + + /** + * Unfortunately doesn't keep annotation on same line + * TODO investigate how this could be achieved + */ + @Test void isMigratedToTimeoutAnnotation_butNotPreservingSameLine() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(timeOut = 500) public void test() { + } + } + """, + """ + import org.junit.jupiter.api.Test; + import org.junit.jupiter.api.Timeout; + + import java.util.concurrent.TimeUnit; + + public class MyTest { + + @Test + @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) + public void test() { + } + } + """ + )); + } + } + + @Nested class MultipleAttributes { + + @Test void expectedExceptions_and_timeOut() { + // language=java + rewriteRun(java( + """ + import org.testng.annotations.Test; + + public class MyTest { + + @Test(expectedExceptions = IllegalArgumentException.class, timeOut = 500) + public void test() { + throw new IllegalArgumentException("boom"); + } + } + """, + """ + import org.junit.jupiter.api.Assertions; + import org.junit.jupiter.api.Test; + import org.junit.jupiter.api.Timeout; + + import java.util.concurrent.TimeUnit; + + public class MyTest { + + @Test + @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) + public void test() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + throw new IllegalArgumentException("boom"); + }); + } + } + """ + )); + } + } +} diff --git a/src/test/java/org/philzen/oss/utils/CleanupTest.java b/src/test/java/org/philzen/oss/utils/CleanupTest.java new file mode 100644 index 00000000..c3c559b8 --- /dev/null +++ b/src/test/java/org/philzen/oss/utils/CleanupTest.java @@ -0,0 +1,155 @@ +package org.philzen.oss.utils; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.openrewrite.ExecutionContext; +import org.openrewrite.internal.lang.NonNullApi; +import org.openrewrite.java.AnnotationMatcher; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +@NonNullApi +class CleanupTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(RewriteTest.toRecipe(() -> new JavaIsoVisitor<>() { + + static final String TYPE_TO_REMOVE = "java.lang.Deprecated"; + private static final AnnotationMatcher MATCHER = new AnnotationMatcher("@" + TYPE_TO_REMOVE); + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) { + if (classDecl.getLeadingAnnotations().stream().noneMatch(MATCHER::matches)) { + return classDecl; + } + + for (final J.Annotation annotation : classDecl.getLeadingAnnotations().stream().filter(MATCHER::matches).toList()) { + maybeRemoveImport(TYPE_TO_REMOVE); + classDecl = Cleanup.removeAnnotation(classDecl, annotation); + } + + return classDecl; + } + })); + } + + @Nested class removeAnnotation { + + @Test void onClassWithModifier() { + rewriteRun( + // language=java + java( + """ + @Deprecated + public class BazTest { + + } + """, + """ + public class BazTest { + + } + """ + ) + ); + } + + @Test void onClassWithoutModifier() { + rewriteRun( + // language=java + java( + """ + @Deprecated + class BazTest { + + } + """, + """ + class BazTest { + + } + """ + ) + ); + } + + /** + * This test is a bit convoluted as it will fail with "Recipe was expected to make a change but made no changes" + * when not also including removal of an import (only happens on typed classes without modifiers) + */ + @Test void onTypedClass() { + rewriteRun( + // language=java + java( + """ + import java.lang.Deprecated; + + @Deprecated + class BazTest { + + } + """, + """ + class BazTest { + + } + """ + ) + ); + } + + @Test void preservingExistingAnnotationsAfterIt() { + rewriteRun( + // language=java + java( + """ + import javax.annotation.concurrent.ThreadSafe; + + @Deprecated + @ThreadSafe + public class BazTest { + + } + """, + """ + import javax.annotation.concurrent.ThreadSafe; + + @ThreadSafe + public class BazTest { + + } + """ + ) + ); + } + + @Test void preservingExistingAnnotationsBeforeIt() { + rewriteRun( + // language=java + java( + """ + import javax.annotation.concurrent.ThreadSafe; + + @ThreadSafe @Deprecated + public class BazTest { + + } + """, + """ + import javax.annotation.concurrent.ThreadSafe; + + @ThreadSafe + public class BazTest { + + } + """ + ) + ); + } + } +}