Compare commits

...

132 Commits

Author SHA1 Message Date
Rick Ossendrijver
5f6d0f0299 Again some OpenJDK specific fixes 2024-07-16 22:05:19 +02:00
Rick Ossendrijver
81ac41cad2 Add support for minimalChanges flag 2024-07-16 20:41:52 +02:00
Rick Ossendrijver
0e785f895b First part of adding behaviorPreserving mode 2024-07-15 22:07:06 +02:00
Rick Ossendrijver
2f594a0773 Initial support for more conservative expectedExceptions and dataprovider migrations 2024-07-15 21:56:19 +02:00
Rick Ossendrijver
cecd21bc9f Try to fix the expected exceptions and dataproviders 2024-07-14 17:48:16 +02:00
Rick Ossendrijver
cfb0ea0363 Some more spaces on other annotations 2024-07-05 10:28:19 +02:00
Rick Ossendrijver
561adcd0d6 Add spaces when adding @Test annotation 2024-07-05 10:13:41 +02:00
Stephan Schroevers
05c5ba2677 Use compatible Maven 2024-07-03 14:25:20 +02:00
Stephan Schroevers
efb9ea91b7 Introduce Jitpack config 2024-07-03 14:14:54 +02:00
Rick Ossendrijver
36ed176c4c Drop incorrect add-exports 2024-07-02 15:42:46 +02:00
Rick Ossendrijver
aa21c57185 Exclude from analysis 2024-07-01 15:07:05 +02:00
Rick Ossendrijver
6e7f21f827 Revert "Apply EP best practices suggestions"
This reverts commit 37580fa5ff.
2024-07-01 14:37:44 +02:00
Rick Ossendrijver
37580fa5ff Apply EP best practices suggestions 2024-07-01 11:56:19 +02:00
Rick Ossendrijver
b26ec206a0 Add Refaster rules 2024-06-25 09:27:53 +02:00
Rick Ossendrijver
354d98dbb7 Apply EPS suggestions 2024-06-19 14:17:30 +02:00
Rick Ossendrijver
6b99639f0e Add specific improvement for OpenJDK 2024-06-19 12:41:55 +02:00
Rick Ossendrijver
a5d4a3e87f Fix Error Prone warnings and pom violation 2024-06-19 11:53:28 +02:00
Rick Ossendrijver
c19a30cdb8 Bump version in testng-junit-migrator 2024-06-19 11:53:28 +02:00
Gijs de Jong
b61dc4e151 Add @DataProvider edge cases 2024-06-19 11:53:27 +02:00
Gijs de Jong
27a248def0 Kill mutants 2024-06-19 11:53:27 +02:00
Gijs de Jong
0e72a531e0 Add test case for conservativeMode 2024-06-19 11:53:26 +02:00
Gijs de Jong
8842b11ad3 Simplify AttributeMigrator 2024-06-19 11:53:26 +02:00
Gijs de Jong
39550227a7 kill mutant 2024-06-19 11:53:25 +02:00
Rick Ossendrijver
867985819a Move VisitorState to be the last parameter in signature 2024-06-19 11:53:25 +02:00
Gijs de Jong
0a6c880e9c fix build 2024-06-19 11:53:24 +02:00
Gijs de Jong
ae4a196fd6 Clean up TestNGScanner 2024-06-19 11:53:24 +02:00
Gijs de Jong
b5ef39c89c Address review 2024-06-19 11:53:23 +02:00
Gijs de Jong
21b6fc7bd1 Implement UnsupportedAttributeMigrator as Migrator 2024-06-19 11:53:23 +02:00
Gijs de Jong
cec781b317 Prefer attribute for annotation attributes 2024-06-19 11:53:23 +02:00
Gijs de Jong
76778344f6 Introduce UnsupportedAttributeMigrator 2024-06-19 11:53:22 +02:00
Gijs de Jong
c663c0cb7a Add Known Limitations section 2024-06-19 11:53:22 +02:00
Gijs de Jong
0f7231a3ad Use @Builder immutable builders 2024-06-19 11:53:21 +02:00
Rick Ossendrijver
4affdc8519 Swap canFix and createFix and other improvements 2024-06-19 11:53:21 +02:00
Rick Ossendrijver
97d87cc9c0 Assorted improvements 2024-06-19 11:53:20 +02:00
Rick Ossendrijver
4c9ab70bb6 Fix pom version 2024-06-19 11:53:20 +02:00
Rick Ossendrijver
03232ad9b9 Post-post rebase fix 2024-06-19 11:53:19 +02:00
Rick Ossendrijver
4f48e05d8f Post-rebase fix 2024-06-19 11:53:19 +02:00
Gijs de Jong
57eb44285a Emphasize importance of checking migrated code 2024-06-19 11:53:18 +02:00
Dima Legeza
4dd39610e7 Replace missed sed commands with platform-independent $sed_command with additional $grep_command usage alignment 2024-06-19 11:53:18 +02:00
Gijs de Jong
643fd4e8a5 Update package-info.java 2024-06-19 11:53:18 +02:00
Gijs de Jong
67c57c2442 clean up some code smells 2024-06-19 11:53:17 +02:00
Gijs de Jong
2ec3938737 0.10.1 2024-06-19 11:53:17 +02:00
Gijs de Jong
486ee7353f (hopefully) fix SonarCloud warnings 2024-06-19 11:53:16 +02:00
Gijs de Jong
7cbe859b8c Specify explicit immutability 2024-06-19 11:53:16 +02:00
Gijs de Jong
3cfcdda0fc Clarify setup/teardown methods 2024-06-19 11:53:15 +02:00
Gijs de Jong
952759209d fmt 2024-06-19 11:53:15 +02:00
Gijs de Jong
9ad5f6e37c Kill mutants in DataProvider 2024-06-19 11:53:14 +02:00
Gijs de Jong
ebb6ff46b4 Add support for the timeOut attribute 2024-06-19 11:53:14 +02:00
Gijs de Jong
a305bf6da0 macOs 2024-06-19 11:53:13 +02:00
Rick Ossendrijver
1d3cc10ff0 Suggestions 2024-06-19 11:53:13 +02:00
Rick Ossendrijver
8d93549935 Rename "argument" with "attribute" and minor improvements 2024-06-19 11:53:12 +02:00
Gijs de Jong
7096ce6075 Use /bin/bash 2024-06-19 11:53:12 +02:00
Gijs de Jong
9652c6f990 Add macOs support to migrator script 2024-06-19 11:53:12 +02:00
Gijs de Jong
f02d9adcb2 Improve macos specific instructions 2024-06-19 11:53:11 +02:00
Gijs de Jong
9dda67e6cc Clarify gnu-{grep, sed} requirements 2024-06-19 11:53:11 +02:00
Gijs de Jong
51fdf18577 Clarify macos requirements picnic script 2024-06-19 11:53:10 +02:00
Gijs de Jong
e3253011c3 Continue migration if JUnit dependency is present 2024-06-19 11:53:10 +02:00
Gijs de Jong
d1836383ab Version 0.9.1-SNAPSHOT 2024-06-19 11:53:09 +02:00
Gijs de Jong
882b256c18 Handle {Before, After}Test annotations 2024-06-19 11:53:09 +02:00
Gijs de Jong
36e0765734 Add --count flag instructions 2024-06-19 11:53:08 +02:00
Gijs de Jong
d46a1ab87e Add support for non parent modukes 2024-06-19 11:53:08 +02:00
Gijs de Jong
3be79074d8 Explain where to run script 2024-06-19 11:53:07 +02:00
Gijs de Jong
d8991627a7 Improve installion steps 2024-06-19 11:53:07 +02:00
Rick Ossendrijver
1344321746 Further improve README 2024-06-19 11:53:07 +02:00
Rick Ossendrijver
559a55b7d0 Tweak README 2024-06-19 11:53:06 +02:00
Gijs de Jong
2e28ac9828 Improve metadata builder api 2024-06-19 11:53:06 +02:00
Gijs de Jong
4d7b88a986 Mention picnic specific migration script 2024-06-19 11:53:05 +02:00
Gijs de Jong
c0e177e4cd eps suggestions 2024-06-19 11:53:05 +02:00
Gijs de Jong
768b05bee6 More DataProvider test cases 2024-06-19 11:53:04 +02:00
Rick Ossendrijver
048203434e Some more tweaks 2024-06-19 11:53:04 +02:00
Rick Ossendrijver
9b89c0863c Update and improve TestNGScanner{,Test} 2024-06-19 11:53:03 +02:00
Rick Ossendrijver
d03d536376 Suggestions and delete SourceCodeTest 2024-06-19 11:53:03 +02:00
Gijs de Jong
c812c95ea9 Add tests for TestNGMatchers 2024-06-19 11:53:02 +02:00
Gijs de Jong
0d23dd1845 Make setup/teardown methods static if needed 2024-06-19 11:53:02 +02:00
Gijs de Jong
e29c08604d Copy over SourceCode test 2024-06-19 11:53:01 +02:00
Gijs de Jong
22c1f07017 Copy SourceCode from contrib 2024-06-19 11:53:01 +02:00
Gijs de Jong
9dd7621c18 Add tests for Tag name specifications 2024-06-19 11:53:00 +02:00
Gijs de Jong
3c59795c1b Adhere to JUnit Tag requirements 2024-06-19 11:53:00 +02:00
Gijs de Jong
2688088750 Introduce GroupsArgumentMigrator 2024-06-19 11:53:00 +02:00
Gijs de Jong
a7b3378bda fmt 2024-06-19 11:52:59 +02:00
Gijs de Jong
d7a10eda13 Handle empty list of expectedExceptions 2024-06-19 11:52:59 +02:00
Gijs de Jong
78e833ff85 Return empty fix for enabled test 2024-06-19 11:52:58 +02:00
Gijs de Jong
edf2b7ca79 Prevent non-test methods from being flagged as such 2024-06-19 11:52:58 +02:00
Gijs de Jong
9ab79cb69b Introduce EnabledArgumentMigrator 2024-06-19 11:52:57 +02:00
Gijs de Jong
ac78cc709d Add support for setup/teardown methods 2024-06-19 11:52:57 +02:00
Rick Ossendrijver
2ef223ec9d Suggestions and try to make the build green 2024-06-19 11:52:56 +02:00
Gijs de Jong
2f797a322a Flatten package structure 2024-06-19 11:52:56 +02:00
Gijs de Jong
3537e58305 suggestion
Co-authored-by: Rick Ossendrijver <rick.ossendrijver@gmail.com>
2024-06-19 11:52:55 +02:00
Gijs de Jong
f5e816d967 Fix build 2024-06-19 11:52:55 +02:00
Gijs de Jong
c0acb8b202 Assume @DataProvider has a return tree 2024-06-19 11:52:55 +02:00
Gijs de Jong
8f2183b6c2 Improve readme 2024-06-19 11:52:54 +02:00
Gijs de Jong
278b679049 Improve migration script 2024-06-19 11:52:54 +02:00
Gijs de Jong
29657997f3 suggestions 2024-06-19 11:52:53 +02:00
Gijs de Jong
3562e4c764 Update README.md 2024-06-19 11:52:53 +02:00
Gijs de Jong
d1b9f93cad Update run-testng-junit-migration.sh 2024-06-19 11:52:52 +02:00
Gijs de Jong
fc177a9355 Initial migration script 2024-06-19 11:52:52 +02:00
Gijs de Jong
9ef2692885 format pom 2024-06-19 11:52:51 +02:00
Gijs de Jong
35ada6b449 remove old check 2024-06-19 11:52:51 +02:00
Gijs de Jong
1a98a4d599 Add test case for multiple expected exceptions 2024-06-19 11:52:50 +02:00
Gijs de Jong
e5d7482133 Fix tests 2024-06-19 11:52:50 +02:00
Gijs de Jong
69e987363a Flatten migrator hierarchy 2024-06-19 11:52:49 +02:00
Gijs de Jong
dad73a8447 suggestions 2024-06-19 11:52:49 +02:00
Gijs de Jong
f22b344d87 Create testngjunitmigrator module 2024-06-19 11:52:49 +02:00
Gijs de Jong
8d78b5b316 Remove unused TestNGMigrationContext 2024-06-19 11:52:48 +02:00
Gijs de Jong
6024caf99c add tests for TestNGScanner 2024-06-19 11:52:48 +02:00
Gijs de Jong
ff60c7be25 use jspecify Nullable 2024-06-19 11:52:47 +02:00
Gijs de Jong
891e3dde5f eps suggestions 2024-06-19 11:52:47 +02:00
Gijs de Jong
ee4d3a70fa fmt 2024-06-19 11:52:46 +02:00
Gijs de Jong
81c0e300a1 Fix tests + behaviour 2024-06-19 11:52:46 +02:00
Gijs de Jong
79abb132f5 Refactor TestNGMetaData to be immutable 2024-06-19 11:52:45 +02:00
Gijs de Jong
d94518cbed begin 2024-06-19 11:52:45 +02:00
Gijs de Jong
6f862401ab Argument migrator 2024-06-19 11:52:44 +02:00
Gijs de Jong
aeae1dd66e feedback 2024-06-19 11:52:44 +02:00
Rick Ossendrijver
8a032bea18 Suggestions and add XXXs 2024-06-19 11:52:43 +02:00
Gijs de Jong
2c3ca2f885 Implement aggressive migration mode + tests 2024-06-19 11:52:43 +02:00
Gijs de Jong
d4a22682cd Implement aggressiveMigration mode 2024-06-19 11:52:43 +02:00
Gijs de Jong
4c03041a47 Remove old code + improve tests 2024-06-19 11:52:42 +02:00
Gijs de Jong
c96cb95ec3 add javadoc 2024-06-19 11:52:42 +02:00
Gijs de Jong
91923ef168 suggestions 2024-06-19 11:52:41 +02:00
Gijs de Jong
b086db14d9 eps suggestions 2024-06-19 11:52:41 +02:00
Gijs de Jong
369566f6f1 Add more test cases 2024-06-19 11:52:40 +02:00
Rick Ossendrijver
555bf36a7a Add XXXs 2024-06-19 11:52:40 +02:00
Rick Ossendrijver
81b27dab08 Tweaks 2024-06-19 11:52:39 +02:00
Gijs de Jong
ecdd2c0f61 Add support for expectedExceptions argument 2024-06-19 11:52:39 +02:00
Gijs de Jong
ef9dd72364 Add/remove imports 2024-06-19 11:52:38 +02:00
Gijs de Jong
f2031b8308 Restructure migration 2024-06-19 11:52:38 +02:00
Gijs de Jong
6995f5627a Add support for migrating groups attribute 2024-06-19 11:52:38 +02:00
Gijs de Jong
2ad8deb6af Remove redundant naming 2024-06-19 11:52:37 +02:00
Gijs de Jong
8f0eea6e44 Improve AnnotationAttributeReplacement 2024-06-19 11:52:37 +02:00
Gijs de Jong
8859417f42 Handle test setup and teardown migration 2024-06-19 11:52:36 +02:00
Gijs de Jong
cc35110ff5 Ignore extends clause in TestNGClassLevelTestAnnotation 2024-06-19 11:52:36 +02:00
Gijs de Jong
eed6f39b10 Introduce BugCheckers TestNG -> JUnit migration
Fix `TestNGDataProviderCheckTest`

