From 14253bdf2c785613e9d74cac67edd0041d49c364 Mon Sep 17 00:00:00 2001
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
Date: Sat, 17 Feb 2024 14:58:56 -0700
Subject: [PATCH] Run round-trip adventure codec tests with JSON, NBT, and Java
 ops. Use JavaOps for conversions. (#10031)

---
 patches/server/Adventure.patch    | 117 +++++++++------
 patches/server/Test-changes.patch | 240 ++++++++++++++++++++++++++++++
 2 files changed, 315 insertions(+), 42 deletions(-)

diff --git a/patches/server/Adventure.patch b/patches/server/Adventure.patch
index 2b54ea84f4..d56513e1c3 100644
--- a/patches/server/Adventure.patch
+++ b/patches/server/Adventure.patch
@@ -1684,9 +1684,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import java.util.function.Function;
 +import net.kyori.adventure.text.Component;
 +import net.kyori.adventure.text.serializer.ComponentSerializer;
-+import net.minecraft.nbt.NbtOps;
-+import net.minecraft.nbt.Tag;
 +import net.minecraft.network.chat.ComponentSerialization;
++import net.minecraft.util.JavaOps;
 +
 +final class WrapperAwareSerializer implements ComponentSerializer<Component, Component, net.minecraft.network.chat.Component> {
 +    @Override
@@ -1694,26 +1693,26 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        if (input instanceof AdventureComponent) {
 +            return ((AdventureComponent) input).adventure;
 +        }
-+        final Tag tag = ComponentSerialization.CODEC.encodeStart(NbtOps.INSTANCE, input)
++        final Object obj = ComponentSerialization.CODEC.encodeStart(JavaOps.INSTANCE, input)
 +            .get().map(Function.identity(), partial -> {
 +                throw new RuntimeException("Failed to encode Minecraft Component: " + input + "; " + partial.message());
 +            });
-+        final Pair<Component, Tag> converted = AdventureCodecs.COMPONENT_CODEC.decode(NbtOps.INSTANCE, tag)
++        final Pair<Component, Object> converted = AdventureCodecs.COMPONENT_CODEC.decode(JavaOps.INSTANCE, obj)
 +            .get().map(Function.identity(), partial -> {
-+                throw new RuntimeException("Failed to decode to adventure Component: " + tag + "; " + partial.message());
++                throw new RuntimeException("Failed to decode to adventure Component: " + obj + "; " + partial.message());
 +            });
 +        return converted.getFirst();
 +    }
 +
 +    @Override
 +    public net.minecraft.network.chat.Component serialize(final Component component) {
-+        final Tag tag = AdventureCodecs.COMPONENT_CODEC.encodeStart(NbtOps.INSTANCE, component)
++        final Object obj = AdventureCodecs.COMPONENT_CODEC.encodeStart(JavaOps.INSTANCE, component)
 +            .get().map(Function.identity(), partial -> {
 +                throw new RuntimeException("Failed to encode adventure Component: " + component + "; " + partial.message());
 +            });
-+        final Pair<net.minecraft.network.chat.Component, Tag> converted = ComponentSerialization.CODEC.decode(NbtOps.INSTANCE, tag)
++        final Pair<net.minecraft.network.chat.Component, Object> converted = ComponentSerialization.CODEC.decode(JavaOps.INSTANCE, obj)
 +            .get().map(Function.identity(), partial -> {
-+                throw new RuntimeException("Failed to decode to Minecraft Component: " + tag + "; " + partial.message());
++                throw new RuntimeException("Failed to decode to Minecraft Component: " + obj + "; " + partial.message());
 +            });
 +        return converted.getFirst();
 +    }
@@ -5640,7 +5639,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import com.mojang.datafixers.util.Pair;
 +import com.mojang.serialization.Codec;
 +import com.mojang.serialization.DataResult;
++import com.mojang.serialization.DynamicOps;
++import com.mojang.serialization.JsonOps;
++import io.papermc.paper.util.MethodParameterSource;
 +import java.io.IOException;
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
 +import java.util.List;
 +import java.util.UUID;
 +import java.util.function.Function;
@@ -5663,6 +5669,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import net.minecraft.nbt.Tag;
 +import net.minecraft.network.chat.ComponentSerialization;
 +import net.minecraft.resources.ResourceLocation;
++import net.minecraft.util.JavaOps;
 +import net.minecraft.world.item.ItemStack;
 +import net.minecraft.world.item.Items;
 +import org.apache.commons.lang3.RandomStringUtils;
@@ -5671,6 +5678,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import org.junit.jupiter.params.ParameterizedTest;
 +import org.junit.jupiter.params.provider.EnumSource;
 +import org.junit.jupiter.params.provider.MethodSource;
++import org.junitpioneer.jupiter.cartesian.CartesianTest;
 +
 +import static io.papermc.paper.adventure.AdventureCodecs.CLICK_EVENT_CODEC;
 +import static io.papermc.paper.adventure.AdventureCodecs.COMPONENT_CODEC;
@@ -5704,6 +5712,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +class AdventureCodecsTest extends AbstractTestingBase {
 +
++    static final String PARAMETERIZED_NAME = "[{index}] {displayName}: {arguments}";
++
 +    @Test
 +    void testTextColor() {
 +        final TextColor color = color(0x1d38df);
@@ -5731,7 +5741,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        assertEquals(key.asString(), location.toString());
 +    }
 +
-+    @ParameterizedTest
++    @ParameterizedTest(name = PARAMETERIZED_NAME)
 +    @EnumSource(value = ClickEvent.Action.class, mode = EnumSource.Mode.EXCLUDE, names = {"OPEN_FILE"})
 +    void testClickEvent(final ClickEvent.Action action) {
 +        final ClickEvent event = ClickEvent.clickEvent(action, RandomStringUtils.randomAlphanumeric(20));
@@ -5794,33 +5804,47 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        assertEquals(requireNonNull(style.color()).value(), requireNonNull(nms.getColor()).getValue());
 +    }
 +
-+    @ParameterizedTest
-+    @MethodSource({"testStyles"})
-+    void testDirectRoundTripStyle(final Style style) {
-+        testDirectRoundTrip(STYLE_MAP_CODEC.codec(), style);
++    @CartesianTest(name = PARAMETERIZED_NAME)
++    void testDirectRoundTripStyle(
++        @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps,
++        @MethodParameterSource("testStyles") final Style style
++    ) {
++        testDirectRoundTrip(dynamicOps, STYLE_MAP_CODEC.codec(), style);
 +    }
 +
-+    @ParameterizedTest
-+    @MethodSource({"testStyles"})
-+    void testMinecraftRoundTripStyle(final Style style) {
-+        testMinecraftRoundTrip(STYLE_MAP_CODEC.codec(), net.minecraft.network.chat.Style.Serializer.CODEC, style);
++    @CartesianTest(name = PARAMETERIZED_NAME)
++    void testMinecraftRoundTripStyle(
++        @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps,
++        @MethodParameterSource("testStyles") final Style style
++    ) {
++        testMinecraftRoundTrip(dynamicOps, STYLE_MAP_CODEC.codec(), net.minecraft.network.chat.Style.Serializer.CODEC, style);
 +    }
 +
-+    @ParameterizedTest
-+    @MethodSource({"testTexts", "testTranslatables", "testKeybinds", "testScores",
-+        "testSelectors", "testBlockNbts", "testEntityNbts", "testStorageNbts"})
-+    void testDirectRoundTripComponent(final Component component) {
-+        testDirectRoundTrip(COMPONENT_CODEC, component);
++    @CartesianTest(name = PARAMETERIZED_NAME)
++    void testDirectRoundTripComponent(
++        @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps,
++        @TestComponents final Component component
++    ) {
++        testDirectRoundTrip(dynamicOps, COMPONENT_CODEC, component);
 +    }
 +
-+    @ParameterizedTest
-+    @MethodSource({"testTexts", "testTranslatables", "testKeybinds", "testScores",
-+        "testSelectors", "testBlockNbts", "testEntityNbts", "testStorageNbts"})
-+    void testMinecraftRoundTripComponent(final Component component) {
-+        testMinecraftRoundTrip(COMPONENT_CODEC, ComponentSerialization.CODEC, component);
++    @CartesianTest(name = PARAMETERIZED_NAME)
++    void testMinecraftRoundTripComponent(
++        @MethodParameterSource("dynamicOps") final DynamicOps<?> dynamicOps,
++        @TestComponents final Component component
++    ) {
++        testMinecraftRoundTrip(dynamicOps, COMPONENT_CODEC, ComponentSerialization.CODEC, component);
 +    }
 +
-+    @ParameterizedTest
++    static List<DynamicOps<?>> dynamicOps() {
++        return List.of(
++            NbtOps.INSTANCE,
++            JavaOps.INSTANCE,
++            JsonOps.INSTANCE
++        );
++    }
++
++    @ParameterizedTest(name = PARAMETERIZED_NAME)
 +    @MethodSource({"invalidData"})
 +    void invalidThrows(final Tag input) {
 +        assertThrows(RuntimeException.class, () -> {
@@ -5831,33 +5855,33 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        });
 +    }
 +
-+    static <A> void testDirectRoundTrip(final Codec<A> codec, final A adventure) {
-+        final Tag encoded = require(
-+            codec.encodeStart(NbtOps.INSTANCE, adventure),
++    static <A, O> void testDirectRoundTrip(final DynamicOps<O> ops, final Codec<A> codec, final A adventure) {
++        final O encoded = require(
++            codec.encodeStart(ops, adventure),
 +            msg -> "Failed to encode " + adventure + ": " + msg
 +        );
-+        final Pair<A, Tag> roundTripResult = require(
-+            codec.decode(NbtOps.INSTANCE, encoded),
++        final Pair<A, O> roundTripResult = require(
++            codec.decode(ops, encoded),
 +            msg -> "Failed to decode " + encoded + ": " + msg
 +        );
 +        assertEquals(adventure, roundTripResult.getFirst());
 +    }
 +
-+    static <A, M> void testMinecraftRoundTrip(final Codec<A> adventureCodec, final Codec<M> minecraftCodec, final A adventure) {
-+        final Tag encoded = require(
-+            adventureCodec.encodeStart(NbtOps.INSTANCE, adventure),
++    static <A, M, O> void testMinecraftRoundTrip(final DynamicOps<O> ops, final Codec<A> adventureCodec, final Codec<M> minecraftCodec, final A adventure) {
++        final O encoded = require(
++            adventureCodec.encodeStart(ops, adventure),
 +            msg -> "Failed to encode " + adventure + ": " + msg
 +        );
 +        final M minecraftResult = require(
-+            minecraftCodec.decode(NbtOps.INSTANCE, encoded),
++            minecraftCodec.decode(ops, encoded),
 +            msg -> "Failed to decode to Minecraft: " + encoded + "; " + msg
 +        ).getFirst();
-+        final Tag minecraftReEncoded = require(
-+            minecraftCodec.encodeStart(NbtOps.INSTANCE, minecraftResult),
++        final O minecraftReEncoded = require(
++            minecraftCodec.encodeStart(ops, minecraftResult),
 +            msg -> "Failed to re-encode Minecraft: " + minecraftResult + "; " + msg
 +        );
-+        final Pair<A, Tag> roundTripResult = require(
-+            adventureCodec.decode(NbtOps.INSTANCE, minecraftReEncoded),
++        final Pair<A, O> roundTripResult = require(
++            adventureCodec.decode(ops, minecraftReEncoded),
 +            msg -> "Failed to decode " + minecraftReEncoded + ": " + msg
 +        );
 +        assertEquals(adventure, roundTripResult.getFirst());
@@ -5902,6 +5926,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        );
 +    }
 +
++    @Retention(RetentionPolicy.RUNTIME)
++    @Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
++    @MethodParameterSource({
++        "testTexts", "testTranslatables", "testKeybinds", "testScores",
++        "testSelectors", "testBlockNbts", "testEntityNbts", "testStorageNbts"
++    })
++    @interface TestComponents {
++    }
++
 +    static List<Component> testTexts() {
 +        return List.of(
 +            Component.empty(),
@@ -5938,7 +5971,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                .key("thisIsA")
 +                .fallback("This is a test.")
 +                .build(),
-+            translatable(key, numeric(5), text("HEY")), // boolean doesn't work in vanilla, can't test here
++            translatable(key, numeric(Integer.MAX_VALUE), text("HEY")), // boolean doesn't work in vanilla, can't test here
 +            translatable(
 +                key,
 +                text().content(name)
diff --git a/patches/server/Test-changes.patch b/patches/server/Test-changes.patch
index 09f135a330..305b3e8ec0 100644
--- a/patches/server/Test-changes.patch
+++ b/patches/server/Test-changes.patch
@@ -8,6 +8,14 @@ diff --git a/build.gradle.kts b/build.gradle.kts
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/build.gradle.kts
 +++ b/build.gradle.kts
+@@ -0,0 +0,0 @@ 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
 @@ -0,0 +0,0 @@ tasks.compileJava {
      options.setIncremental(false)
  }
@@ -97,6 +105,238 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/util/MethodParameterProvider.java
+@@ -0,0 +0,0 @@
++/*
++ * Copyright 2015-2023 the original author or authors of https://github.com/junit-team/junit5/blob/6593317c15fb556febbde11914fa7afe00abf8cd/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java
++ *
++ * 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.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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/util/MethodParameterSource.java
+@@ -0,0 +0,0 @@
++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/test/java/org/bukkit/support/DummyServer.java