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

1 add recipe to migrate dataprovider annotation #10

Merged
merged 12 commits into from
Apr 26, 2024
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@
<scope>compile</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<dependencyManagement>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package io.github.mboegers.openrewrite.testngtojupiter;

import lombok.EqualsAndHashCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package io.github.mboegers.openrewrite.testngtojupiter;

import io.github.mboegers.openrewrite.testngtojupiter.helper.AnnotationArguments;
import io.github.mboegers.openrewrite.testngtojupiter.helper.FindAnnotatedMethods;
import io.github.mboegers.openrewrite.testngtojupiter.helper.FindAnnotation;
import io.github.mboegers.openrewrite.testngtojupiter.helper.UsesAnnotation;
import org.openrewrite.*;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.java.*;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaCoordinates;
import org.openrewrite.java.tree.JavaType;

import java.util.Comparator;
import java.util.Optional;
import java.util.Set;

import static java.util.Objects.requireNonNull;

public class MigrateDataProvider extends Recipe {

private static final String DATA_PROVIDER = "org.testng.annotations.DataProvider";
private static final AnnotationMatcher DATA_PROVIDER_MATCHER = new AnnotationMatcher("@" + DATA_PROVIDER);

@Override
public String getDisplayName() {
return "Migrate @DataProvider utilities";
}

@Override
public String getDescription() {
return "Wrap `@DataProvider` methods into a Jupiter parameterized test with MethodSource.";
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(new UsesAnnotation<>(DATA_PROVIDER_MATCHER), new TreeVisitor<>() {
@Override
public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext executionContext, Cursor parent) {
tree = super.visit(tree, executionContext, parent);
// wrap methods
tree = new WrapDataProviderMethod().visit(tree, executionContext, parent);
// remove @DataProvider
tree = new RemoveAnnotationVisitor(DATA_PROVIDER_MATCHER).visit(tree, executionContext, parent);
// use @MethodeSource and @ParameterizedTest
MBoegers marked this conversation as resolved.
Show resolved Hide resolved
tree = new UseParameterizedTest().visit(tree, executionContext, parent);
MBoegers marked this conversation as resolved.
Show resolved Hide resolved
MBoegers marked this conversation as resolved.
Show resolved Hide resolved
MBoegers marked this conversation as resolved.
Show resolved Hide resolved
// remove dataProviderName and dataProviderClass arguments
tree = new RemoveAnnotationAttribute("org.testng.annotations.Test", "dataProvider")
.getVisitor().visit(tree, executionContext);
tree = new RemoveAnnotationAttribute("org.testng.annotations.Test", "dataProviderName")
.getVisitor().visit(tree, executionContext);
return tree;
}
});
}

private class WrapDataProviderMethod extends JavaIsoVisitor<ExecutionContext> {

private static final JavaTemplate methodeSourceTemplate = JavaTemplate.builder("""
public static Stream<Arguments> #{}() {
return Arrays.stream(#{}()).map(Arguments::of);
}
""")
.imports("org.junit.jupiter.params.provider.Arguments", "java.util.Arrays", "java.util.stream.Stream")
.contextSensitive()
.javaParser(JavaParser.fromJavaVersion()
.logCompilationWarningsAndErrors(true)
.classpath("junit-jupiter-api", "junit-jupiter-params", "testng"))
.build();

@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, org.openrewrite.ExecutionContext ctx) {
classDecl = super.visitClassDeclaration(classDecl, ctx);

Set<J.MethodDeclaration> dataProviders = FindAnnotatedMethods.find(classDecl, DATA_PROVIDER_MATCHER);

// for each add a Wrapper that translates to Jupiter method source
for (J.MethodDeclaration provider : dataProviders) {
String providerMethodName = provider.getSimpleName();
String providerName = FindAnnotation.find(provider, DATA_PROVIDER_MATCHER).stream().findAny()
.flatMap(j -> AnnotationArguments.extractLiteral(j, "name", String.class))
.orElse(providerMethodName);

classDecl = classDecl.withBody(methodeSourceTemplate.apply(
new Cursor(getCursor(), classDecl.getBody()), classDecl.getBody().getCoordinates().lastStatement(),
providerName, providerMethodName));
}

// add new imports
maybeAddImport("org.junit.jupiter.params.provider.Arguments");
maybeAddImport("java.util.Arrays");
maybeAddImport("java.util.stream.Stream");

return classDecl;
}
}