Suggestions

Suggested changes

Introduce `BugChecker`s TestNG -> JUnit migration

Fix `TestNGDataProviderCheckTest`

Suggestions

Suggested changes

Apply fixes for new bugchecks

Rename checks for `BugPatternNaming`

Only match migratable tests

Update javadoc with *legal* characters

Remove static method in inner class

Requested changes

Introduce support for 1d array dataprovider

Retain comments in data provider return tree

Self-apply EP checks

Swapped around a few methods and fix mutable issue

Suggested changes

Suggestions

Suggestions

Suggestions 2

Prefer `orElseThrow()` over `.get()`
2024-06-19 11:52:35 +02:00
29 changed files with 2845 additions and 1 deletions

View File

@@ -162,7 +162,7 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>

7
jitpack.yml Normal file
View File

@@ -0,0 +1,7 @@
before_install:
- source "${HOME}/.sdkman/bin/sdkman-init.sh"
- sdk update
- sdk install java 17.0.10-tem
- sdk use java 17.0.10-tem
- sdk install maven 3.9.8
- sdk use maven 3.9.8

View File

@@ -48,6 +48,7 @@
<module>refaster-runner</module>
<module>refaster-support</module>
<module>refaster-test-support</module>
<module>testng-junit-migrator</module>
</modules>
<scm child.scm.developerConnection.inherit.append.path="false" child.scm.url.inherit.append.path="false">

View File

@@ -0,0 +1,147 @@
# TestNG to JUnit Jupiter migrator
This module contains a tool to automatically migrate TestNG tests to JUnit
Jupiter. The tool is built on top of [Error Prone][error-prone-orig-repo]. To
use it, read the installation guide below.
### Installation
1. First, follow Error Prone's [installation
guide][error-prone-installation-guide]. For extra information, see this
[README][eps-readme]. (This step can be skipped for Picnic repositories!)
2. Clone the Error Prone Support repository and checkout the branch
`gdejong/testng-migrator`.
3. Next, run `mvn versions:set -DnewVersion=0.17.1-testng-migration -DgenerateBackupPoms=false`.
This will update set the version to `0.17.1-testng-migration`.
4. Next, run `mvn clean install`. This will create a `0.17.1-testng-migrator` version
of the `testng-junit-migrator` module. The version will now be available in your local Maven repository.
5. Finally, add the following profile to your `pom.xml`. This should be the `pom.xml` in the root of your module.
Usually this is the parent `pom.xml`, but single module projects are also supported.
```xml
<profiles>
<profile>
<id>testng-migrator</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths combine.children="append">
<path>
<groupId>tech.picnic.error-prone-support</groupId>
<artifactId>testng-junit-migrator</artifactId>
<version>0.10.1-testng-migration</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
```
Having this profile allows the migration script to verify the correctness of
the result by making sure the same amount of tests are executed.
## Run the migration
> **Note**
> For Picnic repositories there is an extra step required _before_ running the
> migration, see [here](#picnic-specific).
Now that the migration is set up, one can start the migration by executing the
[run-testng-junit-migrator.sh][migration-script] script in the same directory as the `pom.xml` file we changed earlier.
This script will:
1. Add the required `JUnit` dependencies to your `pom.xml`.
2. Run the `testng-to-junit` migration.
> **Note**
> Please verify that the migrated code still compiles after each step of the compilation.
### Counting tests
The amount of tests executed before the migration can be counted using the `--count` flag:
```sh
./run-testng-junit-migrator.sh --count
```
This will count the amount of tests that are executed. This is recommended before running the migration
to allow for comparison.
### Picnic specific
The `PicnicSupermarket/picnic-scratch` repository contains a helper script
`java-platform/testng-junit-migration.sh` that migrates some more
Picnic-specific code. This should be executed _before_ starting the actual
migration.
> **Warning**
> This is a warning for `macOs` users.
> Make sure gnu-grep and gnu-sed are installed!
>
> ```brew install grep gnu-sed```
Continue with performing the actual migration [here](#run-the-migration).
Afterward, run the `./picnic-shared-tools/patch.sh` script.
Now you are done! 🤘🚀
### Migration code example
Consider the following TestNG test class:
```java
// TestNG code:
@Test
public class A {
public void simpleTest() {}
@Test(priority = 2)
public void priorityTest() {}
@DataProvider
private static Object[][] dataProviderTestCases() {
return new Object[]{{1}, {2}, {3}};
}
@Test(dataProvider = "dataProviderTestCases")
public void dataProviderTest(int number) {}
}
```
This migration tool will turn this into the following:
```java
// JUnit Jupiter code:
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class A {
@Test
void simpleTest() {}
@Test
@Order(2)
public void priorityTest() {}
private static Stream<Argument> dataProviderTestCases() {
return Stream.of(arguments(1), arguments(2), arguments(3));
}
@ParameterizedTest
@MethodSource("dataProviderTestCases")
public void dataProviderTest(int number) {}
}
```
### Known limitations
- Certain `@DataProvider` methods cannot be automatically migrated (e.g., `return Stream.of(...).toArray(Object[][]::new)`).
- Some uncommon `@Test` attributes are not supported, such as `ignoreMissingDependencies` and `dependsOnMethods`.
- Test setup and teardown methods `@{Before, After}Test` are migrated to `@{Before, After}Each` to avoid introducing breaking changes. `@{Before, After}All` require a static method, while `@{Before, After}Test` are instance methods.
[eps-readme]: ../README.md
[error-prone-installation-guide]: https://errorprone.info/docs/installation#maven
[error-prone-orig-repo]: https://github.com/google/error-prone
[migration-script]: run-testng-junit-migration.sh

View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tech.picnic.error-prone-support</groupId>
<artifactId>error-prone-support</artifactId>
<version>0.16.2-SNAPSHOT</version>
</parent>
<artifactId>testng-junit-migrator</artifactId>
<name>Picnic :: Error Prone Support :: TestNG JUnit Migrator</name>
<description>A tool to migrate TestNG tests to JUnit</description>
<url>https://error-prone.picnic.tech</url>
<dependencies>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_annotation</artifactId>
</dependency>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_check_api</artifactId>
</dependency>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_test_helpers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>refaster-compiler</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>refaster-support</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>refaster-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.auto</groupId>
<artifactId>auto-common</artifactId>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-java</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-java-11</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-templating</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths combine.children="append">
<path>
<groupId>${project.groupId}</groupId>
<artifactId>refaster-compiler</artifactId>
<version>${project.version}</version>
</path>
<path>
<groupId>${project.groupId}</groupId>
<artifactId>refaster-support</artifactId>
<version>${project.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs combine.children="append">
<arg>-Xplugin:RefasterRuleCompiler</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@@ -0,0 +1,142 @@
#!/bin/bash
set -e -u -o pipefail
# If this is not a Maven build, exit here to skip Maven-specific steps.
if [ ! -f pom.xml ]; then
echo "Not a Maven build, exiting."
exit 0
fi
function insert_dependency() {
groupId=${1:?groupId not specified or empty}
artifactId=${2:?artifactId not specified or empty}
classifier=${3?classifier not specified}
scope=${4?scope not specified}
pomFile=${5:-pom.xml}
# If the dependency declaration is already present (irrespective of scope),
# then we don't modify the file.
xmlstarlet sel -T -N 'x=http://maven.apache.org/POM/4.0.0' \
-t -m "/x:project/x:dependencies/x:dependency[
x:groupId/text() = '${groupId}' and
x:artifactId/text() = '${artifactId}' and
(x:classifier/text() = '${classifier}' or '${classifier}' = '')
]" -nl "${pomFile}" && return 0
# Determine the index at which to insert the dependency declaration.
insertionIndex="$(
(xmlstarlet sel -T -N 'x=http://maven.apache.org/POM/4.0.0' \
-t -m '/x:project/x:dependencies/x:dependency' \
-v 'concat(x:groupId, " : ", x:artifactId, " : ", x:classifier, " : ", x:scope)' -nl \
"${pomFile}" || true) |
awk "\$0 < \"${groupId} : ${artifactId} : ${classifier} : ${scope}\"" |
wc -l
)"
# Generate a placeholder that will be inserted at the place where the new
# dependency declaration should reside. We need to jump through this hoop
# because `xmlstarlet` does not support insertion of complex XML
# sub-documents.
placeholder="$(head -c 30 /dev/urandom | base64 | $sed_command 's,[^a-zA-Z0-9],,g')"
# Insert the placeholder. (Note that only one case will match.)
xmlstarlet ed -L -P -N 'x=http://maven.apache.org/POM/4.0.0' \
-s "/x:project[not(x:dependencies)]" \
-t elem -n dependencies -v "${placeholder}" \
-i "/x:project/x:dependencies/x:dependency[${insertionIndex} = 0 and position() = 1]" \
-t text -n placeholder -v "${placeholder}" \
-a "/x:project/x:dependencies/x:dependency[${insertionIndex}]" \
-t text -n placeholder -v "${placeholder}" \
"${pomFile}"
# Generate the XML subdocument we _actually_ want to insert.
decl="$(echo "
<dependency>
<groupId>${groupId}</groupId>
<artifactId>${artifactId}</artifactId>
<classifier>${classifier}</classifier>
<scope>${scope}</scope>
</dependency>" |
$sed_command '/></d' |
$sed_command ':a;N;$!ba;s/\n/\\n/g')"
# Replace the placeholder with the actual dependency declaration.
$sed_command -i "s,${placeholder},${decl}," "${pomFile}"
}
if [[ -n "${1-}" ]] && [[ "${1}" == "--count" ]]; then
echo "Counting number of tests..."
test_results=$(mvn test | $grep_command -n "Results:" -A 3 | $grep_command -oP "Tests run: \K\d+(?=,)" | awk '{s+=$1} END {print s}')
echo "Number of tests run: $test_results"
exit
fi
function handle_file() {
module=${1:?module not specified or empty}
groupId=${2:?groupId not specified or empty}
artifactId=${3:?artifactId not specified or empty}
classifier=${4?classifier not specified}
scope=${5?scope not specified}
pomFile=${6:-pom.xml}
if [[ -d $module ]] && [[ -f "$module/pom.xml" ]]; then
cd "$module"
insert_dependency "$groupId" "$artifactId" "$classifier" "$scope"
echo "[$module] Added $groupId:$artifactId"
cd -
fi
}
case "$(uname -s)" in
Linux*)
grep_command="grep"
sed_command="sed"
;;
Darwin*)
grep_command="ggrep"
sed_command="gsed"
;;
*)
echo "Unsupported distribution $(uname -s) for this script."
exit 1
;;
esac
echo "Migrating to JUnit 5..."
echo "Adding required dependencies..."
if $grep_command -q "<packaging>pom</packaging>" "pom.xml"; then
for module in $($grep_command -rl "org.testng.annotations.Test" $(pwd) | awk -F "$(pwd)" '{print $2}' | awk -F '/' '{print $2}' | uniq); do
(
handle_file "$module" "org.junit.jupiter" "junit-jupiter-api" "" "test"
)
(
handle_file "$module" "org.junit.jupiter" "junit-jupiter-engine" "" "test"
)
done
for module in $($grep_command -rl "org.testng.annotations.DataProvider" $(pwd) | awk -F "$(pwd)" '{print $2}' | awk -F '/' '{print $2}' | uniq); do
(
handle_file "$module" "org.junit.jupiter" "junit-jupiter-params" "" "test"
)
done
else
if $grep_command -rq "org.testng.annotations.Test" "src/"; then
handle_file "./" "org.junit.jupiter" "junit-jupiter-api" "" "test"
handle_file "./" "org.junit.jupiter" "junit-jupiter-engine" "" "test"
fi
if $grep_command -rq "org.testng.annotations.DataProvider" "src/"; then
handle_file "./" "org.junit.jupiter" "junit-jupiter-params" "" "test"
fi
fi
echo "Running migration..."
mvn \
-Perror-prone \
-Ptestng-migrator \
-Ppatch \
clean test-compile fmt:format \
-Derror-prone.patch-checks="TestNGJUnitMigration" \
-Dverification.skip
echo "Finished executing migration!"

