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 24 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,62 @@
/*
* 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.hamcrest;

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;

public class AssertThatBooleanToJUnit5 extends Recipe {
@Override
public String getDisplayName() {
return "Migrate Hamcrest `assertThat(boolean, Matcher)` to AssertJ";
}

@Override
public String getDescription() {
return "Replace Hamcrest `assertThat(String, boolean)` with AssertJ `assertThat(boolean).as(String).isTrue()`.";
}

private static final MethodMatcher ASSERT_THAT_MATCHER = new MethodMatcher("org.hamcrest.MatcherAssert assertThat(String, boolean)");

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation mi = super.visitMethodInvocation(method, ctx);
if (ASSERT_THAT_MATCHER.matches(mi)) {
Expression reasonArgument = mi.getArguments().get(0);
Expression booleanArgument = mi.getArguments().get(1);
maybeAddImport("org.junit.jupiter.api.Assertions", "assertTrue");
maybeRemoveImport("org.hamcrest.MatcherAssert.assertThat");
return JavaTemplate.builder("assertTrue(#{any(boolean)}, #{any(String)})")
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "junit-jupiter-api-5.9"))
.staticImports("org.junit.jupiter.api.Assertions.assertTrue")
.build()
.apply(getCursor(), mi.getCoordinates().replace(), booleanArgument, reasonArgument);
}
return mi;
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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.hamcrest;

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 HamcrestInstanceOfToJUnit5 extends Recipe {
@Override
public String getDisplayName() {
return "Migrate from Hamcrest instanceOf matcher to JUnit5";
}

@Override
public String getDescription() {
return "Migrate from Hamcrest instanceOf and isA matcher to JUnit5 assertInstanceOf assertion.";
}


static final MethodMatcher INSTANCE_OF_MATCHER = new MethodMatcher("org.hamcrest.Matchers instanceOf(..)");
static final MethodMatcher IS_A_MATCHER = new MethodMatcher("org.hamcrest.Matchers isA(..)");
static final MethodMatcher ASSERT_THAT_MATCHER = new MethodMatcher("org.hamcrest.MatcherAssert assertThat(.., org.hamcrest.Matcher)");
static final RemoveNotMatcher REMOVE_NOT_MATCHER_RECIPE= new RemoveNotMatcher();

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>(){
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation mi, ExecutionContext ctx) {
if (ASSERT_THAT_MATCHER.matches(mi)) {
Expression reason;
Expression examinedObject;
Expression hamcrestMatcher;

if (mi.getArguments().size() == 2) {
reason = null;
examinedObject = mi.getArguments().get(0);
hamcrestMatcher = mi.getArguments().get(1);
} else if (mi.getArguments().size() == 3) {
reason = mi.getArguments().get(0);
examinedObject = mi.getArguments().get(1);
hamcrestMatcher = mi.getArguments().get(2);
} else return mi;

J.MethodInvocation matcherInvocation = (J.MethodInvocation) hamcrestMatcher;
while (matcherInvocation.getSimpleName().equals("not")) {
maybeRemoveImport("org.hamcrest.Matchers.not");
maybeRemoveImport("org.hamcrest.CoreMatchers.not");
matcherInvocation = (J.MethodInvocation) REMOVE_NOT_MATCHER_RECIPE.getVisitor().visit(matcherInvocation, ctx);
}

if (INSTANCE_OF_MATCHER.matches(matcherInvocation) || IS_A_MATCHER.matches(matcherInvocation)) {
boolean logicalContext = REMOVE_NOT_MATCHER_RECIPE.getLogicalContext(matcherInvocation, ctx);

String templateString = (logicalContext
? "assertInstanceOf(#{any(java.lang.Class)}, #{any(java.lang.Object)}"
: "assertFalse(#{any(java.lang.Class)}.isAssignableFrom(#{any(java.lang.Object)}.getClass())")
+ (reason == null ? ")" : ", #{any(java.lang.String)})");

JavaTemplate template = JavaTemplate.builder(templateString)
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "junit-jupiter-api-5.9"))
.staticImports("org.junit.jupiter.api.Assertions." + (logicalContext ? "assertInstanceOf" : "assertFalse"))
.build();

maybeRemoveImport("org.hamcrest.MatcherAssert.assertThat");
maybeRemoveImport("org.hamcrest.Matchers.instanceOf");
maybeRemoveImport("org.hamcrest.CoreMatchers.instanceOf");
maybeRemoveImport("org.hamcrest.Matchers.isA");
maybeRemoveImport("org.hamcrest.CoreMatchers.isA");
maybeAddImport("org.junit.jupiter.api.Assertions", logicalContext ? "assertInstanceOf" : "assertFalse");

List<Expression> arguments = new ArrayList<>();
arguments.add(matcherInvocation.getArguments().get(0));
arguments.add(examinedObject);
if (reason != null) {
arguments.add(reason);
}

return template.apply(getCursor(), mi.getCoordinates().replace(), arguments.toArray());
}
}
return super.visitMethodInvocation(mi, ctx);
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* 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.hamcrest;

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 javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;

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

@Override
public String getDescription() {
return "Migrate from Hamcrest `Matcher` to JUnit5 assertions.";
}

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

enum Replacement {
EQUALTO("equalTo", "assertEquals", "assertNotEquals", "#{any(java.lang.Object)}, #{any(java.lang.Object)}", "examinedObjThenMatcherArgs"),
EMPTYARRAY("emptyArray", "assertEquals", "assertNotEquals", "0, #{anyArray(java.lang.Object)}.length", "examinedObjOnly"),
HASENTRY("hasEntry", "assertEquals", "assertNotEquals", "#{any(java.lang.Object)}, #{any(java.util.Map)}.get(#{any(java.lang.Object)})", "matcher1ExaminedObjMatcher0"),
HASSIZE("hasSize", "assertEquals", "assertNotEquals", "#{any(java.util.Collection)}.size(), #{any(double)}", "examinedObjThenMatcherArgs"),
HASTOSTRING("hasToString", "assertEquals", "assertNotEquals", "#{any(java.lang.Object)}.toString(), #{any(java.lang.String)}", "examinedObjThenMatcherArgs"),
CLOSETO("closeTo", "assertTrue", "assertFalse", "Math.abs(#{any(double)} - #{any(double)}) < #{any(double)}", "examinedObjThenMatcherArgs"),
CONTAINSSTRING("containsString", "assertTrue", "assertFalse", "#{any(java.lang.String)}.contains(#{any(java.lang.String)}", "examinedObjThenMatcherArgs"),
EMPTY("empty", "assertTrue", "assertFalse", "#{any(java.util.Collection)}.isEmpty()", "examinedObjOnly"),
ENDSWITH("endsWith", "assertTrue", "assertFalse", "#{any(java.lang.String)}.endsWith(#{any(java.lang.String)})", "examinedObjThenMatcherArgs"),
EQUALTOIGNORINGCASE("equalToIgnoringCase", "assertTrue", "assertFalse", "#{any(java.lang.String)}.equalsIgnoreCase(#{any(java.lang.String)})", "examinedObjThenMatcherArgs"),
GREATERTHAN("greaterThan", "assertTrue", "assertFalse", "#{any(double)} > #{any(double)}", "examinedObjThenMatcherArgs"),
GREATERTHANOREQUALTO("greaterThanOrEqualTo", "assertTrue", "assertFalse", "#{any(double)} >= #{any(double)}", "examinedObjThenMatcherArgs"),
HASKEY("hasKey", "assertTrue", "assertFalse", "#{any(java.util.Map)}.containsKey(#{any(java.lang.Object)})", "examinedObjThenMatcherArgs"),
HASVALUE("hasValue", "assertTrue", "assertFalse", "#{any(java.util.Map)}.containsValue(#{any(java.lang.Object)})", "examinedObjThenMatcherArgs"),
LESSTHAN("lessThan", "assertTrue", "assertFalse", "#{any(double)} < #{any(double)}", "examinedObjThenMatcherArgs"),
LESSTHANOREQUALTO("lessThanOrEqualTo", "assertTrue", "assertFalse", "#{any(double)} <= #{any(double)}", "examinedObjThenMatcherArgs"),
STARTSWITH("startsWith", "assertTrue", "assertFalse", "#{any(java.lang.String)}.startsWith(#{any(java.lang.String)})", "examinedObjThenMatcherArgs"),
TYPECOMPATIBLEWITH("typeCompatibleWith", "assertTrue", "assertFalse", "#{any(java.lang.Class)}.isAssignableFrom(#{any(java.lang.Class)})", "matcherArgsThenExaminedObj"),
NOTNULLVALUE("notNullValue", "assertNotNull", "assertNull", "#{any(java.lang.Object)}", "examinedObjOnly"),
NULLVALUE("nullValue", "assertNull", "assertNotNull", "#{any(java.lang.Object)}", "examinedObjOnly"),
SAMEINSTANCE("sameInstance", "assertSame", "assertNotSame", "#{any(java.lang.Object)}, #{any(java.lang.Object)}", "examinedObjThenMatcherArgs"),
THEINSTANCE("theInstance", "assertSame", "assertNotSame", "#{any(java.lang.Object)}, #{any(java.lang.Object)}", "examinedObjThenMatcherArgs"),
EMPTYITERABLE("emptyIterable", "assertFalse", "assertTrue", "#{any(java.lang.Iterable)}.iterator().hasNext()", "examinedObjOnly")
;

final String hamcrest, junitPositive, junitNegative, template;
final String argumentsMethod;

private static final Map<String, BiFunction<Expression, J.MethodInvocation, List<Expression>>> methods = new HashMap<>();
static {
methods.put("examinedObjThenMatcherArgs", (ex, matcher) -> {
List<Expression> arguments = matcher.getArguments();
arguments.add(0, ex);
return arguments;
});
methods.put("matcherArgsThenExaminedObj", (ex, matcher) -> {
List<Expression> arguments = matcher.getArguments();
arguments.add(ex);
return arguments;
});
methods.put("examinedObjOnly",(ex, matcher) -> {
List<Expression> arguments = new ArrayList<>();
arguments.add(ex);
return arguments;
});
methods.put("matcher1ExaminedObjMatcher0", (ex, matcher) -> {
List<Expression> arguments = new ArrayList<>();
arguments.add(matcher.getArguments().get(1));
arguments.add(ex);
arguments.add(matcher.getArguments().get(0));
return arguments;
});
}

Replacement(String hamcrest, String junitPositive, String junitNegative, String template, String argumentsMethod) {
this.hamcrest = hamcrest;
this.junitPositive = junitPositive;
this.junitNegative = junitNegative;
this.template = template;
this.argumentsMethod = argumentsMethod;
}
}

private static RemoveNotMatcher removeNotRecipe = new RemoveNotMatcher();

private static class MigrationFromHamcrestVisitor extends JavaIsoVisitor<ExecutionContext> {

@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext executionContext) {
J.MethodInvocation mi = super.visitMethodInvocation(method, executionContext);
timtebeek marked this conversation as resolved.
Show resolved Hide resolved
MethodMatcher matcherAssertMatcher = new MethodMatcher("org.hamcrest.MatcherAssert assertThat(.., org.hamcrest.Matcher)");

if (matcherAssertMatcher.matches(mi)) {
Expression reason;
Expression examinedObject;
Expression hamcrestMatcher;

if (mi.getArguments().size() == 2) {
reason = null;
examinedObject = mi.getArguments().get(0);
hamcrestMatcher = mi.getArguments().get(1);
} else if (mi.getArguments().size() == 3) {
reason = mi.getArguments().get(0);
examinedObject = mi.getArguments().get(1);
hamcrestMatcher = mi.getArguments().get(2);
} else return mi;

if (hamcrestMatcher instanceof J.MethodInvocation) {
J.MethodInvocation matcherInvocation = (J.MethodInvocation) hamcrestMatcher;
maybeRemoveImport("org.hamcrest.MatcherAssert.assertThat");

while (matcherInvocation.getSimpleName().equals("not")) {
maybeRemoveImport("org.hamcrest.Matchers.not");
maybeRemoveImport("org.hamcrest.CoreMatchers.not");
matcherInvocation = (J.MethodInvocation) removeNotRecipe.getVisitor().visit(matcherInvocation, executionContext);
timtebeek marked this conversation as resolved.
Show resolved Hide resolved
}

//we do not handle nested matchers
if (!(matcherInvocation.getArguments().get(0) instanceof J.Empty)) {
if ((matcherInvocation.getArguments().get(0).getType()).toString().startsWith("org.hamcrest")) {
return mi;
}
}

boolean logicalContext = removeNotRecipe.getLogicalContext(matcherInvocation, executionContext);
timtebeek marked this conversation as resolved.
Show resolved Hide resolved

Replacement replacement;
try {
replacement = Replacement.valueOf(matcherInvocation.getSimpleName().toUpperCase());
} catch (IllegalArgumentException e) {
return mi;
}
String assertion = logicalContext ? replacement.junitPositive : replacement.junitNegative;

String templateString =
assertion
+ "("
+ replacement.template
+ (reason == null ? ")" : ", #{any(java.lang.String)})");

JavaTemplate template = JavaTemplate.builder(templateString)
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(executionContext, "junit-jupiter-api-5.9"))
timtebeek marked this conversation as resolved.
Show resolved Hide resolved
.staticImports("org.junit.jupiter.api.Assertions." + assertion)
.build();

maybeRemoveImport("org.hamcrest.Matchers." + replacement.hamcrest);
maybeRemoveImport("org.hamcrest.CoreMatchers." + replacement.hamcrest);
maybeAddImport("org.junit.jupiter.api.Assertions", assertion);

List<Expression> arguments = Replacement.methods.get(replacement.argumentsMethod).apply(examinedObject, matcherInvocation);
if (reason != null) {
arguments.add(reason);
}

return template.apply(getCursor(), method.getCoordinates().replace(), arguments.toArray());
}
}
return mi;
}
}
}
Loading