Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate Hamcrest to JUnit 5 #343

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4b2e427
WIP: Add recipe for migration from Hamcrest
matusmatokpt May 22, 2023
e8ffbae
Merge branch 'openrewrite:main' into main
matusmatokpt May 22, 2023
0bb2ffa
Merge branch 'openrewrite:main' into main
matusmatokpt May 23, 2023
0947005
Add missing license headers
timtebeek May 23, 2023
11d9fd2
Resolve some of the test issues
timtebeek May 23, 2023
cc4a929
Fix test import
timtebeek May 23, 2023
bc1d5c8
Add proto implementation for assertEquals
matusmatokpt May 24, 2023
e17fc29
Use static import and #{any(java.lang.Object)} to fix test
timtebeek May 24, 2023
76724ca
Merge branch 'main' into main
matusmatokpt Jun 13, 2023
e360972
Adapt to main
matusmatokpt Jun 13, 2023
66473e8
Add more simple matcher-to-method translations
matusmatokpt Jun 15, 2023
cc581eb
Add more simple matcher-to-method translations
matusmatokpt Jun 22, 2023
0eb3887
Add tests
matusmatokpt Jun 27, 2023
7d11579
Finalise the pull request
matusmatokpt Jun 29, 2023
1730fbf
Merge branch 'main' into main
matusmatokpt Jun 29, 2023
3525302
Merge branch 'main' into main
timtebeek Jun 30, 2023
9357f7a
Add required license header
timtebeek Jun 30, 2023
9e23d71
Move classes to align with the Hamcrest to AssertJ implementation
timtebeek Jun 30, 2023
d75e50f
Consistently use `class Test` to avoid conflicts with `@Test`
timtebeek Jun 30, 2023
21d4728
Refactored and split HamcrestMatcherToJUnit5 recipe
matusmatokpt Aug 2, 2023
5395d63
Merge branch 'main' into main
matusmatokpt Aug 2, 2023
ff0e775
Add license headers
matusmatokpt Aug 2, 2023
483fcb4
Merge branch 'main' into main
timtebeek Nov 21, 2023
26a8646
Merge branch 'main' into main
timtebeek Feb 5, 2024
93448e3
Apply suggestions from code review
timtebeek Jun 15, 2024
be54077
Merge branch 'main' into main
timtebeek Jun 15, 2024
be57cfe
Apply suggestions from code review
timtebeek Jun 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,375 @@
/*
* Copyright 2023 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.testing.junit5;
timtebeek marked this conversation as resolved.
Show resolved Hide resolved

import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.MethodMatcher;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;

import java.util.ArrayList;
import java.util.List;

public class MigrateFromHamcrest extends Recipe {
@Override
public String getDisplayName() {
return "Migrate from Hamcrest Matchers to JUnit5";
}

@Override
public String getDescription() {
return "This recipe will migrate all Hamcrest Matchers to JUnit5 assertions.";
}

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

private static class MigrationFromHamcrestVisitor extends JavaIsoVisitor<ExecutionContext> {

@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext executionContext) {
System.out.println("RECIPE RUN");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
System.out.println("RECIPE RUN");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all of these console prints are just to help to debug the non-working tests. They will of course disappear once I make the tests work.

J.MethodInvocation mi = super.visitMethodInvocation(method, executionContext);
MethodMatcher matcherAssertTrue = new MethodMatcher("org.hamcrest.MatchAssert assertThat(String, boolean)");
MethodMatcher matcherAssertMatcher = new MethodMatcher("org.hamcrest.MatcherAssert assertThat(..)");
MethodMatcher matcherAssertMatcherWithReason = new MethodMatcher("org.hamcrest.MatcherAssert assertThat(String,*,org.hamcrest.Matcher)");

if (matcherAssertTrue.matches(mi)) {
//TODO simple
} else if (matcherAssertMatcher.matches(mi)) {
Expression hamcrestMatcher = mi.getArguments().get(1);
if (hamcrestMatcher instanceof J.MethodInvocation) {
System.out.println("matched");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
System.out.println("matched");

J.MethodInvocation matcherInvocation = (J.MethodInvocation)hamcrestMatcher;
maybeRemoveImport("org.hamcrest.Matchers." + matcherInvocation.getSimpleName());
maybeRemoveImport("org.hamcrest.MatcherAssert.assertThat");
String targetAssertion = getTranslatedAssert(matcherInvocation, false);
if (targetAssertion.equals("")) {
return mi;
}

JavaTemplate template = JavaTemplate.builder(getTemplateForMatcher(matcherInvocation,null, false))
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(executionContext, "junit-jupiter-api-5.9"))
.staticImports("org.junit.jupiter.api.Assertions." + targetAssertion)
.build();

maybeAddImport("org.junit.jupiter.api.Assertions", targetAssertion);
return template.apply(getCursor(), method.getCoordinates().replace(),
getArgumentsForTemplate(matcherInvocation, null, mi.getArguments().get(0), false));
}
else throw new IllegalArgumentException("Parameter mismatch for " + mi + ".");
}
System.out.println("FINISH RUN");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
System.out.println("FINISH RUN");

return mi;
}

private String getTranslatedAssert(J.MethodInvocation methodInvocation, boolean negated) {
//to be replaced with a static map
switch (methodInvocation.getSimpleName()) {
case "equalTo":
case "emptyArray":
case "equalToIgnoringCase":
case "hasEntry":
case "hasSize":
case "hasToString":
return negated ? "assertNotEquals" : "assertEquals";
case "closeTo":
case "containsString":
case "empty":
case "emptyCollectionOf":
case "emptyIterable":
case "emptyIterableOf":
case "endsWith":
case "greaterThan":
case "greaterThanOrEqualTo":
case "hasKey":
case "hasValue":
case "lessThan":
case "lessThanOrEqualTo":
case "sameInstance":
case "startsWith":
case "theInstance":
case "isCompatibleWith":
return negated ? "assertFalse" : "assertTrue";
case "instanceOf":
case "isA":
return negated ? "assertFalse" : "assertInstanceOf";
case "is":
if (methodInvocation.getArguments().get(0).toString().startsWith("org.hamcrest")) {
if (methodInvocation.getArguments().get(0) instanceof J.MethodInvocation) {
return getTranslatedAssert((J.MethodInvocation)methodInvocation.getArguments().get(0), negated);
} else {
throw new IllegalArgumentException();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of throwing an exception we prefer not to make a change; would that be possible here?
Otherwise we trip up for instance a complete Spring Boot 3.1 migration, if that somewhere down to line includes the Hamcrest migration as part of the JUnit 4 to 5 migration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of course, I will change it to that behavior

}
} else {
return negated ? "assertNotEquals" : "assertEquals";
}
case "not":
if (methodInvocation.getArguments().get(0).toString().startsWith("org.hamcrest")) {
if (methodInvocation.getArguments().get(0) instanceof J.MethodInvocation) {
return getTranslatedAssert((J.MethodInvocation)methodInvocation.getArguments().get(0), !negated);
} else {
throw new IllegalArgumentException();
}
} else {
return negated ? "assertEquals" : "assertNotEquals";
}
case "notNullValue":
return negated ? "assertNull" : "assertNotNull";
case "nullValue":
return negated ? "assertNotNull" : "assertNull";
}
return "";
}

private String getTemplateForMatcher(J.MethodInvocation matcher, Expression errorMsg, boolean negated) {
StringBuilder sb = new StringBuilder();
sb.append(getTranslatedAssert(matcher, negated));

//to be replaced with a static map
switch (matcher.getSimpleName()) {
case "equalTo":
sb.append("(#{any(java.lang.Object)}, #{any(java.lang.Object)}");
break;
case "closeTo":
sb.append("(Math.abs(#{any(java.lang.Object)} - #{any(java.lang.Object)}) < #{any(java.lang.Object)}");
break;
case "containsString":
sb.append("(#{any(java.lang.Object)}.contains(#{any(java.lang.Object)}");
break;
case "empty":
sb.append("(#{any(java.lang.Object)}.isEmpty()");
break;
case "emptyArray":
sb.append("(0, #{any(java.lang.Object)}.length");
break;
case "emptyCollectionOf":
sb.append("(#{any(java.lang.Object)}.isEmpty() && ");
sb.append("#{any(java.lang.Object)}.isAssignableFrom(#{any(java.lang.Object)}.getClass())");
break;
case "emptyIterable":
sb.append("(#{any(java.lang.Object)}.iterator().hasNext()");
break;
case "emptyIterableOf":
sb.append("(#{any(java.lang.Object)}.iterator().hasNext() && ");
sb.append("#{any(java.lang.Object)}.isAssignableFrom(#{any(java.lang.Object)}.getClass())");
break;
case "endsWith":
sb.append("(#{any(java.lang.Object)}.substring(Math.abs(#{any(java.lang.Object)}.length() - #{any(java.lang.Object)}.length()))");
sb.append(".equals(#{any(java.lang.Object)})");
break;
case "equalToIgnoringCase":
sb.append("(#{any(java.lang.String)}.toLowerCase(), (#{any(java.lang.String)}.toLowerCase()");
break;
case "greaterThan":
sb.append("(#{any(java.lang.Object)} > #{any(java.lang.Object)}");
break;
case "greaterThanOrEqualTo":
sb.append("(#{any(java.lang.Object)} >= #{any(java.lang.Object)}");
break;
case "hasEntry":
if (matcher.getArguments().get(0).getType().toString().startsWith("org.hamcrest")) {
return "";
}
sb.append("(#{any(java.lang.Object)}, #{any(java.lang.Object)}.get(#{any(java.lang.Object)})");
break;
case "hasKey":
if (matcher.getArguments().get(0).getType().toString().startsWith("org.hamcrest")) {
return "";
}
sb.append("(#{any(java.lang.Object)}.containsKey(#{any(java.lang.Object)})");
break;
case "hasSize":
if (matcher.getArguments().get(0).getType().toString().startsWith("org.hamcrest")) {
return "";
}
sb.append("(#{any(java.lang.Object)}.size(), #{any(java.lang.Object)}");
break;
case "hasToString":
if (matcher.getArguments().get(0).getType().toString().startsWith("org.hamcrest")) {
return "";
}
sb.append("(#{any(java.lang.Object)}.toString(), #{any(java.lang.Object)}");
break;
case "hasValue":
if (matcher.getArguments().get(0).getType().toString().startsWith("org.hamcrest")) {
return "";
}
sb.append("(#{any(java.lang.Object)}.containsValue(#{any(java.lang.Object)})");
break;
case "instanceOf":
case "isA":
if (negated) {
sb.append("(#{any(java.lang.Object)} instanceof #{any(java.lang.Object)}");
} else {
sb.append("(#{any(java.lang.Object)}, #{any(java.lang.Object)}");
}
break;
case "is":
if (matcher.getArguments().get(0).toString().startsWith("org.hamcrest")) {
if (matcher.getArguments().get(0) instanceof J.MethodInvocation) {
return getTemplateForMatcher((J.MethodInvocation) matcher.getArguments().get(0), errorMsg, negated);
} else {
throw new IllegalArgumentException();
}
} else {
sb.append("(#{any(java.lang.Object)}, #{any(java.lang.Object)}");
}
break;
case "lessThan":
sb.append("(#{any(java.lang.Object)} < #{any(java.lang.Object)}");
break;
case "lessThanOrEqualTo":
sb.append("(#{any(java.lang.Object)} <= #{any(java.lang.Object)}");
break;
case "not":
if (matcher.getArguments().get(0).toString().startsWith("org.hamcrest")) {
if (matcher.getArguments().get(0) instanceof J.MethodInvocation) {
return getTemplateForMatcher((J.MethodInvocation) matcher.getArguments().get(0), errorMsg, !negated);
} else {
throw new IllegalArgumentException();
}
} else {
sb.append("(#{any(java.lang.Object)}, #{any(java.lang.Object)}");
}
break;
case "notNullValue":
case "nullValue":
sb.append("(#{any(java.lang.Object)}");
break;
case "sameInstance":
case "theInstance":
sb.append("(#{any(java.lang.Object)} == #{any(java.lang.Object)}");
break;
case "startsWith":
sb.append("(#{any(java.lang.String)}.startsWith(#{any(java.lang.Object)})");
break;
case "isCompatibleWith":
sb.append("(#{any(java.lang.String)}.isAssignableFrom(#{any(java.lang.String)}.getClass())");
break;
default:
return "";
}

if (errorMsg != null) {
sb.append(", #{any(java.lang.String)})");
} else {
sb.append(")");
}
return sb.toString();
}

private Object[] getArgumentsForTemplate(J.MethodInvocation matcher, Expression errorMsg, Expression examinedObj, boolean negated) {
List<Expression> result = new ArrayList<>();

switch (matcher.getSimpleName()) {
case "equalTo":
case "closeTo":
case "containsString":
case "equalToIgnoringCase":
case "greaterThan":
case "greaterThanOrEqualTo":
case "hasKey":
case "hasSize":
case "hasToString":
case "hasValue":
case "lessThan":
case "lessThanOrEqualTo":
case "sameInstance":
case "startsWith":
case "theInstance":
result.add(examinedObj);
result.addAll(matcher.getArguments());
break;
case "empty":
case "emptyArray":
case "emptyIterable":
case "notNullValue":
case "nullValue":
result.add(examinedObj);
break;
case "emptyCollectionOf":
case "emptyIterableOf":
result.add(examinedObj);
result.add(matcher.getArguments().get(0));
result.add(examinedObj);
break;
case "endsWith":
result.add(examinedObj);
result.add(examinedObj);
result.add(matcher.getArguments().get(0));
result.add(matcher.getArguments().get(0));
break;
case "hasEntry":
result.add(matcher.getArguments().get(1));
result.add(examinedObj);
result.add(matcher.getArguments().get(0));
break;
case "instanceOf":
case "isA":
if (negated) {
result.add(examinedObj);
result.add(matcher.getArguments().get(0));
} else {
result.add(matcher.getArguments().get(0));
result.add(examinedObj);
}
break;
case "is":
if (matcher.getArguments().get(0).toString().startsWith("org.hamcrest")) {
if (matcher.getArguments().get(0) instanceof J.MethodInvocation) {
return getArgumentsForTemplate((J.MethodInvocation) matcher.getArguments().get(0), errorMsg, examinedObj, negated);
} else {
throw new IllegalArgumentException();
}
} else {
result.add(examinedObj);
result.addAll(matcher.getArguments());
}
break;
case "not":
if (matcher.getArguments().get(0).toString().startsWith("org.hamcrest")) {
if (matcher.getArguments().get(0) instanceof J.MethodInvocation) {
return getArgumentsForTemplate((J.MethodInvocation) matcher.getArguments().get(0), errorMsg, examinedObj, !negated);
} else {
throw new IllegalArgumentException();
}
} else {
result.add(examinedObj);
result.addAll(matcher.getArguments());
}
break;
case "isCompatibleWith":
result.add(matcher.getArguments().get(0));
result.add(examinedObj);
break;
default:
return new Object[]{};
}

if (errorMsg != null) result.add(errorMsg);

return result.toArray();
}
}
}
Loading