private class UseParameterizedTest extends JavaIsoVisitor<ExecutionContext> {
@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext executionContext) {
method = super.visitMethodDeclaration(method, executionContext);
MBoegers marked this conversation as resolved.
Show resolved Hide resolved

Optional<J.Annotation> testNgAnnotation = FindAnnotation.findFirst(method, new AnnotationMatcher("@org.testng.annotations.Test"));
if (testNgAnnotation.isEmpty()) {
return method;
}

// determine Provider name and class
String dataProviderMethodName = AnnotationArguments.extractLiteral(testNgAnnotation.get(), "dataProvider", String.class)
.orElse(method.getSimpleName());
String dataProviderClass = AnnotationArguments.extractAssignments(testNgAnnotation.get(), "dataProviderClass").stream()
.findAny()
.map(J.FieldAccess.class::cast)
.map(J.FieldAccess::getTarget)
.map(e -> e.unwrap().getType())
.filter(JavaType.Class.class::isInstance)
.map(JavaType.Class.class::cast)
.map(JavaType.Class::getFullyQualifiedName)
.orElse(requireNonNull(getCursor().firstEnclosingOrThrow(J.ClassDeclaration.class).getType()).getFullyQualifiedName());

// add parameterized test annotation
JavaCoordinates addAnnotationCoordinate = method.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName));
method = JavaTemplate
.builder("@ParameterizedTest")
.javaParser(JavaParser.fromJavaVersion().classpath("junit-jupiter-params"))
.imports("org.junit.jupiter.params.ParameterizedTest")
.build()
.apply(getCursor(), addAnnotationCoordinate);
maybeAddImport("org.junit.jupiter.params.ParameterizedTest", false);

// add MethodSource annotation
addAnnotationCoordinate = method.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName));
method = JavaTemplate
.builder("@MethodSource(\"#{}##{}Source\")")
.javaParser(JavaParser.fromJavaVersion().classpath("junit-jupiter-params"))
.imports("org.junit.jupiter.params.provider.MethodSource")
.build()
.apply(getCursor(), addAnnotationCoordinate, dataProviderClass, dataProviderMethodName);
maybeAddImport("org.junit.jupiter.params.provider.MethodSource", false);

return method;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package io.github.mboegers.openrewrite.testngtojupiter;

import io.github.mboegers.openrewrite.testngtojupiter.helper.AnnotationArguments;
import io.github.mboegers.openrewrite.testngtojupiter.helper.FindAnnotation;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.*;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;

import java.time.Duration;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

@Value
@EqualsAndHashCode(callSuper = false)
Expand Down Expand Up @@ -43,27 +54,11 @@ static class MigrateEnabledArgumentVisitor extends JavaIsoVisitor<ExecutionConte
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
method = super.visitMethodDeclaration(method, ctx);

// return early if not @Test annotation with argument absent present
var testNgAnnotation = method.getLeadingAnnotations().stream()
.filter(TESTNG_TEST_MATCHER::matches)
.findAny();
if (testNgAnnotation.isEmpty()) {
return method;
}

var enabledArgument = testNgAnnotation
.map(J.Annotation::getArguments).orElse(List.of())
.stream()
.filter(this::isEnabledExpression)
.map(J.Assignment.class::cast)
.findAny();
if (enabledArgument.isEmpty()) {
return method;
}

// add @Disables if enabled=false
Boolean isEnabled = (Boolean) ((J.Literal) enabledArgument.get().getAssignment().unwrap()).getValue();
if (Boolean.FALSE.equals(isEnabled)) {
Optional<Boolean> isEnabled = FindAnnotation.find(method, TESTNG_TEST_MATCHER).stream().findAny()
.flatMap(j -> AnnotationArguments.extractLiteral(j, "enabled", Boolean.class));

if (isEnabled.isPresent() && !isEnabled.get()) {
var addAnnotationCoordinate = method.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName));
method = JavaTemplate
.builder("@Disabled")
Expand All @@ -79,10 +74,5 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex

return method;
}

private boolean isEnabledExpression(Expression expr) {
return expr instanceof J.Assignment &&
"enabled".equals(((J.Identifier) ((J.Assignment) expr).getVariable()).getSimpleName());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

package io.github.mboegers.openrewrite.testngtojupiter;

import io.github.mboegers.openrewrite.testngtojupiter.helper.AnnotationArguments;
import io.github.mboegers.openrewrite.testngtojupiter.helper.FindAnnotation;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.ExecutionContext;
Expand Down Expand Up @@ -55,20 +57,14 @@ class ReplaceTestAnnotationVisitor extends JavaIsoVisitor<ExecutionContext> {
@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
method = super.visitMethodDeclaration(method, ctx);
var methodAnnotations = method.getLeadingAnnotations();

//return early if no TestNG used or still has arguments
var testNgAnnotation = methodAnnotations.stream()
.filter(TESTNG_TEST_MATCHER::matches)
.findAny();
var testNgAnnotation = FindAnnotation.findFirst(method, TESTNG_TEST_MATCHER);
if (testNgAnnotation.isEmpty()) {
return method;
}

boolean hasArguments = testNgAnnotation
.map(J.Annotation::getArguments)
.map(as -> !as.isEmpty() && as.stream().noneMatch(J.Empty.class::isInstance))
.orElse(false);
boolean hasArguments = AnnotationArguments.hasAny(testNgAnnotation.get());
if (hasArguments) {
return method;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.github.mboegers.openrewrite.testngtojupiter.helper;

import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;

import java.util.List;
import java.util.Optional;

/**
* Answer questions regarding annotation arguments and there values
*
* @see J.Annotation
*/
public final class AnnotationArguments {

/**
* Determins if the annotation has any arguments
*
* @param annotation
* @return
*/
public static boolean hasAny(J.Annotation annotation) {
List<Expression> arguments = annotation.getArguments();

if (arguments == null || arguments.isEmpty()) {
return false;
}

boolean containsNoEmpty = arguments.stream().noneMatch(J.Empty.class::isInstance);
return containsNoEmpty;
}

/**
* Extracts all assignments with the given argument name from the annotation
*
* @param annotation to extract the assignments from
* @param argumentName to extract
* @return
*/
public static List<Expression> extractAssignments(J.Annotation annotation, String argumentName) {
List<Expression> arguments = annotation.getArguments();

if (arguments == null) {
return List.of();
}

return arguments.stream()
.filter(J.Assignment.class::isInstance)
.map(J.Assignment.class::cast)
.filter(a -> argumentName.equals(((J.Identifier) a.getVariable()).getSimpleName()))
.map(J.Assignment::getAssignment)
.toList();
}

/**
* Extract an annotation argument as literal
*
* @param annotation to extract literal from
* @param argumentName to extract
* @param valueClass expected type of the value
* @param <T> Type of the value
* @return the value or Optional#empty
*/
public static <T> Optional<T> extractLiteral(J.Annotation annotation, String argumentName, Class<T> valueClass) {
List<Expression> arguments = annotation.getArguments();

if (arguments == null) {
return Optional.empty();
}

return extractAssignments(annotation, argumentName).stream()
.filter(J.Literal.class::isInstance)
.map(J.Literal.class::cast)
.findAny()
.map(J.Literal::getValue)
.map(valueClass::cast);
}
}
Loading
Loading