View File

@@ -0,0 +1,32 @@
package tech.picnic.errorprone.testngjunit;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.fixes.SuggestedFix;
import com.sun.source.tree.MethodTree;
import java.util.Optional;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.AnnotationMetadata;
/**
* Interface implemented by classes that define how to migrate a specific attribute from a TestNG
* {@code Test} annotation to JUnit.
*/
@Immutable
interface AttributeMigrator {
/**
* Attempts to create a {@link SuggestedFix}.
*
* @param methodTree The method tree the annotation is on.
* @param minimalChangesMode Whether the migration should introduce the minimal changes required
* to migrate.
* @param state The visitor state.
* @return an {@link Optional} containing the created fix. This returns an {@link
* Optional#empty()} if the {@link AttributeMigrator} is not able to migrate the attribute.
*/
Optional<SuggestedFix> migrate(
TestNgMetadata metadata,
AnnotationMetadata annotation,
MethodTree methodTree,
boolean minimalChangesMode,
VisitorState state);
}

View File

@@ -0,0 +1,43 @@
package tech.picnic.errorprone.testngjunit;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import java.util.Optional;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.AnnotationMetadata;
/**
* A {@link AttributeMigrator} that migrates the {@code org.testng.annotations.Test#dataProvider}
* attributes.
*/
@Immutable
final class DataProviderAttributeMigrator implements AttributeMigrator {
@Override
public Optional<SuggestedFix> migrate(
TestNgMetadata metadata,
AnnotationMetadata annotation,
MethodTree methodTree,
boolean minimalChangesMode,
VisitorState state) {
ExpressionTree dataProviderNameExpressionTree = annotation.getAttributes().get("dataProvider");
if (dataProviderNameExpressionTree == null) {
return Optional.empty();
}
String dataProviderName = ASTHelpers.constValue(dataProviderNameExpressionTree, String.class);
if (!metadata.getDataProviderMetadata().containsKey(dataProviderName)) {
return Optional.empty();
}
return Optional.of(
SuggestedFix.builder()
.addImport("org.junit.jupiter.params.ParameterizedTest")
.addImport("org.junit.jupiter.params.provider.MethodSource")
.prefixWith(methodTree, "@ParameterizedTest\n")
.prefixWith(methodTree, String.format(" @MethodSource(\"%s\")", dataProviderName))
.build());
}
}

View File

@@ -0,0 +1,185 @@
package tech.picnic.errorprone.testngjunit;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.sun.source.tree.Tree.Kind.NEW_ARRAY;
import static java.util.stream.Collectors.joining;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.VisitorState;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.util.ASTHelpers;
import com.google.errorprone.util.ErrorProneToken;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.ReturnTree;
import com.sun.tools.javac.parser.Tokens.Comment;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import tech.picnic.errorprone.util.SourceCode;
// XXX: Can this one also implement a `Migrator`?
/** A helper class that migrates a TestNG {@code DataProvider} to a JUnit {@code MethodSource}. */
final class DataProviderMigrator {
/** This regular expression replaces matches instances of `this.getClass()` and `getClass()`. */
private static final Pattern GET_CLASS =
Pattern.compile("((?<!\\b\\.)|(\\bthis\\.))(getClass\\(\\))");
private DataProviderMigrator() {}
/**
* Tells whether the specified {@code DataProvider} can be migrated.
*
* @param methodTree The dataprovider method tree.
* @return {@code true} if the data provider can be migrated or else {@code false}.
*/
static boolean canFix(MethodTree methodTree) {
// XXX: Make it configurable that we can migrate in `minimalChanges` mode.
return true; // getDataProviderReturnTree(getReturnTree(methodTree)).isPresent();
}
/**
* Create the {@link SuggestedFix} required to migrate a TestNG {@code DataProvider} to a JUnit
* {@code MethodSource}.
*
* @param classTree The class containing the data provider.
* @param methodTree The data provider method.
* @param state The {@link VisitorState}.
* @return An {@link Optional} containing the created fix.
*/
static Optional<SuggestedFix> createFix(
ClassTree classTree, MethodTree methodTree, VisitorState state) {
return tryMigrateDataProvider(methodTree, classTree, state);
}
private static Optional<SuggestedFix> tryMigrateDataProvider(
MethodTree methodTree, ClassTree classTree, VisitorState state) {
ReturnTree returnTree = getReturnTree(methodTree);
return getDataProviderReturnTree(returnTree)
.map(
dataProviderReturnTree ->
SuggestedFix.builder()
.addStaticImport("org.junit.jupiter.params.provider.Arguments.arguments")
.addImport(Stream.class.getCanonicalName())
.addImport("org.junit.jupiter.params.provider.Arguments")
.delete(methodTree)
.postfixWith(
methodTree,
buildMethodSource(
classTree.getSimpleName().toString(),
methodTree.getName().toString(),
methodTree,
returnTree,
dataProviderReturnTree,
state))
.build());
}
private static ReturnTree getReturnTree(MethodTree methodTree) {
return methodTree.getBody().getStatements().stream()
.filter(ReturnTree.class::isInstance)
.findFirst()
.map(ReturnTree.class::cast)
.orElseThrow();
}
private static Optional<NewArrayTree> getDataProviderReturnTree(ReturnTree returnTree) {
if (returnTree.getExpression().getKind() != NEW_ARRAY
|| ((NewArrayTree) returnTree.getExpression()).getInitializers().isEmpty()) {
return Optional.empty();
}
return Optional.of((NewArrayTree) returnTree.getExpression());
}
private static String buildMethodSource(
String className,
String name,
MethodTree methodTree,
ReturnTree returnTree,
NewArrayTree newArrayTree,
VisitorState state) {
StringBuilder sourceBuilder =
new StringBuilder()
.append(" private static Stream<Arguments> ")
.append(name)
.append(" () ");
if (!methodTree.getThrows().isEmpty()) {
sourceBuilder
.append(" throws ")
.append(
methodTree.getThrows().stream()
.filter(IdentifierTree.class::isInstance)
.map(IdentifierTree.class::cast)
.map(identifierTree -> identifierTree.getName().toString())
.collect(joining(", ")));
}
return sourceBuilder
.append(" {\n")
.append(extractMethodBodyWithoutReturnStatement(methodTree, returnTree, state))
.append(" return ")
.append(buildArgumentStream(className, newArrayTree, state))
.append(";\n}")
.toString();
}
private static String extractMethodBodyWithoutReturnStatement(
MethodTree methodTree, ReturnTree returnTree, VisitorState state) {
String body = SourceCode.treeToString(methodTree.getBody(), state);
return body.substring(2, body.indexOf(SourceCode.treeToString(returnTree, state)) - 1);
}
private static String buildArgumentStream(
String className, NewArrayTree newArrayTree, VisitorState state) {
int startPos = ASTHelpers.getStartPosition(newArrayTree);
int endPos = state.getEndPosition(newArrayTree);
ImmutableMap<Integer, List<Comment>> comments =
state.getOffsetTokens(startPos, endPos).stream()
.collect(toImmutableMap(ErrorProneToken::pos, ErrorProneToken::comments));
StringBuilder argumentsBuilder = new StringBuilder();
argumentsBuilder.append(
newArrayTree.getInitializers().stream()
.map(
expression ->
wrapTestValueWithArguments(
expression,
comments.getOrDefault(
ASTHelpers.getStartPosition(expression), ImmutableList.of()),
state))
.collect(joining(",")));
/*
* This replaces all instances of `{,this.}getClass()` with the fully qualified class name to
* retain functionality in static context.
*/
return GET_CLASS
.matcher(String.format("Stream.of(%s%n)", argumentsBuilder))
.replaceAll(className + ".class");
}
/**
* Wraps a value in {@code org.junit.jupiter.params.provider#arguments()}.
*
* <p>Drops curly braces from array initialisation values.
*/
private static String wrapTestValueWithArguments(
ExpressionTree tree, List<Comment> comments, VisitorState state) {
String source = SourceCode.treeToString(tree, state);
String argumentValue =
tree.getKind() == NEW_ARRAY ? source.substring(1, source.length() - 1) : source;
return String.format(
"\t\t%s%n\t\targuments(%s)",
comments.stream().map(Comment::getText).collect(joining("\n")), argumentValue);
}
}

View File

@@ -0,0 +1,32 @@
package tech.picnic.errorprone.testngjunit;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.fixes.SuggestedFix;
import com.sun.source.tree.MethodTree;
import java.util.Optional;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.AnnotationMetadata;
import tech.picnic.errorprone.util.SourceCode;
/** A {@link AttributeMigrator} that migrates the {@code description} attribute. */
@Immutable
final class DescriptionAttributeMigrator implements AttributeMigrator {
@Override
public Optional<SuggestedFix> migrate(
TestNgMetadata metadata,
AnnotationMetadata annotation,
MethodTree methodTree,
boolean minimalChangesMode,
VisitorState state) {
return Optional.ofNullable(annotation.getAttributes().get("description"))
.map(
description ->
SuggestedFix.builder()
.addImport("org.junit.jupiter.api.DisplayName")
.prefixWith(
methodTree,
String.format(
"@DisplayName(%s)%n ", SourceCode.treeToString(description, state)))
.build());
}
}

View File

@@ -0,0 +1,32 @@
package tech.picnic.errorprone.testngjunit;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.fixes.SuggestedFix;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.MethodTree;
import java.util.Optional;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.AnnotationMetadata;
/** A {@link AttributeMigrator} that migrates the {@code enabled} attribute. */
@Immutable
final class EnabledAttributeMigrator implements AttributeMigrator {
@Override
public Optional<SuggestedFix> migrate(
TestNgMetadata metadata,
AnnotationMetadata annotation,
MethodTree methodTree,
boolean minimalChangesMode,
VisitorState state) {
return Optional.ofNullable(annotation.getAttributes().get("enabled"))
.map(enabled -> ((LiteralTree) enabled).getValue())
.filter(Boolean.FALSE::equals)
.map(
unused ->
SuggestedFix.builder()
.addImport("org.junit.jupiter.api.Disabled")
.prefixWith(methodTree, "@Disabled\n ")
.build())
.or(() -> Optional.of(SuggestedFix.emptyFix()));
}
}

View File

@@ -0,0 +1,121 @@
package tech.picnic.errorprone.testngjunit;
import static com.google.auto.common.MoreStreams.toImmutableList;
import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT;
import static com.sun.source.tree.Tree.Kind.NEW_ARRAY;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewArrayTree;
import java.util.Optional;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.AnnotationMetadata;
import tech.picnic.errorprone.util.SourceCode;
/** A {@link AttributeMigrator} that migrates the {@code expectedExceptions} attribute. */
@Immutable
final class ExpectedExceptionsAttributeMigrator implements AttributeMigrator {
@Override
public Optional<SuggestedFix> migrate(
TestNgMetadata metadata,
AnnotationMetadata annotation,
MethodTree methodTree,
boolean minimalChangesMode,
VisitorState state) {
if (minimalChangesMode) {
String methodName = methodTree.getName().toString();
AnnotationTree testAnnotation =
ASTHelpers.getAnnotationWithSimpleName(ASTHelpers.getAnnotations(methodTree), "Test");
SuggestedFix.Builder fix = SuggestedFix.builder().delete(testAnnotation);
Optional<String> exception =
Optional.ofNullable(annotation.getAttributes().get("expectedExceptions"))
.flatMap(expectedExceptions -> getExpectedException(expectedExceptions, state));
if (exception.isEmpty()) {
return Optional.empty();
}
String newMethod =
"""
@org.junit.jupiter.api.Test
void test%s() {
assertThrows(%s, () -> %s());
}
"""
.formatted(
Character.toUpperCase(methodName.charAt(0)) + methodName.substring(1),
exception.orElseThrow(),
methodName);
fix.prefixWith(methodTree, newMethod)
.addStaticImport("org.junit.jupiter.api.Assertions.assertThrows");
return Optional.of(fix.build());
}
return Optional.ofNullable(annotation.getAttributes().get("expectedExceptions"))
.map(
expectedExceptions ->
getExpectedException(expectedExceptions, state)
.map(
expectedException -> {
SuggestedFix.Builder fix =
SuggestedFix.builder()
.replace(
methodTree.getBody(),
buildWrappedBody(
methodTree.getBody(), expectedException, state));
ImmutableList<String> removedExceptions =
getRemovedExceptions(expectedExceptions, state);
if (!removedExceptions.isEmpty()) {
fix.prefixWith(
methodTree,
String.format(
"// XXX: Removed handling of `%s` because this migration doesn't support%n// XXX: multiple expected exceptions.%n",
String.join(", ", removedExceptions)));
}
return fix.build();
})
.orElseGet(SuggestedFix::emptyFix));
}
private static Optional<String> getExpectedException(
ExpressionTree expectedExceptions, VisitorState state) {
if (expectedExceptions.getKind() == NEW_ARRAY) {
NewArrayTree arrayTree = (NewArrayTree) expectedExceptions;
if (arrayTree.getInitializers().isEmpty()) {
return Optional.empty();
}
return Optional.of(SourceCode.treeToString(arrayTree.getInitializers().get(0), state));
} else if (expectedExceptions.getKind() == MEMBER_SELECT) {
return Optional.of(SourceCode.treeToString(expectedExceptions, state));
}
return Optional.empty();
}
private static ImmutableList<String> getRemovedExceptions(
ExpressionTree expectedExceptions, VisitorState state) {
if (expectedExceptions.getKind() != NEW_ARRAY) {
return ImmutableList.of();
}
NewArrayTree arrayTree = (NewArrayTree) expectedExceptions;
return arrayTree.getInitializers().subList(1, arrayTree.getInitializers().size()).stream()
.map(initializer -> SourceCode.treeToString(initializer, state))
.collect(toImmutableList());
}
private static String buildWrappedBody(BlockTree tree, String exception, VisitorState state) {
return String.format(
"{%norg.junit.jupiter.api.Assertions.assertThrows(%s, () -> %s);%n}",
exception, SourceCode.treeToString(tree, state));
}
}

View File

@@ -0,0 +1,61 @@
package tech.picnic.errorprone.testngjunit;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.fixes.SuggestedFix;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.Tree;
import java.util.Optional;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.AnnotationMetadata;
import tech.picnic.errorprone.util.SourceCode;
/** A {@link AttributeMigrator} that migrates the {@code group} attribute. */
@Immutable
final class GroupsAttributeMigrator implements AttributeMigrator {
@Override
public Optional<SuggestedFix> migrate(
TestNgMetadata metadata,
AnnotationMetadata annotation,
MethodTree methodTree,
boolean minimalChangesMode,
VisitorState state) {
ExpressionTree groupsExpression = annotation.getAttributes().get("groups");
if (groupsExpression == null) {
return Optional.empty();
}
ImmutableList<String> groups = extractGroups(groupsExpression, state);
if (!groups.stream().allMatch(GroupsAttributeMigrator::isValidTagName)) {
return Optional.empty();
}
SuggestedFix.Builder fix = SuggestedFix.builder().addImport("org.junit.jupiter.api.Tag");
groups.forEach(group -> fix.prefixWith(methodTree, String.format("@Tag(\"%s\")%n", group)));
return Optional.of(fix.build());
}
private static boolean isValidTagName(String tagName) {
return !tagName.isEmpty() && tagName.chars().noneMatch(Character::isISOControl);
}
private static ImmutableList<String> extractGroups(ExpressionTree dataValue, VisitorState state) {
if (dataValue.getKind() == Tree.Kind.STRING_LITERAL) {
return ImmutableList.of(trimTagName(SourceCode.treeToString(dataValue, state)));
}
NewArrayTree groupsTree = (NewArrayTree) dataValue;
return groupsTree.getInitializers().stream()
.map(initializer -> trimTagName(SourceCode.treeToString(initializer, state)))
.collect(toImmutableList());
}
private static String trimTagName(String tagName) {
return tagName.replaceAll("(^\")|(\"$)", "").trim();
}
}

View File

@@ -0,0 +1,39 @@
package tech.picnic.errorprone.testngjunit;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.fixes.SuggestedFix;
import com.sun.source.tree.MethodTree;
import java.util.Optional;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.AnnotationMetadata;
import tech.picnic.errorprone.util.SourceCode;
/**
* A {@link AttributeMigrator} that migrates the {@code org.testng.annotations.Test#priority}
* attribute.
*/
@Immutable
final class PriorityAttributeMigrator implements AttributeMigrator {
@Override
public Optional<SuggestedFix> migrate(
TestNgMetadata metadata,
AnnotationMetadata annotation,
MethodTree methodTree,
boolean minimalChangesMode,
VisitorState state) {
return Optional.ofNullable(annotation.getAttributes().get("priority"))
.map(
priority ->
SuggestedFix.builder()
.addImport("org.junit.jupiter.api.Order")
.addImport("org.junit.jupiter.api.TestMethodOrder")
.addImport("org.junit.jupiter.api.MethodOrderer")
.prefixWith(
methodTree,
String.format("@Order(%s)%n", SourceCode.treeToString(priority, state)))
.prefixWith(
metadata.getClassTree(),
"@TestMethodOrder(MethodOrderer.OrderAnnotation.class)\n")
.build());
}
}

View File

@@ -0,0 +1,51 @@
package tech.picnic.errorprone.testngjunit;
import com.google.errorprone.VisitorState;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.MethodTree;
import java.util.Optional;
import javax.lang.model.element.Modifier;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.SetupTeardownType;
/**
* A helper class that migrates TestNG setup and teardown methods to their JUnit Jupiter equivalent.
*/
final class SetupTeardownMethodMigrator {
private SetupTeardownMethodMigrator() {}
/**
* Create the {@link SuggestedFix} required to migrate a TestNG setup/teardown methods to the
* JUnit Jupiter variant.
*
* @param tree The setup/teardown method tree.
* @param type The setup/teardown type.
* @param state The visitor state.
* @return An {@link Optional} containing the created fix.
*/
static Optional<SuggestedFix> createFix(
MethodTree tree, SetupTeardownType type, VisitorState state) {
return getSetupTeardownAnnotationTree(tree, type, state)
.map(
annotation -> {
SuggestedFix.Builder fix =
SuggestedFix.builder()
.replace(annotation, String.format("@%s", type.getJunitAnnotationClass()));
if (type.requiresStaticMethod()
&& !tree.getModifiers().getFlags().contains(Modifier.STATIC)) {
SuggestedFixes.addModifiers(tree, state, Modifier.STATIC).ifPresent(fix::merge);
}
return fix.build();
});
}
private static Optional<? extends AnnotationTree> getSetupTeardownAnnotationTree(
MethodTree tree, SetupTeardownType type, VisitorState state) {
return ASTHelpers.getAnnotations(tree).stream()
.filter(annotation -> type.getAnnotationMatcher().matches(annotation, state))
.findFirst();
}
}

View File

@@ -0,0 +1,32 @@
package tech.picnic.errorprone.testngjunit;
import static java.util.Arrays.stream;
import java.util.Optional;
/** The annotation attributes that are supported by the TestNG to JUnit Jupiter migration. */
enum TestAnnotationAttribute {
DATA_PROVIDER("dataProvider", new DataProviderAttributeMigrator()),
DESCRIPTION("description", new DescriptionAttributeMigrator()),
ENABLED("enabled", new EnabledAttributeMigrator()),
EXPECTED_EXCEPTIONS("expectedExceptions", new ExpectedExceptionsAttributeMigrator()),
GROUPS("groups", new GroupsAttributeMigrator()),
PRIORITY("priority", new PriorityAttributeMigrator()),
TIMEOUT("timeOut", new TimeOutAttributeMigrator());
private final String name;
private final AttributeMigrator attributeMigrator;
TestAnnotationAttribute(String name, AttributeMigrator attributeMigrator) {
this.name = name;
this.attributeMigrator = attributeMigrator;
}
AttributeMigrator getAttributeMigrator() {
return attributeMigrator;
}
static Optional<TestAnnotationAttribute> fromString(String attribute) {
return stream(values()).filter(v -> v.name.equals(attribute)).findFirst();
}
}

View File

