All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.robtimus.junit.support.extension.MethodLookup Maven / Gradle / Ivy

Go to download

Contains interfaces and classes that make it easier to write tests with JUnit

There is a newer version: 3.0
Show newest version
/*
 * MethodLookup.java
 * Copyright 2022 Rob Spoor
 *
 * 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
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.robtimus.junit.support.extension;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.PreconditionViolationException;
import org.junit.platform.commons.support.HierarchyTraversalMode;
import org.junit.platform.commons.support.ReflectionSupport;

/**
 * A class to help find methods based on annotations or other method references. These lookups work similar to the lookups used for
 * {@link MethodSource}. In addition, it supports multiple sets of supported parameter types. This can be used to validate method references if
 * the parameter types are given, or perform multiple lookups otherwise.
 * 

* Instances of this class are not thread safe when configuring them using {@link #orParameterTypes(Class...)}. Once an instance is configured, it's * safe to call {@link #find(String, ExtensionContext)} from different threads concurrently. * * @author Rob Spoor * @since 2.0 */ @SuppressWarnings("nls") public final class MethodLookup { static final Pattern METHOD_REFERENCE_PATTERN = createMethodReferencePattern(); private final List[]> parameterTypeCombinations; private final List combinationRepresentations; private MethodLookup() { parameterTypeCombinations = new ArrayList<>(); combinationRepresentations = new ArrayList<>(); } /** * Creates a new method lookup instance. * * @param parameterTypes The preferred set of parameter types. * @return The created method lookup instance. */ public static MethodLookup withParameterTypes(Class... parameterTypes) { MethodLookup lookup = new MethodLookup(); lookup.addParameterTypes(parameterTypes); return lookup; } /** * Adds another allowed set of parameter types. * * @param parameterTypes The set of parameter types. * @return This object. */ public MethodLookup orParameterTypes(Class... parameterTypes) { addParameterTypes(parameterTypes); return this; } private void addParameterTypes(Class... parameterTypes) { combinationRepresentations.add(toString(parameterTypes)); parameterTypeCombinations.add(parameterTypes.clone()); } private String toString(Class... classes) { return Arrays.stream(classes) .map(this::toString) .collect(Collectors.joining(", ", "(", ")")); } private String toString(Class clazz) { String canonicalName = clazz.getCanonicalName(); return canonicalName != null ? canonicalName : clazz.getName(); } /** * Tries to find a method. If the method cannot be found, an exception is thrown. *

* The method reference can be defined in a number of ways: *

    *
  • {@code methodName} for a method in the test class itself. * The parameter types for the method are those used to create this instance; the first match will be returned.
  • *
  • {@code className}{@code #}{@code methodName} for a method in the defined class. * The parameter types for the method are those used to create this instance; the first match will be returned.
  • *
  • {@code methodName}(parameterTypes) for a method in the test class itself. * The parameter types are used as-is, but these must match one of the sets of parameter types used to create this instance.
  • *
  • {@code className}{@code #}{@code methodName}(parameterTypes) for a method in the defined class. * The parameter types are used as-is, but these must match one of the sets of parameter types used to create this instance.
  • *
* * @param methodReference A reference to the method to find. * @param context The current extension context; never {@code null}. * @return A result describing the method that was found. */ public Result find(String methodReference, ExtensionContext context) { if (isBlank(methodReference)) { throw new PreconditionViolationException("methodReference must not be null or blank"); } Matcher matcher = METHOD_REFERENCE_PATTERN.matcher(methodReference); if (!matcher.matches()) { throw new PreconditionViolationException(String.format("[%s] is not a valid method reference: " + "it must be the method name, optionally preceded by a fully qualified class name followed by a '#', " + "and optionally followed by a parameter list enclosed in parentheses.", methodReference)); } String className = className(matcher); String methodName = methodName(matcher); String methodArguments = methodArguments(matcher); Class factoryClass = className == null ? context.getRequiredTestClass() : ReflectionSupport.tryToLoadClass(className) .getOrThrow(cause -> new JUnitException(String.format("Could not load class [%s]", className), cause)); return methodArguments == null ? findUsingParameterTypes(factoryClass, methodName) : findUsingMethodArguments(factoryClass, methodName, methodArguments); } private Result findUsingParameterTypes(Class factoryClass, String methodName) { // Try all combinations until a match is found for (ListIterator[]> iterator = parameterTypeCombinations.listIterator(); iterator.hasNext(); ) { int index = iterator.nextIndex(); Class[] parameterTypes = iterator.next(); Method match = ReflectionSupport.findMethod(factoryClass, methodName, parameterTypes).orElse(null); if (match != null) { return new Result(match, index); } } // No match if (parameterTypeCombinations.size() == 1) { throw new PreconditionViolationException(String.format("Could not find method [%s] in class [%s] with parameter combination %s", methodName, factoryClass.getName(), combinationRepresentations.get(0))); } throw new PreconditionViolationException(String.format("Could not find method [%s] in class [%s] with a parameter combination in %s", methodName, factoryClass.getName(), combinationRepresentations)); } private Result findUsingMethodArguments(Class factoryClass, String methodName, String methodArguments) { Method method = ReflectionSupport.findMethod(factoryClass, methodName, methodArguments) .orElseThrow(() -> new PreconditionViolationException(String.format("Could not find method [%s(%s)] in class [%s]", methodName, methodArguments, factoryClass.getName()))); // don't just return the method; check that it's one of the expected ones for (ListIterator[]> iterator = parameterTypeCombinations.listIterator(); iterator.hasNext(); ) { int index = iterator.nextIndex(); Class[] parameterTypes = iterator.next(); Method match = ReflectionSupport.findMethod(factoryClass, methodName, parameterTypes).orElse(null); if (match != null && method.equals(match)) { return new Result(method, index); } } // Although the method exists, it is not supported if (parameterTypeCombinations.size() == 1) { throw new PreconditionViolationException(String.format("Method [%s(%s)] in class [%s] does not have parameter combination %s", methodName, methodArguments, factoryClass.getName(), combinationRepresentations.get(0))); } throw new PreconditionViolationException(String.format("Method [%s(%s)] in class [%s] does not have a parameter combination in %s", methodName, methodArguments, factoryClass.getName(), combinationRepresentations)); } /** * Tries to find a method. If the method cannot be found, an exception is thrown. *

* The method reference can be defined in a number of ways: *

    *
  • {@code methodName} for a method in the test class itself. * If multiple methods are found with the given name, an exception is thrown.
  • *
  • {@code className}{@code #}{@code methodName} for a method in the defined class. * If multiple methods are found with the given name, an exception is thrown.
  • *
  • {@code methodName}(parameterTypes) for a method in the test class itself. * The parameter types are used as-is.
  • *
  • {@code className}{@code #}{@code methodName}(parameterTypes) for a method in the defined class. * The parameter types are used as-is.
  • *
* * @param methodReference A reference to the method to find. * @param context The current extension context; never {@code null}. * @return The method that was found. */ public static Method findMethod(String methodReference, ExtensionContext context) { if (isBlank(methodReference)) { throw new PreconditionViolationException("methodReference must not be null or blank"); } Matcher matcher = METHOD_REFERENCE_PATTERN.matcher(methodReference); if (!matcher.matches()) { throw new PreconditionViolationException(String.format("[%s] is not a valid method reference: " + "it must be the method name, optionally preceded by a fully qualified class name followed by a '#', " + "and optionally followed by a parameter list enclosed in parentheses.", methodReference)); } String className = className(matcher); String methodName = methodName(matcher); String methodArguments = methodArguments(matcher); Class factoryClass = className == null ? context.getRequiredTestClass() : ReflectionSupport.tryToLoadClass(className) .getOrThrow(cause -> new JUnitException(String.format("Could not load class [%s]", className), cause)); return methodArguments == null ? findSingleMethodWithName(factoryClass, methodName) : findMethodUsingMethodArguments(factoryClass, methodName, methodArguments); } private static Method findSingleMethodWithName(Class factoryClass, String methodName) { List methods = ReflectionSupport.findMethods(factoryClass, method -> methodName.equals(method.getName()), HierarchyTraversalMode.TOP_DOWN); if (methods.isEmpty()) { throw new PreconditionViolationException(String.format("Could not find method [%s] in class [%s]", methodName, factoryClass.getName())); } if (methods.size() > 1) { throw new PreconditionViolationException(String.format("Found several methods named [%s] in class [%s]", methodName, factoryClass.getName())); } return methods.get(0); } private static Method findMethodUsingMethodArguments(Class factoryClass, String methodName, String methodArguments) { return ReflectionSupport.findMethod(factoryClass, methodName, methodArguments) .orElseThrow(() -> new PreconditionViolationException(String.format("Could not find method [%s(%s)] in class [%s]", methodName, methodArguments, factoryClass.getName()))); } private static boolean isBlank(String value) { return value == null || value.chars().allMatch(Character::isWhitespace); } private static Pattern createMethodReferencePattern() { String javaIdentifier = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"; String javaType = String.format("%s(?:\\.%s)*", javaIdentifier, javaIdentifier); String classNamePart = String.format("(?%s)", javaType); String methodNamePart = String.format("(?%s)", javaIdentifier); String methodArgumentsPart = String.format("(?(?:%s(?:,\\s*%s)*)?)", javaType, javaType); String regex = String.format("(?:%s#)?%s(?:\\(%s\\))?", classNamePart, methodNamePart, methodArgumentsPart); return Pattern.compile(regex); } static String className(Matcher matcher) { return matcher.group("className"); } static String methodName(Matcher matcher) { return matcher.group("methodName"); } static String methodArguments(Matcher matcher) { return matcher.group("methodArguments"); } /** * The result of finding a method. * Besides the method itself, this class also knows which parameter type combination was used to find the method, * and supports invoking the method. * * @author Rob Spoor * @since 2.0 */ public static final class Result { private final Method method; private final int index; Result(Method method, int index) { this.method = method; this.index = index; } /** * Returns the method that was found. * * @return The method that was found. */ public Method method() { return method; } /** * Returns the index of the parameter type combination that was used to find the method. * * @return The index of the parameter type combination that was used to find the method. */ public int index() { return index; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy