Skip to content

Commit

Permalink
Move test utils
Browse files Browse the repository at this point in the history
  • Loading branch information
jpenilla committed Dec 12, 2023
1 parent 94dfb25 commit 67c401b
Show file tree
Hide file tree
Showing 2 changed files with 232 additions and 236 deletions.
234 changes: 232 additions & 2 deletions patches/server/0004-Test-changes.patch
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ Subject: [PATCH] Test changes


diff --git a/build.gradle.kts b/build.gradle.kts
index f83cda5b820ebb3dcbc3a39059579ba9487586e8..ef91080dc7123677190839b057c3458c4d5b9f32 100644
index f83cda5b820ebb3dcbc3a39059579ba9487586e8..7c330ad519655b2652094bde0bfaab12b1273ca6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -58,6 +58,12 @@ tasks.compileJava {
@@ -23,6 +23,7 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testImplementation("org.hamcrest:hamcrest:2.2")
testImplementation("org.mockito:mockito-core:5.5.0")
+ testImplementation("org.junit-pioneer:junit-pioneer:2.2.0") // Paper - CartesianTest
}

val craftbukkitPackageVersion = "1_20_R3" // Paper
@@ -58,6 +59,12 @@ tasks.compileJava {
options.setIncremental(false)
}

Expand Down Expand Up @@ -97,6 +105,228 @@ index 0000000000000000000000000000000000000000..6eb95a5e2534974c0e52e2b78b04e7c2
+ return Collections.emptySet();
+ }
+}
diff --git a/src/test/java/io/papermc/paper/util/MethodParameterProvider.java b/src/test/java/io/papermc/paper/util/MethodParameterProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..c26640b1d603cff0971083d60947cb594f1e3fbe
--- /dev/null
+++ b/src/test/java/io/papermc/paper/util/MethodParameterProvider.java
@@ -0,0 +1,196 @@
+package io.papermc.paper.util;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestFactory;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.params.support.AnnotationConsumer;
+import org.junit.platform.commons.JUnitException;
+import org.junit.platform.commons.PreconditionViolationException;
+import org.junit.platform.commons.util.ClassLoaderUtils;
+import org.junit.platform.commons.util.CollectionUtils;
+import org.junit.platform.commons.util.Preconditions;
+import org.junit.platform.commons.util.ReflectionUtils;
+import org.junit.platform.commons.util.StringUtils;
+import org.junitpioneer.jupiter.cartesian.CartesianParameterArgumentsProvider;
+
+import static java.lang.String.format;
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.toList;
+import static org.junit.platform.commons.util.AnnotationUtils.isAnnotated;
+import static org.junit.platform.commons.util.CollectionUtils.isConvertibleToStream;
+
+public class MethodParameterProvider implements CartesianParameterArgumentsProvider<Object>, AnnotationConsumer<MethodParameterSource> {
+ private MethodParameterSource source;
+
+ MethodParameterProvider() {
+ }
+
+ @Override
+ public void accept(final MethodParameterSource source) {
+ this.source = source;
+ }
+
+ @Override
+ public Stream<Object> provideArguments(ExtensionContext context, Parameter parameter) {
+ return this.provideArguments(context, this.source);
+ }
+
+ // Below is mostly copied from MethodArgumentsProvider
+
+ private static final Predicate<Method> isFactoryMethod = //
+ method -> isConvertibleToStream(method.getReturnType()) && !isTestMethod(method);
+
+ protected Stream<Object> provideArguments(ExtensionContext context, MethodParameterSource methodSource) {
+ Class<?> testClass = context.getRequiredTestClass();
+ Method testMethod = context.getRequiredTestMethod();
+ Object testInstance = context.getTestInstance().orElse(null);
+ String[] methodNames = methodSource.value();
+ // @formatter:off
+ return stream(methodNames)
+ .map(factoryMethodName -> findFactoryMethod(testClass, testMethod, factoryMethodName))
+ .map(factoryMethod -> validateFactoryMethod(factoryMethod, testInstance))
+ .map(factoryMethod -> context.getExecutableInvoker().invoke(factoryMethod, testInstance))
+ .flatMap(CollectionUtils::toStream);
+ // @formatter:on
+ }
+
+ private static Method findFactoryMethod(Class<?> testClass, Method testMethod, String factoryMethodName) {
+ String originalFactoryMethodName = factoryMethodName;
+
+ // If the user did not provide a factory method name, find a "default" local
+ // factory method with the same name as the parameterized test method.
+ if (StringUtils.isBlank(factoryMethodName)) {
+ factoryMethodName = testMethod.getName();
+ return findFactoryMethodBySimpleName(testClass, testMethod, factoryMethodName);
+ }
+
+ // Convert local factory method name to fully-qualified method name.
+ if (!looksLikeAFullyQualifiedMethodName(factoryMethodName)) {
+ factoryMethodName = testClass.getName() + "#" + factoryMethodName;
+ }
+
+ // Find factory method using fully-qualified name.
+ Method factoryMethod = findFactoryMethodByFullyQualifiedName(testClass, testMethod, factoryMethodName);
+
+ // Ensure factory method has a valid return type and is not a test method.
+ Preconditions.condition(isFactoryMethod.test(factoryMethod), () -> format(
+ "Could not find valid factory method [%s] for test class [%s] but found the following invalid candidate: %s",
+ originalFactoryMethodName, testClass.getName(), factoryMethod));
+
+ return factoryMethod;
+ }
+
+ private static boolean looksLikeAFullyQualifiedMethodName(String factoryMethodName) {
+ if (factoryMethodName.contains("#")) {
+ return true;
+ }
+ int indexOfFirstDot = factoryMethodName.indexOf('.');
+ if (indexOfFirstDot == -1) {
+ return false;
+ }
+ int indexOfLastOpeningParenthesis = factoryMethodName.lastIndexOf('(');
+ if (indexOfLastOpeningParenthesis > 0) {
+ // Exclude simple/local method names with parameters
+ return indexOfFirstDot < indexOfLastOpeningParenthesis;
+ }
+ // If we get this far, we conclude the supplied factory method name "looks"
+ // like it was intended to be a fully-qualified method name, even if the
+ // syntax is invalid. We do this in order to provide better diagnostics for
+ // the user when a fully-qualified method name is in fact invalid.
+ return true;
+ }
+
+ // package-private for testing
+ static Method findFactoryMethodByFullyQualifiedName(
+ Class<?> testClass, Method testMethod,
+ String fullyQualifiedMethodName
+ ) {
+ String[] methodParts = ReflectionUtils.parseFullyQualifiedMethodName(fullyQualifiedMethodName);
+ String className = methodParts[0];
+ String methodName = methodParts[1];
+ String methodParameters = methodParts[2];
+ ClassLoader classLoader = ClassLoaderUtils.getClassLoader(testClass);
+ Class<?> clazz = loadRequiredClass(className, classLoader);
+
+ // Attempt to find an exact match first.
+ Method factoryMethod = ReflectionUtils.findMethod(clazz, methodName, methodParameters).orElse(null);
+ if (factoryMethod != null) {
+ return factoryMethod;
+ }
+
+ boolean explicitParameterListSpecified = //
+ StringUtils.isNotBlank(methodParameters) || fullyQualifiedMethodName.endsWith("()");
+
+ // If we didn't find an exact match but an explicit parameter list was specified,
+ // that's a user configuration error.
+ Preconditions.condition(!explicitParameterListSpecified,
+ () -> format("Could not find factory method [%s(%s)] in class [%s]", methodName, methodParameters,
+ className));
+
+ // Otherwise, fall back to the same lenient search semantics that are used
+ // to locate a "default" local factory method.
+ return findFactoryMethodBySimpleName(clazz, testMethod, methodName);
+ }
+
+ /**
+ * Find the factory method by searching for all methods in the given {@code clazz}
+ * with the desired {@code factoryMethodName} which have return types that can be
+ * converted to a {@link Stream}, ignoring the {@code testMethod} itself as well
+ * as any {@code @Test}, {@code @TestTemplate}, or {@code @TestFactory} methods
+ * with the same name.
+ *
+ * @return the single factory method matching the search criteria
+ * @throws PreconditionViolationException if the factory method was not found or
+ * multiple competing factory methods with the same name were found
+ */
+ private static Method findFactoryMethodBySimpleName(Class<?> clazz, Method testMethod, String factoryMethodName) {
+ Predicate<Method> isCandidate = candidate -> factoryMethodName.equals(candidate.getName())
+ && !testMethod.equals(candidate);
+ List<Method> candidates = ReflectionUtils.findMethods(clazz, isCandidate);
+
+ List<Method> factoryMethods = candidates.stream().filter(isFactoryMethod).collect(toList());
+
+ Preconditions.condition(factoryMethods.size() > 0, () -> {
+ // If we didn't find the factory method using the isFactoryMethod Predicate, perhaps
+ // the specified factory method has an invalid return type or is a test method.
+ // In that case, we report the invalid candidates that were found.
+ if (candidates.size() > 0) {
+ return format(
+ "Could not find valid factory method [%s] in class [%s] but found the following invalid candidates: %s",
+ factoryMethodName, clazz.getName(), candidates);
+ }
+ // Otherwise, report that we didn't find anything.
+ return format("Could not find factory method [%s] in class [%s]", factoryMethodName, clazz.getName());
+ });
+ Preconditions.condition(factoryMethods.size() == 1,
+ () -> format("%d factory methods named [%s] were found in class [%s]: %s", factoryMethods.size(),
+ factoryMethodName, clazz.getName(), factoryMethods));
+ return factoryMethods.get(0);
+ }
+
+ private static boolean isTestMethod(Method candidate) {
+ return isAnnotated(candidate, Test.class) || isAnnotated(candidate, TestTemplate.class)
+ || isAnnotated(candidate, TestFactory.class);
+ }
+
+ private static Class<?> loadRequiredClass(String className, ClassLoader classLoader) {
+ return ReflectionUtils.tryToLoadClass(className, classLoader).getOrThrow(
+ cause -> new JUnitException(format("Could not load class [%s]", className), cause));
+ }
+
+ private static Method validateFactoryMethod(Method factoryMethod, Object testInstance) {
+ Preconditions.condition(
+ factoryMethod.getDeclaringClass().isInstance(testInstance) || ReflectionUtils.isStatic(factoryMethod),
+ () -> format("Method '%s' must be static: local factory methods must be static "
+ + "unless the PER_CLASS @TestInstance lifecycle mode is used; "
+ + "external factory methods must always be static.",
+ factoryMethod.toGenericString()));
+ return factoryMethod;
+ }
+}
diff --git a/src/test/java/io/papermc/paper/util/MethodParameterSource.java b/src/test/java/io/papermc/paper/util/MethodParameterSource.java
new file mode 100644
index 0000000000000000000000000000000000000000..6cbf11c898439834cffb99ef84e5df1494356809
--- /dev/null
+++ b/src/test/java/io/papermc/paper/util/MethodParameterSource.java
@@ -0,0 +1,14 @@
+package io.papermc.paper.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junitpioneer.jupiter.cartesian.CartesianArgumentsSource;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
+@CartesianArgumentsSource(MethodParameterProvider.class)
+public @interface MethodParameterSource {
+ String[] value() default {};
+}
diff --git a/src/test/java/org/bukkit/support/DummyServer.java b/src/test/java/org/bukkit/support/DummyServer.java
index d96efc9aa90debcca5f237c949ba11b10070223a..d1253a07a90aa4cc29d0140795203a71118c827a 100644
--- a/src/test/java/org/bukkit/support/DummyServer.java
Expand Down
Loading

0 comments on commit 67c401b

Please sign in to comment.