@@ -0,0 +1,262 @@
package tech.picnic.errorprone.testngjunit;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.BugPattern.StandardTags.REFACTORING;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.util.TreeScanner;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import javax.inject.Inject;
import javax.lang.model.element.Modifier;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.AnnotationMetadata;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.DataProviderMetadata;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.SetupTeardownType;
/**
* A {@link BugChecker} that migrates TestNG unit tests to JUnit 5.
*
* <p>Supported TestNG annotation attributes are:
*
* <ul>
* <li>{@code dataProvider}
* <li>{@code description}
* <li>{@code isEnabled}
* <li>{@code expectedExceptions}
* <li>{@code priority}
* <li>{@code groups}
* </ul>
*
* This migration will also take care of any setup/teardown methods.
*
* <p>Note: As the {@code @BeforeAll} and {@code @AfterAll} methods in JUnit are required to be
* static, this <em>might</em> introduce breaking changes.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Migrate TestNG tests to their JUnit equivalent",
linkType = NONE,
tags = REFACTORING,
severity = ERROR)
public final class TestNGJUnitMigration extends BugChecker implements CompilationUnitTreeMatcher {
private static final long serialVersionUID = 1L;
private static final String CONSERVATIVE_MIGRATION_MODE_FLAG =
"TestNGJUnitMigration:ConservativeMode";
private static final String MINIMAL_CHANGES_MODE_FLAG = "TestNGJUnitMigration:MinimalChanges";
private final boolean conservativeMode;
private final boolean strictBehaviorPreserving;
/**
* Instantiates a new {@link TestNGJUnitMigration} instance. This will default to the aggressive
* migration mode.
*/
public TestNGJUnitMigration() {
this(ErrorProneFlags.empty());
}
/**
* Instantiates a new {@link TestNGJUnitMigration} with the specified {@link ErrorProneFlags}.
*
* @param flags The Error Prone flags used to set the migration mode.
*/
@Inject
TestNGJUnitMigration(ErrorProneFlags flags) {
conservativeMode = flags.getBoolean(CONSERVATIVE_MIGRATION_MODE_FLAG).orElse(false);
strictBehaviorPreserving = flags.getBoolean(MINIMAL_CHANGES_MODE_FLAG).orElse(true);
}
@Override
public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) {
TestNGScanner scanner = new TestNGScanner(state);
ImmutableMap<ClassTree, TestNgMetadata> classMetaData = scanner.collectMetadataForClasses(tree);
new TreeScanner<@Nullable Void, TestNgMetadata>() {
@Override
public @Nullable Void visitClass(ClassTree node, TestNgMetadata testNgMetadata) {
TestNgMetadata metadata = classMetaData.get(node);
if (metadata == null) {
return super.visitClass(node, testNgMetadata);
}
// XXX: Why is this not fixed anymore?
if (strictBehaviorPreserving) {
List<? extends AnnotationTree> annotationTrees = ASTHelpers.getAnnotations(node);
if (!annotationTrees.isEmpty()) {
AnnotationTree classLevelTestAnnotation = annotationTrees.get(0);
state.reportMatch(
describeMatch(
classLevelTestAnnotation, SuggestedFix.delete(classLevelTestAnnotation)));
}
}
for (DataProviderMetadata dataProviderMetadata : metadata.getDataProvidersInUse()) {
if (strictBehaviorPreserving) {
MethodTree methodTree = dataProviderMetadata.getMethodTree();
state.reportMatch(
describeMatch(
methodTree,
SuggestedFixes.addModifiers(methodTree, state, Modifier.STATIC).orElseThrow()));
AnnotationTree dpAnnotation =
ASTHelpers.getAnnotationWithSimpleName(
ASTHelpers.getAnnotations(methodTree), "DataProvider");
if (dpAnnotation != null) {
// state.reportMatch(describeMatch(dpAnnotation,
// SuggestedFix.delete(dpAnnotation)));
// XXX: VERY UGLY WAY TO FIX THIS THE OPENJDK empty line problem.
state.reportMatch(
describeMatch(
dpAnnotation,
SuggestedFix.replace(
ASTHelpers.getStartPosition(dpAnnotation) - 5,
state.getEndPosition(dpAnnotation),
"")));
}
continue;
}
DataProviderMigrator.createFix(
metadata.getClassTree(), dataProviderMetadata.getMethodTree(), state)
.ifPresent(
fix ->
state.reportMatch(
describeMatch(
dataProviderMetadata.getMethodTree(),
fix.toBuilder().removeStaticImport("org.testng.Assert.*").build())));
}
for (Entry<MethodTree, SetupTeardownType> entry : metadata.getSetupTeardown().entrySet()) {
SetupTeardownMethodMigrator.createFix(entry.getKey(), entry.getValue(), state)
.ifPresent(fix -> state.reportMatch(describeMatch(entry.getKey(), fix)));
}
super.visitClass(node, metadata);
return null;
}
@Override
public @Nullable Void visitMethod(MethodTree tree, TestNgMetadata metadata) {
/* Make sure ALL Tests in the class can be migrated. */
if (conservativeMode && !canMigrateAllTestsInClass(metadata, state)) {
return super.visitMethod(tree, metadata);
}
metadata
.getAnnotation(tree)
.ifPresent(
annotation -> {
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
buildAttributeFixes(metadata, annotation, tree, state).forEach(fixBuilder::merge);
fixBuilder.merge(migrateAnnotation(annotation, tree));
state.reportMatch(describeMatch(tree, fixBuilder.build()));
});
return super.visitMethod(tree, metadata);
}
}.scan(tree, null);
/* All suggested fixes are already directly reported to the `VisitorState`. */
return Description.NO_MATCH;
}
private ImmutableList<SuggestedFix> buildAttributeFixes(
TestNgMetadata metadata,
AnnotationMetadata annotationMetadata,
MethodTree methodTree,
VisitorState state) {
return annotationMetadata.getAttributes().entrySet().stream()
.flatMap(
entry ->
trySuggestFix(metadata, annotationMetadata, entry.getKey(), methodTree, state)
.stream())
.collect(toImmutableList());
}
private boolean canMigrateTest(
MethodTree methodTree,
TestNgMetadata metadata,
AnnotationMetadata annotationMetadata,
VisitorState state) {
ImmutableList<TestAnnotationAttribute> attributes =
annotationMetadata.getAttributes().keySet().stream()
.map(TestAnnotationAttribute::fromString)
.flatMap(Optional::stream)
.collect(toImmutableList());
return (annotationMetadata.getAttributes().isEmpty() || !attributes.isEmpty())
&& attributes.stream()
.allMatch(
kind ->
kind.getAttributeMigrator()
.migrate(
metadata,
annotationMetadata,
methodTree,
strictBehaviorPreserving,
state)
.isPresent());
}
private boolean canMigrateAllTestsInClass(TestNgMetadata metadata, VisitorState state) {
return metadata.getMethodAnnotations().entrySet().stream()
.allMatch(entry -> canMigrateTest(entry.getKey(), metadata, entry.getValue(), state));
}
private Optional<SuggestedFix> trySuggestFix(
TestNgMetadata metadata,
AnnotationMetadata annotation,
String attributeName,
MethodTree methodTree,
VisitorState state) {
return TestAnnotationAttribute.fromString(attributeName)
.map(TestAnnotationAttribute::getAttributeMigrator)
.flatMap(
migrator ->
migrator.migrate(metadata, annotation, methodTree, strictBehaviorPreserving, state))
.or(
() ->
UnsupportedAttributeMigrator.migrate(annotation, methodTree, attributeName, state));
}
private SuggestedFix migrateAnnotation(
AnnotationMetadata annotationMetadata, MethodTree methodTree) {
SuggestedFix.Builder builder = SuggestedFix.builder();
if (annotationMetadata.getAttributes().isEmpty() && strictBehaviorPreserving) {
builder.replace(annotationMetadata.getAnnotationTree(), "@org.junit.jupiter.api.Test");
return builder.build();
}
SuggestedFix.Builder fixBuilder = builder.delete(annotationMetadata.getAnnotationTree());
boolean hasDataProviderAttr = annotationMetadata.getAttributes().containsKey("dataProvider");
boolean hasExpectedExceptionsAttr =
annotationMetadata.getAttributes().containsKey("expectedExceptions");
if (!hasDataProviderAttr && (!strictBehaviorPreserving || !hasExpectedExceptionsAttr)) {
fixBuilder.prefixWith(methodTree, "@org.junit.jupiter.api.Test\n ");
}
return fixBuilder.build();
}
}

View File

@@ -0,0 +1,30 @@
package tech.picnic.errorprone.testngjunit;
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
import static com.google.errorprone.matchers.Matchers.isType;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.TestNgMatchers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
/**
* A collection of TestNG-specific helper methods and {@link Matcher}s.
*
* <p>These constants and methods are additions to the ones found in {@link TestNgMatchers}.
*/
final class TestNGMatchers {
/**
* Matches the TestNG {@code Test} annotation specifically. As {@link
* TestNgMatchers#hasTestNgAnnotation(ClassTree)} also other TestNG annotations.
*/
public static final Matcher<AnnotationTree> TESTNG_TEST_ANNOTATION =
isType("org.testng.annotations.Test");
/** Matches the TestNG {@code DataProvider} annotation specifically. */
public static final Matcher<MethodTree> TESTNG_VALUE_FACTORY_METHOD =
hasAnnotation("org.testng.annotations.DataProvider");
private TestNGMatchers() {}
}

View File

@@ -0,0 +1,116 @@
package tech.picnic.errorprone.testngjunit;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
import static com.google.errorprone.matchers.Matchers.hasModifier;
import static com.google.errorprone.matchers.Matchers.not;
import static tech.picnic.errorprone.testngjunit.TestNGMatchers.TESTNG_TEST_ANNOTATION;
import static tech.picnic.errorprone.testngjunit.TestNGMatchers.TESTNG_VALUE_FACTORY_METHOD;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreeScanner;
import java.util.Optional;
import javax.lang.model.element.Modifier;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.AnnotationMetadata;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.DataProviderMetadata;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.SetupTeardownType;
/**
* A {@link TreeScanner} which will scan a {@link com.sun.source.tree.CompilationUnitTree} and
* collect data required for the migration from each class in the compilation unit.
*
* <p>This data can be retrieved using {@link #collectMetadataForClasses(CompilationUnitTree)}.
*/
final class TestNGScanner extends TreeScanner<@Nullable Void, TestNgMetadata.Builder> {
private static final Matcher<MethodTree> TESTNG_TEST_METHOD =
anyOf(
hasAnnotation("org.testng.annotations.Test"),
allOf(hasModifier(Modifier.PUBLIC), not(hasModifier(Modifier.STATIC))));
private final ImmutableMap.Builder<ClassTree, TestNgMetadata> metadataBuilder =
ImmutableMap.builder();
private final VisitorState state;
TestNGScanner(VisitorState state) {
this.state = state;
}
@Override
public @Nullable Void visitClass(ClassTree tree, TestNgMetadata.Builder unused) {
TestNgMetadata.Builder builder = TestNgMetadata.builder();
builder.setClassTree(tree);
getTestNgAnnotation(tree, state).ifPresent(builder::setClassLevelAnnotationMetadata);
super.visitClass(tree, builder);
metadataBuilder.put(tree, builder.build());
return null;
}
@Override
public @Nullable Void visitMethod(MethodTree tree, TestNgMetadata.Builder builder) {
if (ASTHelpers.isGeneratedConstructor(tree)) {
return super.visitMethod(tree, builder);
}
if (TESTNG_VALUE_FACTORY_METHOD.matches(tree, state) && DataProviderMigrator.canFix(tree)) {
builder
.dataProviderMetadataBuilder()
.put(tree.getName().toString(), DataProviderMetadata.create(tree));
return super.visitMethod(tree, builder);
}
Optional<SetupTeardownType> setupTeardownType = SetupTeardownType.matchType(tree, state);
if (setupTeardownType.isPresent()) {
builder.setupTeardownBuilder().put(tree, setupTeardownType.orElseThrow());
return super.visitMethod(tree, builder);
}
if (TESTNG_TEST_METHOD.matches(tree, state)) {
getTestNgAnnotation(tree, state)
.or(builder::getClassLevelAnnotationMetadata)
.ifPresent(annotation -> builder.methodAnnotationsBuilder().put(tree, annotation));
}
return super.visitMethod(tree, builder);
}
public ImmutableMap<ClassTree, TestNgMetadata> collectMetadataForClasses(
CompilationUnitTree tree) {
scan(tree, null);
return metadataBuilder.build();
}
@CanIgnoreReturnValue
private static Optional<AnnotationMetadata> getTestNgAnnotation(Tree tree, VisitorState state) {
return ASTHelpers.getAnnotations(tree).stream()
.filter(annotation -> TESTNG_TEST_ANNOTATION.matches(annotation, state))
.findFirst()
.map(
annotationTree ->
AnnotationMetadata.create(
annotationTree,
annotationTree.getArguments().stream()
.filter(AssignmentTree.class::isInstance)
.map(AssignmentTree.class::cast)
.collect(
toImmutableMap(
assignment ->
((IdentifierTree) assignment.getVariable())
.getName()
.toString(),
AssignmentTree::getExpression))));
}
}

View File

@@ -0,0 +1,216 @@
package tech.picnic.errorprone.testngjunit;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
import static com.google.errorprone.matchers.Matchers.isType;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.VisitorState;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
/**
* POJO containing data collected using {@link TestNGScanner} for use in {@link
* TestNGJUnitMigration}.
*/
@AutoValue
abstract class TestNgMetadata {
abstract ClassTree getClassTree();
abstract Optional<AnnotationMetadata> getClassLevelAnnotationMetadata();
abstract ImmutableMap<MethodTree, AnnotationMetadata> getMethodAnnotations();
abstract ImmutableMap<MethodTree, SetupTeardownType> getSetupTeardown();
/**
* Retrieve the tests that can be migrated.
*
* @return An {@link ImmutableMap} with mapping {@code DataProvider}'s name to its respective
* metadata.
*/
public abstract ImmutableMap<String, DataProviderMetadata> getDataProviderMetadata();
final ImmutableList<DataProviderMetadata> getDataProvidersInUse() {
return getDataProviderMetadata().entrySet().stream()
.filter(
entry ->
getAnnotations().stream()
.anyMatch(
annotation -> {
ExpressionTree dataProviderNameExpression =
annotation.getAttributes().get("dataProvider");
if (dataProviderNameExpression == null) {
return false;
}
return ASTHelpers.constValue(dataProviderNameExpression, String.class)
.equals(entry.getKey());
}))
.map(Map.Entry::getValue)
.collect(toImmutableList());
}
final ImmutableSet<AnnotationMetadata> getAnnotations() {
return ImmutableSet.copyOf(getMethodAnnotations().values());
}
final Optional<AnnotationMetadata> getAnnotation(MethodTree methodTree) {
return Optional.ofNullable(getMethodAnnotations().get(methodTree));
}
static Builder builder() {
return new AutoValue_TestNgMetadata.Builder();
}
@AutoValue.Builder
abstract static class Builder {
abstract ImmutableMap.Builder<MethodTree, AnnotationMetadata> methodAnnotationsBuilder();
abstract ImmutableMap.Builder<MethodTree, SetupTeardownType> setupTeardownBuilder();
abstract ImmutableMap.Builder<String, DataProviderMetadata> dataProviderMetadataBuilder();
abstract Builder setClassTree(ClassTree value);
abstract Optional<AnnotationMetadata> getClassLevelAnnotationMetadata();
abstract Builder setClassLevelAnnotationMetadata(AnnotationMetadata value);
abstract Builder setMethodAnnotations(ImmutableMap<MethodTree, AnnotationMetadata> value);
abstract Builder setSetupTeardown(ImmutableMap<MethodTree, SetupTeardownType> value);
abstract Builder setDataProviderMetadata(ImmutableMap<String, DataProviderMetadata> value);
abstract TestNgMetadata build();
}
/**
* POJO containing data for a specific {@code Test} annotation for use in {@link
* TestNGJUnitMigration}.
*/
@AutoValue
public abstract static class AnnotationMetadata {
/**
* Get the {@link AnnotationTree} of this metadata instance.
*
* @return the annotation tree this metadata contains information on.
*/
public abstract AnnotationTree getAnnotationTree();
/**
* A mapping for all attributes in the annotation to their value.
*
* @return an {@link ImmutableMap} mapping each annotation attribute to their respective value.
*/
public abstract ImmutableMap<String, ExpressionTree> getAttributes();
/**
* Instantiate a new {@link AnnotationMetadata}.
*
* @param annotationTree The annotation tree.
* @param attributes The attributes in that annotation tree.
* @return The new {@link AnnotationMetadata} instance.
*/
public static AnnotationMetadata create(
AnnotationTree annotationTree, ImmutableMap<String, ExpressionTree> attributes) {
return new AutoValue_TestNgMetadata_AnnotationMetadata(annotationTree, attributes);
}
}
@SuppressWarnings("ImmutableEnumChecker" /* Matcher instances are final. */)
public enum SetupTeardownType {
// XXX: Consider using `@BeforeAll` to more accurately preserve behavior. However, note that it
// requires a static method and therefore may introduce breaking changes.
BEFORE_TEST(
"org.testng.annotations.BeforeTest",
"org.junit.jupiter.api.BeforeEach",
/* requiresStaticMethod= */ false),
BEFORE_CLASS(
"org.testng.annotations.BeforeClass",
"org.junit.jupiter.api.BeforeAll",
/* requiresStaticMethod= */ true),
BEFORE_METHOD(
"org.testng.annotations.BeforeMethod",
"org.junit.jupiter.api.BeforeEach",
/* requiresStaticMethod= */ false),
// XXX: Consider using `@AfterAll` to more accurately preserve behavior. However, note that it
// requires a static method and therefore may introduce breaking changes.
AFTER_TEST(
"org.testng.annotations.AfterTest",
"org.junit.jupiter.api.AfterEach",
/* requiresStaticMethod= */ false),
AFTER_CLASS(
"org.testng.annotations.AfterClass",
"org.junit.jupiter.api.AfterAll",
/* requiresStaticMethod= */ true),
AFTER_METHOD(
"org.testng.annotations.AfterMethod",
"org.junit.jupiter.api.AfterEach",
/* requiresStaticMethod= */ false);
private final Matcher<AnnotationTree> annotationMatcher;
private final Matcher<MethodTree> methodTreeMatcher;
private final String junitAnnotationClass;
private final boolean requiresStaticMethod;
SetupTeardownType(
String testNgAnnotationClass, String junitAnnotationClass, boolean requiresStaticMethod) {
annotationMatcher = isType(testNgAnnotationClass);
methodTreeMatcher = hasAnnotation(testNgAnnotationClass);
this.junitAnnotationClass = junitAnnotationClass;
this.requiresStaticMethod = requiresStaticMethod;
}
static Optional<SetupTeardownType> matchType(MethodTree methodTree, VisitorState state) {
return Arrays.stream(values())
.filter(v -> v.methodTreeMatcher.matches(methodTree, state))
.findFirst();
}
Matcher<AnnotationTree> getAnnotationMatcher() {
return annotationMatcher;
}
String getJunitAnnotationClass() {
return junitAnnotationClass;
}
// XXX: Improve method name.
boolean requiresStaticMethod() {
return requiresStaticMethod;
}
}
/**
* Contains data for a {@code DataProvider} annotation for use in {@link TestNGJUnitMigration}.
*/
@AutoValue
public abstract static class DataProviderMetadata {
abstract MethodTree getMethodTree();
abstract String getName();
/**
* Instantiate a new {@link DataProviderMetadata} instance.
*
* @param methodTree The value factory method tree.
* @return A new {@link DataProviderMetadata} instance.
*/
public static DataProviderMetadata create(MethodTree methodTree) {
return new AutoValue_TestNgMetadata_DataProviderMetadata(
methodTree, methodTree.getName().toString());
}
}
}

View File

@@ -0,0 +1,39 @@
package tech.picnic.errorprone.testngjunit;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.fixes.SuggestedFix;
import com.sun.source.tree.MethodTree;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import tech.picnic.errorprone.util.SourceCode;
/**
* A {@link AttributeMigrator} that migrates the {@code org.testng.annotations.Test#timeOut}
* attribute. The TestNG {@code org.testng.annotations.Test#timeOut} attribute is always in
* milliseconds, the JUnit variant {@code @Timeout} takes a value in seconds by default, hence we
* add the {@link java.util.concurrent.TimeUnit#MILLISECONDS} attribute.
*/
@Immutable
final class TimeOutAttributeMigrator implements AttributeMigrator {
@Override
public Optional<SuggestedFix> migrate(
TestNgMetadata metadata,
TestNgMetadata.AnnotationMetadata annotation,
MethodTree methodTree,
boolean minimalChangesMode,
VisitorState state) {
return Optional.ofNullable(annotation.getAttributes().get("timeOut"))
.map(
timeOut ->
SuggestedFix.builder()
.addImport("org.junit.jupiter.api.Timeout")
.addStaticImport(TimeUnit.class.getCanonicalName() + ".MILLISECONDS")
.prefixWith(
methodTree,
String.format(
"@Timeout(value = %s, unit = MILLISECONDS)%n",
SourceCode.treeToString(timeOut, state)))
.build());
}
}

View File

@@ -0,0 +1,32 @@
package tech.picnic.errorprone.testngjunit;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.fixes.SuggestedFix;
import com.sun.source.tree.MethodTree;
import java.util.Optional;
import tech.picnic.errorprone.util.SourceCode;
/**
* A {@link AttributeMigrator} that leaves a comment for attributes that aren't supported in the
* migration.
*/
@Immutable
final class UnsupportedAttributeMigrator {
private UnsupportedAttributeMigrator() {}
static Optional<SuggestedFix> migrate(
TestNgMetadata.AnnotationMetadata annotation,
MethodTree methodTree,
String attributeName,
VisitorState state) {
return Optional.ofNullable(annotation.getAttributes().get(attributeName))
.map(
value ->
SuggestedFix.prefixWith(
methodTree,
String.format(
"// XXX: Attribute `%s` is not supported, value: `%s`%n",
attributeName, SourceCode.treeToString(value, state))));
}
}

View File

@@ -0,0 +1,4 @@
/** TestNG to JUnit migration using Error-Prone. */
@com.google.errorprone.annotations.CheckReturnValue
@org.jspecify.annotations.NullMarked
package tech.picnic.errorprone.testngjunit;

View File

@@ -0,0 +1,127 @@
package tech.picnic.errorprone.testngjunit.refasterrules;
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import org.junit.jupiter.api.Assertions;
import org.testng.Assert;
/** Refaster rules to replace TestNG assertions with JUnit equivalents. */
@SuppressWarnings("StaticImport")
final class TestNgAssertionsToJUnitRules {
private TestNgAssertionsToJUnitRules() {}
@SuppressWarnings("AssertEqual")
static final class AssertEquals {
@BeforeTemplate
void before(Object expected, Object actual) {
Assert.assertEquals(actual, expected);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object expected, Object actual) {
Assertions.assertEquals(expected, actual);
}
}
@SuppressWarnings("AssertEqualWithMessage")
static final class AssertEqualsMessage {
@BeforeTemplate
void before(Object expected, Object actual, String message) {
Assert.assertEquals(actual, expected, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object expected, Object actual, String message) {
Assertions.assertEquals(expected, actual, message);
}
}
@SuppressWarnings("AssertUnequal")
static final class AssertNotEquals {
@BeforeTemplate
void before(Object expected, Object actual) {
Assert.assertNotEquals(actual, expected);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object expected, Object actual) {
Assertions.assertNotEquals(expected, actual);
}
}
@SuppressWarnings("AssertUnequalWithMessage")
static final class AssertNotEqualsMessage {
@BeforeTemplate
void before(Object expected, Object actual, String message) {
Assert.assertNotEquals(actual, expected, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(Object expected, Object actual, String message) {
Assertions.assertNotEquals(expected, actual, message);
}
}
@SuppressWarnings({"AssertFalse", "AssertThatIsFalse"})
static final class AssertFalseCondition {
@BeforeTemplate
void before(boolean condition) {
Assert.assertFalse(condition);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean condition) {
Assertions.assertFalse(condition);
}
}
@SuppressWarnings({"AssertFalseWithMessage", "AssertThatWithFailMessageStringIsFalse"})
static final class AssertFalseConditionMessage {
@BeforeTemplate
void before(boolean condition, String message) {
Assert.assertFalse(condition, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean condition, String message) {
Assertions.assertFalse(condition, message);
}
}
@SuppressWarnings({"AssertThatIsTrue", "AssertTrue"})
static final class AssertTrueCondition {
@BeforeTemplate
void before(boolean condition) {
Assert.assertTrue(condition);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean condition) {
Assertions.assertTrue(condition);
}
}
@SuppressWarnings({"AssertThatWithFailMessageStringIsTrue", "AssertTrueWithMessage"})
static final class AssertTrueConditionMessage {
@BeforeTemplate
void before(boolean condition, String message) {
Assert.assertTrue(condition, message);
}
@AfterTemplate
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
void after(boolean condition, String message) {
Assertions.assertTrue(condition, message);
}
}
}

View File

@@ -0,0 +1,4 @@
/** Picnic Refaster rules. */
@com.google.errorprone.annotations.CheckReturnValue
@org.jspecify.annotations.NullMarked
package tech.picnic.errorprone.testngjunit.refasterrules;

View File

@@ -0,0 +1,27 @@
package tech.picnic.errorprone.util;
import com.google.errorprone.VisitorState;
import com.sun.source.tree.Tree;
/**
* A collection of Error Prone utility methods for dealing with the source code representation of
* AST nodes.
*/
// XXX: This is a duplicate of `error-prone-contrib`s `SourceCode`, improve this.
public final class SourceCode {
private SourceCode() {}
/**
* Returns a string representation of the given {@link Tree}, preferring the original source code
* (if available) over its prettified representation.
*
* @param tree The AST node of interest.
* @param state A {@link VisitorState} describing the context in which the given {@link Tree} is
* found.
* @return A non-{@code null} string.
*/
public static String treeToString(Tree tree, VisitorState state) {
String src = state.getSourceForNode(tree);
return src != null ? src : tree.toString();
}
}

View File

@@ -0,0 +1,562 @@
package tech.picnic.errorprone.testngjunit;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class TestNGJUnitMigrationTest {
@Test
void identification() {
CompilationTestHelper.newInstance(TestNGJUnitMigration.class, getClass())
.addSourceLines(
"A.java",
"import java.util.stream.Stream;",
"import org.testng.annotations.DataProvider;",
"import org.testng.annotations.Test;",
"",
"@Test",
"public class A {",
" // BUG: Diagnostic contains:",
" public void classLevelAnnotation() {}",
"",
" public static void staticNotATest() {}",
"",
" private void notATest() {}",
"",
" @Test(description = \"bar\")",
" // BUG: Diagnostic contains:",
" public void methodAnnotation() {}",
"",
" @Test",
" public static class B {",
" // BUG: Diagnostic contains:",
" public void nestedClass() {}",
" }",
"",
" @Test(dataProvider = \"\")",
" // BUG: Diagnostic contains:",
" public void dataProviderEmptyString() {}",
"",
" @Test(dataProvider = \"dataProviderTestCases\")",
" // BUG: Diagnostic contains:",
" public void dataProvider(int foo) {}",
"",
" @DataProvider",
" // BUG: Diagnostic contains:",
" private static Object[][] dataProviderTestCases() {",
" return new Object[][] {{1}, {2}};",
" }",
"",
" @DataProvider",
" private static Object[][] unusedDataProvider() {",
" return new Object[][] {{1}, {2}};",
" }",
"",
" @Test(dataProvider = \"notMigratableDataProviderTestCases\")",
" // BUG: Diagnostic contains:",
" public void notMigratableDataProvider(int foo) {}",
"",
" @DataProvider",
" private static Object[][] notMigratableDataProviderTestCases() {",
" return Stream.of(1, 2, 3).map(i -> new Object[] {i}).toArray(Object[][]::new);",
" }",
"}")
.doTest();
}
@Test
void identificationConservativeMode() {
CompilationTestHelper.newInstance(TestNGJUnitMigration.class, getClass())
.setArgs("-XepOpt:TestNGJUnitMigration:ConservativeMode=true")
.addSourceLines(
"A.java",
"import java.util.stream.Stream;",
"import org.testng.annotations.DataProvider;",
"import org.testng.annotations.Test;",
"",
"@Test",
"public class A {",
" public void classLevelAnnotation() {}",
"",
" @Test(description = \"bar\")",
" public void methodAnnotation() {}",
"",
" @Test",
" public static class B {",
" // BUG: Diagnostic contains:",
" public void nestedClass() {}",
" }",
"",
" @Test(dataProvider = \"notMigratableDataProviderTestCases\")",
" public void notMigratableDataProvider(int foo) {}",
"",
" @DataProvider",
" private static Object[][] notMigratableDataProviderTestCases() {",
" return Stream.of(1, 2, 3).map(i -> new Object[] {i}).toArray(Object[][]::new);",
" }",
"}")
.addSourceLines(
"B.java",
"import org.testng.annotations.Test;",
"",
"@Test",
"public class B {",
" public void classLevelAnnotation() {}",
"",
" @Test(description = \"bar\")",
" public void methodAnnotation() {}",
"",
" @Test(testName = \"unsupportedAttribute\")",
" public void unsupportedAttribute() {}",
"",
" @Test(testName = \"unsupportedAttribute\", suiteName = \"unsupportedAttribute\")",
" public void multipleUnsupportedAttributes() {}",
"}")
.addSourceLines(
"C.java",
"import org.testng.annotations.Test;",
"",
"@Test",
"public class C {",
" // BUG: Diagnostic contains:",
" public void classLevelAnnotation() {}",
"",
" @Test(description = \"bar\")",
" // BUG: Diagnostic contains:",
" public void methodAnnotation() {}",
"}")
.doTest();
}
@Test
void replacement() {
BugCheckerRefactoringTestHelper.newInstance(TestNGJUnitMigration.class, getClass())
.addInputLines(
"A.java",
"import static org.testng.Assert.*;",
"",
"import org.testng.annotations.AfterClass;",
"import org.testng.annotations.AfterMethod;",
"import org.testng.annotations.AfterTest;",
"import org.testng.annotations.BeforeClass;",
"import org.testng.annotations.BeforeMethod;",
"import org.testng.annotations.BeforeTest;",
"import org.testng.annotations.DataProvider;",
"import org.testng.annotations.Test;",
"",
"@Test",
"class A {",
" @BeforeTest",
" private void setupTest() {}",
"",
" @BeforeClass",
" private void setupClass() {}",
"",
" @BeforeMethod",
" private void setup() {}",
"",
" @AfterTest",
" private void teardownTest() {}",
"",
" @AfterClass",
" private void teardownClass() {}",
"",
" @AfterMethod",
" private void teardown() {}",
"",
" public void classLevelAnnotation() {}",
"",
" @Test(priority = 1, timeOut = 500, description = \"unit\")",
" public void priorityTimeOutAndDescription() {}",
"",
" @Test(testName = \"unsupportedAttribute\")",
" public void unsupportedAttribute() {}",
"",
" @Test(testName = \"unsupportedAttribute\", suiteName = \"unsupportedAttribute\")",
" public void multipleUnsupportedAttributes() {}",
"",
" @Test(dataProvider = \"dataProviderTestCases\")",
" public void dataProvider(String string, int number) {}",
"",
" @DataProvider",
" private Object[][] dataProviderTestCases() {",
" int[] values = new int[] {1, 2, 3};",
" return new Object[][] {",
" {\"1\", values[0], 1},",
" {\"2\", values[1], 2},",
" {\"3\", /* inline comment */ values[2], 3}",
" };",
" }",
"",
" @Test(dataProvider = \"dataProviderFieldReturnValueTestCases\")",
" public void dataProviderFieldReturnValue(int foo, int bar) {}",
"",
" @DataProvider",
" private Object[][] dataProviderFieldReturnValueTestCases() {",
" Object[][] foo = new Object[][] {{1, 2}};",
" return foo;",
" }",
"",
" @Test(dataProvider = \"dataProviderThrowsTestCases\")",
" public void dataProviderThrows(String foo, int bar) {}",
"",
" @DataProvider",
" private Object[][] dataProviderThrowsTestCases() throws RuntimeException {",
" return new Object[][] {",
" {\"1\", 0},",
" };",
" }",
"",
" @Test(expectedExceptions = RuntimeException.class)",
" public void singleExpectedException() {",
" throw new RuntimeException(\"foo\");",
" }",
"",
" @Test(expectedExceptions = {RuntimeException.class})",
" public void singleExpectedExceptionArray() {",
" throw new RuntimeException(\"foo\");",
" }",
"",
" @Test(expectedExceptions = {})",
" public void emptyExpectedExceptions() {}",
"",
" @Test(expectedExceptions = {IllegalArgumentException.class, RuntimeException.class})",
" public void multipleExpectedExceptions() {",
" throw new RuntimeException(\"foo\");",
" }",
"",
" @Test(enabled = false)",
" public void disabledTest() {}",
"",
" @Test(enabled = true)",
" public void enabledTest() {}",
"",
" @Test(groups = \"foo\")",
" public void groupsTest() {}",
"",
" @Test(groups = {\"foo\", \"bar\"})",
" public void multipleGroupsTest() {}",
"",
" @Test(groups = {})",
" public void emptyGroupsTest() {}",
"",
" @Test(groups = \"\")",
" public void invalidGroupsNameTest() {}",
"",
" @Test(groups = \" whitespace \")",
" public void whitespaceGroupsNameTest() {}",
"}")
.addOutputLines(
"A.java",
"import static java.util.concurrent.TimeUnit.MILLISECONDS;",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import java.util.stream.Stream;",
"import org.junit.jupiter.api.Disabled;",
"import org.junit.jupiter.api.DisplayName;",
"import org.junit.jupiter.api.MethodOrderer;",
"import org.junit.jupiter.api.Order;",
"import org.junit.jupiter.api.Tag;",
"import org.junit.jupiter.api.TestMethodOrder;",
"import org.junit.jupiter.api.Timeout;",
"import org.junit.jupiter.params.ParameterizedTest;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.junit.jupiter.params.provider.MethodSource;",
"import org.testng.annotations.AfterClass;",
"import org.testng.annotations.AfterMethod;",
"import org.testng.annotations.AfterTest;",
"import org.testng.annotations.BeforeClass;",
"import org.testng.annotations.BeforeMethod;",
"import org.testng.annotations.BeforeTest;",
"import org.testng.annotations.DataProvider;",
"import org.testng.annotations.Test;",
"",
"@TestMethodOrder(MethodOrderer.OrderAnnotation.class)",
"class A {",
" @org.junit.jupiter.api.BeforeEach",
" private void setupTest() {}",
"",
" @org.junit.jupiter.api.BeforeAll",
" private static void setupClass() {}",
"",
" @org.junit.jupiter.api.BeforeEach",
" private void setup() {}",
"",
" @org.junit.jupiter.api.AfterEach",
" private void teardownTest() {}",
"",
" @org.junit.jupiter.api.AfterAll",
" private static void teardownClass() {}",
"",
" @org.junit.jupiter.api.AfterEach",
" private void teardown() {}",
"",
" @org.junit.jupiter.api.Test",
" public void classLevelAnnotation() {}",
"",
" @Order(1)",
" @Timeout(value = 500, unit = MILLISECONDS)",
" @DisplayName(\"unit\")",
" @org.junit.jupiter.api.Test",
" public void priorityTimeOutAndDescription() {}",
"",
" // XXX: Attribute `testName` is not supported, value: `\"unsupportedAttribute\"`",
" @org.junit.jupiter.api.Test",
" public void unsupportedAttribute() {}",
"",
" // XXX: Attribute `testName` is not supported, value: `\"unsupportedAttribute\"`",
" // XXX: Attribute `suiteName` is not supported, value: `\"unsupportedAttribute\"`",
" @org.junit.jupiter.api.Test",
" public void multipleUnsupportedAttributes() {}",
"",
" @ParameterizedTest",
" @MethodSource(\"dataProviderTestCases\")",
" public void dataProvider(String string, int number) {}",
"",
" private static Stream<Arguments> dataProviderTestCases() {",
" int[] values = new int[] {1, 2, 3};",
" return Stream.of(",
" arguments(\"1\", values[0], A.class),",
" arguments(\"2\", values[1], A.class),",
" arguments(\"3\", /* inline comment */ values[2], A.class));",
" }",
"",
" // XXX: Attribute `dataProvider` is not supported, value:",
" // `\"dataProviderFieldReturnValueTestCases\"`",
"",
" public void dataProviderFieldReturnValue(int foo, int bar) {}",
"",
" @DataProvider",
" private Object[][] dataProviderFieldReturnValueTestCases() {",
" Object[][] foo = new Object[][] {{1, 2}};",
" return foo;",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"dataProviderThrowsTestCases\")",
" public void dataProviderThrows(String foo, int bar) {}",
"",
" private static Stream<Arguments> dataProviderThrowsTestCases() throws RuntimeException {",
" return Stream.of(arguments(\"1\", 0));",
" }",
"",
" @org.junit.jupiter.api.Test",
" public void singleExpectedException() {",
" org.junit.jupiter.api.Assertions.assertThrows(",
" RuntimeException.class,",
" () -> {",
" throw new RuntimeException(\"foo\");",
" });",
" }",
"",
" @org.junit.jupiter.api.Test",
" public void singleExpectedExceptionArray() {",
" org.junit.jupiter.api.Assertions.assertThrows(",
" RuntimeException.class,",
" () -> {",
" throw new RuntimeException(\"foo\");",
" });",
" }",
"",
" @org.junit.jupiter.api.Test",
" public void emptyExpectedExceptions() {}",
"",
" // XXX: Removed handling of `RuntimeException.class` because this migration doesn't support",
" // XXX: multiple expected exceptions.",
" @org.junit.jupiter.api.Test",
" public void multipleExpectedExceptions() {",
" org.junit.jupiter.api.Assertions.assertThrows(",
" IllegalArgumentException.class,",
" () -> {",
" throw new RuntimeException(\"foo\");",
" });",
" }",
"",
" @Disabled",
" @org.junit.jupiter.api.Test",
" public void disabledTest() {}",
"",
" @org.junit.jupiter.api.Test",
" public void enabledTest() {}",
"",
" @Tag(\"foo\")",
" @org.junit.jupiter.api.Test",
" public void groupsTest() {}",
"",
" @Tag(\"foo\")",
" @Tag(\"bar\")",
" @org.junit.jupiter.api.Test",
" public void multipleGroupsTest() {}",
"",
" @org.junit.jupiter.api.Test",
" public void emptyGroupsTest() {}",
"",
" // XXX: Attribute `groups` is not supported, value: `\"\"`",
" @org.junit.jupiter.api.Test",
" public void invalidGroupsNameTest() {}",
"",
" @Tag(\"whitespace\")",
" @org.junit.jupiter.api.Test",
" public void whitespaceGroupsNameTest() {}",
"}")
.doTest(TestMode.TEXT_MATCH);
}
@Test
void replacementMoreBehaviorPreserving() {
BugCheckerRefactoringTestHelper.newInstance(TestNGJUnitMigration.class, getClass())
.setArgs("-XepOpt:TestNGJUnitMigration:MinimalChanges=true")
.addInputLines(
"A.java",
"import org.testng.annotations.DataProvider;",
"import org.testng.annotations.Test;",
"",
"@Test",
"class A {",
" @Test(dataProvider = \"dataProviderTestCases\")",
" public void dataProvider(String string, int number) {}",
"",
" @DataProvider",
" private Object[][] dataProviderTestCases() {",
" int[] values = new int[] {1, 2, 3};",
" return new Object[][] {",
" {\"1\", values[0], 1},",
" {\"2\", values[1], 2},",
" {\"3\", /* inline comment */ values[2], 3}",
" };",
" }",
"",
" @Test(dataProvider = \"dataProviderFieldReturnValueTestCases\")",
" public void dataProviderFieldReturnValue(int foo, int bar) {}",
"",
" @DataProvider",
" private Object[][] dataProviderFieldReturnValueTestCases() {",
" Object[][] foo = new Object[][] {{1, 2}};",
" return foo;",
" }",
"",
" @Test(dataProvider = \"dataProviderThrowsTestCases\")",
" public void dataProviderThrows(String foo, int bar) {}",
"",
" @DataProvider",
" private Object[][] dataProviderThrowsTestCases() throws RuntimeException {",
" return new Object[][] {",
" {\"1\", 0},",
" };",
" }",
"",
" @Test(expectedExceptions = RuntimeException.class)",
" public void singleExpectedException() {",
" throw new RuntimeException(\"foo\");",
" }",
"",
" @Test(expectedExceptions = {RuntimeException.class})",
" public void singleExpectedExceptionArray() {",
" throw new RuntimeException(\"foo\");",
" }",
"",
" @Test(expectedExceptions = {})",
" public void emptyExpectedExceptions() {}",
"",
" @Test(expectedExceptions = {IllegalArgumentException.class, RuntimeException.class})",
" public void multipleExpectedExceptions() {",
" throw new RuntimeException(\"foo\");",
" }",
"}")
.addOutputLines(
"A.java",
"import static org.junit.jupiter.api.Assertions.assertThrows;",
"",
"import org.junit.jupiter.params.ParameterizedTest;",
"import org.junit.jupiter.params.provider.MethodSource;",
"import org.testng.annotations.DataProvider;",
"import org.testng.annotations.Test;",
"",
"class A {",
" @ParameterizedTest",
" @MethodSource(\"dataProviderTestCases\")",
" public void dataProvider(String string, int number) {}",
"",
" private static Object[][] dataProviderTestCases() {",
" int[] values = new int[] {1, 2, 3};",
" return new Object[][] {",
" {\"1\", values[0], 1},",
" {\"2\", values[1], 2},",
" {\"3\", /* inline comment */ values[2], 3}",
" };",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"dataProviderFieldReturnValueTestCases\")",
" public void dataProviderFieldReturnValue(int foo, int bar) {}",
"",
" private static Object[][] dataProviderFieldReturnValueTestCases() {",
" Object[][] foo = new Object[][] {{1, 2}};",
" return foo;",
" }",
"",
" @ParameterizedTest",
" @MethodSource(\"dataProviderThrowsTestCases\")",
" public void dataProviderThrows(String foo, int bar) {}",
"",
" private static Object[][] dataProviderThrowsTestCases() throws RuntimeException {",
" return new Object[][] {",
" {\"1\", 0},",
" };",
" }",
"",
" @org.junit.jupiter.api.Test",
" void testSingleExpectedException() {",
" assertThrows(RuntimeException.class, () -> singleExpectedException());",
" }",
"",
" public void singleExpectedException() {",
" throw new RuntimeException(\"foo\");",
" }",
"",
" @org.junit.jupiter.api.Test",
" void testSingleExpectedExceptionArray() {",
" assertThrows(RuntimeException.class, () -> singleExpectedExceptionArray());",
" }",
"",
" public void singleExpectedExceptionArray() {",
" throw new RuntimeException(\"foo\");",
" }",
"",
" // XXX: Attribute `expectedExceptions` is not supported, value: `{}`",
"",
" public void emptyExpectedExceptions() {}",
"",
" @org.junit.jupiter.api.Test",
" void testMultipleExpectedExceptions() {",
" assertThrows(IllegalArgumentException.class, () -> multipleExpectedExceptions());",
" }",
"",
" public void multipleExpectedExceptions() {",
" throw new RuntimeException(\"foo\");",
" }",
"}")
.addInputLines(
"B.java",
"import org.testng.annotations.Test;",
"",
"public class B {",
" @Test",
" public static void testEqualsMap() {",
" throw new RuntimeException(\"foo\");",
"}",
"}")
.addOutputLines(
"B.java",
"import org.testng.annotations.Test;",
"",
"public class B {",
" @org.junit.jupiter.api.Test",
" public static void testEqualsMap() {",
" throw new RuntimeException(\"foo\");",
"}",
"}")
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -0,0 +1,68 @@
package tech.picnic.errorprone.testngjunit;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import com.google.errorprone.BugPattern;
import com.google.errorprone.CompilationTestHelper;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.AnnotationTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.MethodTree;
import org.junit.jupiter.api.Test;
final class TestNGMatchersTest {
@Test
void matches() {
CompilationTestHelper.newInstance(TestNGMatchersTestChecker.class, getClass())
.addSourceLines(
"A.java",
"import org.testng.annotations.DataProvider;",
"import org.testng.annotations.Test;",
"",
"// BUG: Diagnostic contains: TestNG annotation",
"@Test",
"class A {",
" // BUG: Diagnostic contains: TestNG annotation",
" @Test",
" void basic() {}",
"",
" // BUG: Diagnostic contains: TestNG annotation",
" @Test(dataProvider = \"dataProviderTestCases\")",
" void withDataProvider() {}",
"",
" @DataProvider",
" // BUG: Diagnostic contains: TestNG value factory method",
" private static Object[][] dataProviderTestCases() {",
" return new Object[][] {};",
" }",
"",
" @org.junit.jupiter.api.Test",
" void junitTest() {}",
"}")
.doTest();
}
/** A {@link BugChecker} used to report TestNG annotations as errors for testing purposes. */
@BugPattern(summary = "Interacts with `TestNGMatchers` for testing purposes", severity = ERROR)
public static final class TestNGMatchersTestChecker extends BugChecker
implements MethodTreeMatcher, AnnotationTreeMatcher {
private static final long serialVersionUID = 1L;
@Override
public Description matchAnnotation(AnnotationTree annotationTree, VisitorState visitorState) {
return TestNGMatchers.TESTNG_TEST_ANNOTATION.matches(annotationTree, visitorState)
? buildDescription(annotationTree).setMessage("TestNG annotation").build()
: Description.NO_MATCH;
}
@Override
public Description matchMethod(MethodTree methodTree, VisitorState visitorState) {
return TestNGMatchers.TESTNG_VALUE_FACTORY_METHOD.matches(methodTree, visitorState)
? buildDescription(methodTree).setMessage("TestNG value factory method").build()
: Description.NO_MATCH;
}
}
}

View File

@@ -0,0 +1,276 @@
package tech.picnic.errorprone.testngjunit;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static java.util.function.Predicate.isEqual;
import static java.util.function.Predicate.not;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.BugPattern;
import com.google.errorprone.CompilationTestHelper;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import java.util.Map;
import java.util.Optional;
import javax.lang.model.element.Name;
import org.junit.jupiter.api.Test;
import tech.picnic.errorprone.testngjunit.TestNgMetadata.AnnotationMetadata;
final class TestNGScannerTest {
@Test
void classLevelAndMethodLevel() {
CompilationTestHelper.newInstance(TestChecker.class, getClass())
.addSourceLines(
"A.java",
"import org.testng.annotations.Test;",
"",
"// BUG: Diagnostic contains: Class: A attributes: {}",
"@Test",
"class A {",
"",
" public void inferClassLevelAnnotation() {}",
"",
" void packagePrivateNotATest() {}",
"",
" private void privateNotATest() {}",
"",
" static void notATest() {}",
"",
" public static void staticNotATest() {}",
"",
" // BUG: Diagnostic contains: Class: A attributes: {}",
" @Test",
" public void localAnnotation() {}",
"",
" // BUG: Diagnostic contains: Class: A attributes: {description=\"foo\"}",
" @Test(description = \"foo\")",
" public void singleArgument() {}",
"",
" // BUG: Diagnostic contains: Class: A attributes: {priority=1, description=\"foo\"}",
" @Test(priority = 1, description = \"foo\")",
" public void multipleArguments() {}",
"",
" // BUG: Diagnostic contains: Class: A attributes: {dataProvider=\"dataProviderTestCases\"}",
" @Test(dataProvider = \"dataProviderTestCases\")",
" public void dataProvider() {}",
"",
" @SuppressWarnings(\"onlyMatchTestNGAnnotations\")",
" // BUG: Diagnostic contains: Class: B attributes: {description=\"nested\"}",
" @Test(description = \"nested\")",
" class B {",
" public void nestedTest() {}",
"",
" // BUG: Diagnostic contains: Class: B attributes: {priority=1}",
" @Test(priority = 1)",
" public void nestedTestWithArguments() {}",
" }",
"}")
.doTest();
}
// XXX: Here we need to add some edge cases for the DataProvider probably?
@Test
void dataProvider() {
CompilationTestHelper.newInstance(TestChecker.class, getClass())
.addSourceLines(
"A.java",
"import java.util.stream.Stream;",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
" @DataProvider",
" // BUG: Diagnostic contains: Class: A DataProvider: dataProviderTestCases",
" private static Object[][] dataProviderTestCases() {",
" return new Object[][] {{1}, {2}};",
" }",
"",
" private static Object[][] notMigratableDataProviderTestCases() {",
" return Stream.of(1, 2, 3).map(i -> new Object[] {i}).toArray(Object[][]::new);",
" }",
"",
" private static Object[][] notMigratableDataProvider2TestCases() {",
" Object[][] testCases = new Object[][] {{1}, {2}};",
" return testCases;",
" }",
"",
" private static Object[] notMigratableDataProvider3TestCases() {",
" return new Object[] {new Object[] {1}, new Object[] {2}};",
" }",
"}")
.doTest();
}
@Test
void junitTestClass() {
CompilationTestHelper.newInstance(TestChecker.class, getClass())
.addSourceLines(
"A.java",
"import org.junit.jupiter.api.Test;",
"",
"class A {",
" @Test",
" private void foo() {}",
"}")
.doTest();
}
@Test
void normalClass() {
CompilationTestHelper.newInstance(TestChecker.class, getClass())
.addSourceLines("A.java", "class A {", " private void foo() {}", "}")
.doTest();
}
@Test
void teardownAndSetupMethods() {
CompilationTestHelper.newInstance(TestChecker.class, getClass())
.addSourceLines(
"A.java",
"import org.testng.annotations.AfterClass;",
"import org.testng.annotations.AfterMethod;",
"import org.testng.annotations.BeforeClass;",
"import org.testng.annotations.BeforeMethod;",
"",
"class A {",
" @BeforeClass",
" // BUG: Diagnostic contains: Class: A SetupTearDown: BEFORE_CLASS",
" private static void beforeClass() {}",
"",
" @BeforeMethod",
" // BUG: Diagnostic contains: Class: A SetupTearDown: BEFORE_METHOD",
" private void beforeMethod() {}",
"",
" @AfterClass",
" // BUG: Diagnostic contains: Class: A SetupTearDown: AFTER_CLASS",
" private static void afterClass() {}",
"",
" @AfterMethod",
" // BUG: Diagnostic contains: Class: A SetupTearDown: AFTER_METHOD",
" private void afterMethod() {}",
"}")
.doTest();
}
/**
* A {@link BugChecker} that flags classes with a diagnostics message that indicates, whether a
* TestNG element was collected.
*/
@BugPattern(severity = ERROR, summary = "Interacts with `TestNGScanner` for testing purposes")
public static final class TestChecker extends BugChecker
implements CompilationUnitTreeMatcher, ClassTreeMatcher, MethodTreeMatcher {
private static final long serialVersionUID = 1L;
// XXX: find better way to do this
private ImmutableMap<ClassTree, TestNgMetadata> classMetaData = ImmutableMap.of();
@Override
public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) {
TestNGScanner scanner = new TestNGScanner(state);
classMetaData = scanner.collectMetadataForClasses(tree);
return Description.NO_MATCH;
}
@Override
public Description matchClass(ClassTree tree, VisitorState state) {
Optional.ofNullable(classMetaData.get(tree))
.flatMap(TestNgMetadata::getClassLevelAnnotationMetadata)
.ifPresent(annotation -> reportAnnotationMessage(tree, annotation, state));
return Description.NO_MATCH;
}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
ClassTree classTree = state.findEnclosing(ClassTree.class);
Optional<TestNgMetadata> metadata = Optional.ofNullable(classTree).map(classMetaData::get);
if (metadata.isEmpty()) {
return Description.NO_MATCH;
}
reportClassLevelAnnotation(classTree, metadata.orElseThrow(), state);
reportTestMethods(tree, classTree, metadata.orElseThrow(), state);
reportDataProviderMethods(tree, classTree, metadata.orElseThrow(), state);
reportSetupTeardownMethods(tree, classTree, metadata.orElseThrow(), state);
return Description.NO_MATCH;
}
private void reportClassLevelAnnotation(
ClassTree classTree, TestNgMetadata metadata, VisitorState state) {
metadata
.getClassLevelAnnotationMetadata()
.ifPresent(annotation -> reportAnnotationMessage(classTree, annotation, state));
}
private void reportTestMethods(
MethodTree tree, ClassTree classTree, TestNgMetadata metadata, VisitorState state) {
metadata
.getClassLevelAnnotationMetadata()
.filter(not(isEqual(metadata.getMethodAnnotations().get(tree))))
.map(unused -> metadata.getMethodAnnotations().get(tree))
.ifPresent(annotation -> reportAnnotationMessage(classTree, annotation, state));
}
private void reportSetupTeardownMethods(
MethodTree tree, ClassTree classTree, TestNgMetadata metadata, VisitorState state) {
metadata.getSetupTeardown().entrySet().stream()
.filter(entry -> entry.getKey().equals(tree))
.findFirst()
.ifPresent(
entry ->
reportMethodMessage(
classTree.getSimpleName(),
"SetupTearDown",
entry.getValue().name(),
entry.getKey(),
state));
}
private void reportDataProviderMethods(
MethodTree tree, ClassTree classTree, TestNgMetadata metadata, VisitorState state) {
metadata.getDataProviderMetadata().entrySet().stream()
.filter(entry -> entry.getValue().getMethodTree().equals(tree))
.findFirst()
.map(Map.Entry::getValue)
.ifPresent(
dataProvider -> {
reportMethodMessage(
classTree.getSimpleName(),
"DataProvider",
dataProvider.getName(),
dataProvider.getMethodTree(),
state);
});
}
private void reportAnnotationMessage(
ClassTree classTree, AnnotationMetadata annotation, VisitorState state) {
state.reportMatch(
buildDescription(annotation.getAnnotationTree())
.setMessage(createMetaDataMessage(classTree, annotation))
.build());
}
private void reportMethodMessage(
Name className, String message, String name, Tree tree, VisitorState state) {
state.reportMatch(
buildDescription(tree)
.setMessage(String.format("Class: %s %s: %s", className, message, name))
.build());
}
private static String createMetaDataMessage(
ClassTree classTree, AnnotationMetadata annotationMetadata) {
return String.format(
"Class: %s attributes: %s",
classTree.getSimpleName(), annotationMetadata.getAttributes());
}
}
}