diff --git a/wpilibj/src/main/java/org/wpilib/driverstation/DefaultUserControls.java b/wpilibj/src/main/java/org/wpilib/driverstation/DefaultUserControls.java new file mode 100644 index 0000000000..3416bd38fe --- /dev/null +++ b/wpilibj/src/main/java/org/wpilib/driverstation/DefaultUserControls.java @@ -0,0 +1,31 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.driverstation; + +/** + * A default implementation of UserControls that provides Gamepad instances for each of the 6 + * joystick ports provided by the DS. + */ +public class DefaultUserControls implements UserControls { + private final Gamepad[] m_gamepads; + + /** Constructs a DefaultUserControls instance with Gamepads for each port. */ + public DefaultUserControls() { + m_gamepads = new Gamepad[DriverStation.kJoystickPorts]; + for (int i = 0; i < m_gamepads.length; i++) { + m_gamepads[i] = new Gamepad(i); + } + } + + /** + * Returns the Gamepad instance for the specified port. + * + * @param port The joystick port number. + * @return The Gamepad instance for the given port. + */ + public Gamepad getGamepad(int port) { + return m_gamepads[port]; + } +} diff --git a/wpilibj/src/main/java/org/wpilib/driverstation/UserControls.java b/wpilibj/src/main/java/org/wpilib/driverstation/UserControls.java new file mode 100644 index 0000000000..ad6c03d5b3 --- /dev/null +++ b/wpilibj/src/main/java/org/wpilib/driverstation/UserControls.java @@ -0,0 +1,15 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.driverstation; + +/** + * An interface representing user controls such as gamepads or joysticks. If your main robot class + * has a UserControlsInstance attribute with a class implementing this interface, the constructor is + * able to receive an instance of that class. Additionally, any OpModes can also receive that same + * instance. + * + *

The implementation of this class must have a default constructor + */ +public interface UserControls {} diff --git a/wpilibj/src/main/java/org/wpilib/driverstation/UserControlsInstance.java b/wpilibj/src/main/java/org/wpilib/driverstation/UserControlsInstance.java new file mode 100644 index 0000000000..f10b074320 --- /dev/null +++ b/wpilibj/src/main/java/org/wpilib/driverstation/UserControlsInstance.java @@ -0,0 +1,26 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.driverstation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation to specify the UserControls implementation class to be used for a robot. Apply this + * annotation to your main robot class, providing a class that implements the UserControls + * interface. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserControlsInstance { + /** + * The UserControls implementation class to be used. + * + * @return The class that implements UserControls. + */ + Class value(); +} diff --git a/wpilibj/src/main/java/org/wpilib/framework/OpModeRobot.java b/wpilibj/src/main/java/org/wpilib/framework/OpModeRobot.java index 1e37b41c0b..ba06d54387 100644 --- a/wpilibj/src/main/java/org/wpilib/framework/OpModeRobot.java +++ b/wpilibj/src/main/java/org/wpilib/framework/OpModeRobot.java @@ -6,17 +6,19 @@ package org.wpilib.framework; import java.io.File; import java.io.IOException; -import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.net.URL; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.wpilib.driverstation.DriverStation; +import org.wpilib.driverstation.UserControls; +import org.wpilib.driverstation.UserControlsInstance; import org.wpilib.hardware.hal.ControlWord; import org.wpilib.hardware.hal.DriverStationJNI; import org.wpilib.hardware.hal.HAL; @@ -27,6 +29,7 @@ import org.wpilib.opmode.OpMode; import org.wpilib.opmode.Teleop; import org.wpilib.opmode.TestOpMode; import org.wpilib.util.Color; +import org.wpilib.util.ConstructorMatch; import org.wpilib.util.WPIUtilJNI; /** @@ -56,51 +59,68 @@ public abstract class OpModeRobot extends RobotBase { DriverStation.reportError("Error adding OpMode " + cls.getSimpleName() + ": " + message, false); } - /** - * Find a public constructor to instantiate the opmode. Prefer a single-arg public constructor - * whose parameter type is assignable from this.getClass() (if multiple, pick the most specific - * parameter type). Otherwise return the public no-arg constructor. Return null if neither exists. - */ - private Constructor findOpModeConstructor(Class cls) { - Constructor bestCtor = null; - Class bestParam = null; - for (Constructor ctor : cls.getConstructors()) { - Class[] params = ctor.getParameterTypes(); - if (params.length != 1) { - continue; - } - Class param = params[0]; - if (!param.isAssignableFrom(getClass())) { - continue; - } - if (bestCtor == null || bestParam.isAssignableFrom(param)) { - bestCtor = ctor; - bestParam = param; - } + private final Optional> m_userControlsBaseClass; + private UserControls m_userControlsInstance; + + void setUserControlsInstance(UserControls userControlsInstance) { + if (m_userControlsBaseClass.isEmpty()) { + throw new IllegalStateException("No UserControls class specified"); } - if (bestCtor != null) { - return bestCtor; - } - try { - return cls.getConstructor(); - } catch (NoSuchMethodException e) { - return null; + + if (!m_userControlsBaseClass.get().isAssignableFrom(userControlsInstance.getClass())) { + throw new IllegalArgumentException( + userControlsInstance.getClass().getSimpleName() + + " is not assignable to " + + m_userControlsBaseClass.get().getSimpleName()); } + m_userControlsInstance = userControlsInstance; } - private OpMode constructOpModeClass(Class cls) { - Constructor constructor = findOpModeConstructor(cls); - if (constructor == null) { + /** + * Find a public constructor to instantiate the opmode. This constructor can have up to 2 + * parameters. The first parameter (if present) must be assignable from this.getClass(). The + * second parameter (if present) must be assignable from DriverStationBase. If multiple, first + * sort by most parameters, then by most specific first, then by most specific second. + */ + private Optional> findOpModeConstructor(Class cls) { + Optional> ctor; + + // try 2-parameter constructor + ctor = ConstructorMatch.findBestConstructor(cls, getClass(), m_userControlsInstance.getClass()); + if (ctor.isPresent()) { + return ctor; + } + + // try 1-parameter constructor with RobotBase parameter + ctor = ConstructorMatch.findBestConstructor(cls, getClass()); + if (ctor.isPresent()) { + return ctor; + } + + // try 1-parameter constructor with UserControls parameter + ctor = ConstructorMatch.findBestConstructor(cls, m_userControlsInstance.getClass()); + if (ctor.isPresent()) { + return ctor; + } + + // try no-parameter constructor + ctor = ConstructorMatch.findBestConstructor(cls); + if (ctor.isPresent()) { + return ctor; + } + + return Optional.empty(); + } + + private T constructOpModeClass(Class cls) { + Optional> constructor = findOpModeConstructor(cls); + if (constructor.isEmpty()) { DriverStation.reportError( "No suitable constructor to instantiate OpMode " + cls.getSimpleName(), true); return null; } try { - if (constructor.getParameterCount() == 1) { - return (OpMode) constructor.newInstance(this); - } else { - return (OpMode) constructor.newInstance(); - } + return constructor.get().newInstance(this, m_userControlsInstance); } catch (ReflectiveOperationException e) { DriverStation.reportError( "Could not instantiate OpMode " + cls.getSimpleName(), e.getStackTrace()); @@ -128,7 +148,7 @@ public abstract class OpModeRobot extends RobotBase { } // it must have a public no-arg constructor or a public constructor that accepts this class // (or a superclass/interface) as an argument - if (findOpModeConstructor(cls) == null) { + if (findOpModeConstructor(cls).isEmpty()) { throw new IllegalArgumentException( "missing public no-arg constructor or constructor accepting " + getClass().getSimpleName()); @@ -288,7 +308,7 @@ public abstract class OpModeRobot extends RobotBase { } private void addOpModeClassImpl( - Class cls, + Class cls, RobotMode mode, String name, String group, @@ -305,7 +325,7 @@ public abstract class OpModeRobot extends RobotBase { } private void addAnnotatedOpModeImpl( - Class cls, Autonomous auto, Teleop teleop, TestOpMode test) { + Class cls, Autonomous auto, Teleop teleop, TestOpMode test) { checkOpModeClass(cls); // add an opmode for each annotation @@ -364,10 +384,10 @@ public abstract class OpModeRobot extends RobotBase { private void addAnnotatedOpModeClass(String name) { // trim ".class" from end String className = name.replace('/', '.').substring(0, name.length() - 6); - Class cls; + Class cls; try { - cls = Class.forName(className); - } catch (ClassNotFoundException e) { + cls = Class.forName(className).asSubclass(OpMode.class); + } catch (ClassNotFoundException | ClassCastException e) { return; } Autonomous auto = cls.getAnnotation(Autonomous.class); @@ -468,6 +488,14 @@ public abstract class OpModeRobot extends RobotBase { /** Constructor. */ @SuppressWarnings("this-escape") public OpModeRobot() { + // Check to see if we have a DS annotation + UserControlsInstance userControlsAnnotation = + getClass().getAnnotation(UserControlsInstance.class); + if (userControlsAnnotation != null) { + m_userControlsBaseClass = Optional.of(userControlsAnnotation.value()); + } else { + m_userControlsBaseClass = Optional.empty(); + } // Scan for annotated opmode classes within the derived class's package and subpackages addAnnotatedOpModeClasses(getClass().getPackage()); DriverStation.publishOpModes(); diff --git a/wpilibj/src/main/java/org/wpilib/framework/RobotBase.java b/wpilibj/src/main/java/org/wpilib/framework/RobotBase.java index 4174313633..b4bb63fb52 100644 --- a/wpilibj/src/main/java/org/wpilib/framework/RobotBase.java +++ b/wpilibj/src/main/java/org/wpilib/framework/RobotBase.java @@ -4,9 +4,11 @@ package org.wpilib.framework; -import java.lang.reflect.Constructor; +import java.util.Optional; import java.util.concurrent.locks.ReentrantLock; import org.wpilib.driverstation.DriverStation; +import org.wpilib.driverstation.UserControls; +import org.wpilib.driverstation.UserControlsInstance; import org.wpilib.hardware.hal.HAL; import org.wpilib.hardware.hal.HALUtil; import org.wpilib.math.util.MathShared; @@ -18,6 +20,7 @@ import org.wpilib.system.Notifier; import org.wpilib.system.RuntimeType; import org.wpilib.system.Timer; import org.wpilib.system.WPILibVersion; +import org.wpilib.util.ConstructorMatch; import org.wpilib.util.WPIUtilJNI; import org.wpilib.vision.stream.CameraServerShared; import org.wpilib.vision.stream.CameraServerSharedStore; @@ -286,28 +289,29 @@ public abstract class RobotBase implements AutoCloseable { private static boolean m_suppressExitWarning; private static T constructRobot(Class robotClass) throws Throwable { - Constructor[] constructors = robotClass.getConstructors(); - Constructor defaultConstructor = null; - for (Constructor constructor : constructors) { - Class[] paramTypes = constructor.getParameterTypes(); - if (paramTypes.length == 0) { - if (defaultConstructor != null) { - throw new IllegalArgumentException( - "Multiple default constructors found in robot class " + robotClass.getName()); - } - defaultConstructor = constructor; - } + UserControlsInstance userControlsAttribute = + robotClass.getDeclaredAnnotation(UserControlsInstance.class); + UserControls userControlsInstance = null; + Optional> constructorMatch; + if (userControlsAttribute != null) { + var userControlsClass = userControlsAttribute.value(); + userControlsInstance = userControlsClass.getDeclaredConstructor().newInstance(); + constructorMatch = ConstructorMatch.findBestConstructor(robotClass, userControlsClass); + } else { + constructorMatch = ConstructorMatch.findBestConstructor(robotClass); } - T robot; - - if (defaultConstructor != null) { - robot = robotClass.cast(defaultConstructor.newInstance()); - } else { + if (constructorMatch.isEmpty()) { throw new IllegalArgumentException( "No valid constructor found in robot class " + robotClass.getName()); } + T robot = constructorMatch.get().newInstance(userControlsInstance); + + if (robot instanceof OpModeRobot opModeRobot) { + // Insert the UserControls instance into the opModeRobot for use when constructing opmodes + opModeRobot.setUserControlsInstance(userControlsInstance); + } return robot; } @@ -392,9 +396,10 @@ public abstract class RobotBase implements AutoCloseable { /** * Starting point for the applications. * + * @param Robot subclass. * @param robotClass Robot subclass type. */ - public static void startRobot(Class robotClass) { + public static void startRobot(Class robotClass) { // Check that the MSVC runtime is valid. WPIUtilJNI.checkMsvcRuntime(); diff --git a/wpiutil/src/main/java/org/wpilib/util/ConstructorMatch.java b/wpiutil/src/main/java/org/wpilib/util/ConstructorMatch.java new file mode 100644 index 0000000000..b279d48261 --- /dev/null +++ b/wpiutil/src/main/java/org/wpilib/util/ConstructorMatch.java @@ -0,0 +1,176 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.util; + +import java.lang.reflect.Constructor; +import java.util.List; +import java.util.Optional; + +/** + * Utility class to find the best matching constructor for a given set of parameter types. The + * constructor must have parameter types that are assignable from the given parameter types, and the + * parameter types must not be assignable to each other. If multiple constructors match, the one + * with the most specific parameter types is chosen. If there is still a tie, the one with the most + * specific first parameter type is chosen, then the second parameter type, and so on. + * + * @param the type of the class to find the constructor for + */ +public class ConstructorMatch { + private final Constructor m_constructor; + private final List> m_parameterTypes; + + /** + * Constructs a ConstructorMatch with the given constructor and parameter types. The parameter + * types must not be assignable to each other. + * + * @param constructor the constructor to match + * @param parameterTypes the parameter types for the constructor + */ + public ConstructorMatch(Constructor constructor, Class... parameterTypes) { + m_constructor = constructor; + m_parameterTypes = List.of(parameterTypes); + if (!isValidParameterPack(parameterTypes)) { + throw new IllegalArgumentException("Parameter types must not be assignable to each other"); + } + } + + private static boolean isValidParameterPack(Class... types) { + // Verify that all of the parameter types are not assignable to each other + for (int i = 0; i < types.length; i++) { + // Don't allow object parameters, as they would match any parameter type + // and prevent more specific matches from being found + if (types[i].equals(Object.class)) { + return false; + } + + for (int j = i + 1; j < types.length; j++) { + if (types[i].isAssignableFrom(types[j]) || types[j].isAssignableFrom(types[i])) { + return false; + } + } + } + return true; + } + + /** + * Creates a new instance of the constructor's class using the given arguments. The arguments must + * match the parameter types of the constructor, and must not be assignable to each other. The + * order of the arguments does not matter, as they will be matched to the parameter types. + * Duplicate arguments are ignored, as the first match will match. + * + * @param args the arguments to pass to the constructor + * @return a new instance of the constructor's class + * @throws ReflectiveOperationException if the constructor cannot be invoked + */ + public T newInstance(Object... args) throws ReflectiveOperationException { + Object[] parameterArgs = new Object[m_parameterTypes.size()]; + // Find the incoming argument that matches each parameter type + for (int i = 0; i < m_parameterTypes.size(); i++) { + boolean found = false; + for (Object arg : args) { + if (m_parameterTypes.get(i).isAssignableFrom(arg.getClass())) { + parameterArgs[i] = arg; + found = true; + break; + } + } + if (!found) { + throw new IllegalArgumentException( + "No argument found for parameter type " + m_parameterTypes.get(i)); + } + } + return m_constructor.newInstance(parameterArgs); + } + + /** + * Finds the best matching constructor for the given class and parameter types. The constructor + * must have parameter types that are assignable from the given parameter types, and the parameter + * types must not be assignable to each other. If multiple constructors match, the one with the + * most specific parameter types is chosen. If there is still a tie, the one with the most + * specific first parameter type is chosen, then the second parameter type, and so on. The order + * of the parameter types does not matter, as they will be matched to the constructor's parameter + * types. Duplicate parameter types are ignored, as the first match will match. + * + * @param the type of the class to find the constructor for + * @param clazz the class to find the constructor for + * @param parameterTypes the parameter types to match + * @return an Optional containing the best matching ConstructorMatch, or empty if no match is + * found + */ + public static Optional> findBestConstructor( + Class clazz, Class... parameterTypes) { + if (!isValidParameterPack(parameterTypes)) { + return Optional.empty(); + } + Constructor bestCtor = null; + Class[] bestParameterTypes = new Class[parameterTypes.length]; + @SuppressWarnings("unchecked") + Constructor[] constructors = (Constructor[]) clazz.getConstructors(); + for (Constructor constructor : constructors) { + Class[] ctorParameterTypes = constructor.getParameterTypes(); + if (ctorParameterTypes.length != parameterTypes.length) { + continue; + } + boolean matches = true; + for (int i = 0; i < parameterTypes.length; i++) { + // Don't allow object parameters, as they would match any parameter type and + // prevent more specific matches from being found + if (ctorParameterTypes[i].equals(Object.class)) { + matches = false; + break; + } + if (!ctorParameterTypes[i].isAssignableFrom(parameterTypes[i])) { + matches = false; + break; + } + } + if (!matches) { + continue; + } + boolean better = false; + if (bestCtor == null) { + better = true; + } else { + // Check if this constructor is more specific than the best one found so far + // Order by parameter order so that if one constructor has a more specific + // parameter type for the first parameter, it is preferred over a constructor + // that has a more specific parameter type for the second parameter + for (int i = 0; i < parameterTypes.length; i++) { + if (ctorParameterTypes[i] != bestParameterTypes[i]) { + if (bestParameterTypes[i].isAssignableFrom(ctorParameterTypes[i])) { + better = true; + } + break; + } + } + } + if (better) { + bestCtor = constructor; + System.arraycopy(ctorParameterTypes, 0, bestParameterTypes, 0, parameterTypes.length); + } + } + return bestCtor == null + ? Optional.empty() + : Optional.of(new ConstructorMatch<>(bestCtor, bestParameterTypes)); + } + + /** + * Returns the constructor that was matched. + * + * @return the constructor that was matched + */ + public Constructor getConstructor() { + return m_constructor; + } + + /** + * Returns the parameter types for the constructor. + * + * @return the parameter types for the constructor + */ + public List> getParameterTypes() { + return m_parameterTypes; + } +} diff --git a/wpiutil/src/test/java/org/wpilib/util/ConstructorMatchTest.java b/wpiutil/src/test/java/org/wpilib/util/ConstructorMatchTest.java new file mode 100644 index 0000000000..57e979c0d3 --- /dev/null +++ b/wpiutil/src/test/java/org/wpilib/util/ConstructorMatchTest.java @@ -0,0 +1,278 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({ + "PMD.TestClassWithoutTestCases", + "PMD.UnusedFormalParameter", + "RedundantModifier" +}) +class ConstructorMatchTest { + public static class TestClass { + public TestClass() {} + + public TestClass(String s) {} + + public TestClass(Optional o) {} + + public TestClass(String s, Optional o) {} + } + + public static class TestInvalidParameterClass { + public TestInvalidParameterClass(String s, Object o) {} + } + + @Test + void testTooManyParameters() { + var ctor = ConstructorMatch.findBestConstructor(TestClass.class, String.class, Object.class); + assertTrue(ctor.isEmpty()); + } + + @Test + void testUnassignableParameters() { + var ctor = + ConstructorMatch.findBestConstructor( + TestInvalidParameterClass.class, String.class, Object.class); + assertTrue(ctor.isEmpty()); + } + + @Test + void testValidConstructorNoArgs() throws ReflectiveOperationException { + var ctor = ConstructorMatch.findBestConstructor(TestClass.class); + assertTrue(ctor.isPresent()); + ctor.get().newInstance(); + ctor.get().newInstance("test", Optional.empty()); + ctor.get().newInstance(Optional.empty(), "test"); + ctor.get().newInstance("test"); + ctor.get().newInstance(Optional.empty()); + } + + @Test + void testValidConstructorString() throws ReflectiveOperationException { + var ctor = ConstructorMatch.findBestConstructor(TestClass.class, String.class); + assertTrue(ctor.isPresent()); + ctor.get().newInstance("test", Optional.empty()); + ctor.get().newInstance(Optional.empty(), "test"); + ctor.get().newInstance("test"); + } + + @Test + void testInvalidConstructorString() throws ReflectiveOperationException { + var ctor = ConstructorMatch.findBestConstructor(TestClass.class, String.class); + assertTrue(ctor.isPresent()); + assertThrows(IllegalArgumentException.class, () -> ctor.get().newInstance()); + assertThrows(IllegalArgumentException.class, () -> ctor.get().newInstance(Optional.empty())); + } + + @Test + void testValidConstructorOptional() throws ReflectiveOperationException { + var ctor = ConstructorMatch.findBestConstructor(TestClass.class, Optional.class); + assertTrue(ctor.isPresent()); + ctor.get().newInstance("test", Optional.empty()); + ctor.get().newInstance(Optional.empty(), "test"); + ctor.get().newInstance(Optional.empty()); + } + + @Test + void testInvalidConstructorOptional() throws ReflectiveOperationException { + var ctor = ConstructorMatch.findBestConstructor(TestClass.class, Optional.class); + assertTrue(ctor.isPresent()); + assertThrows(IllegalArgumentException.class, () -> ctor.get().newInstance()); + assertThrows(IllegalArgumentException.class, () -> ctor.get().newInstance("test")); + } + + @Test + void testValidConstructorStringOptional() throws ReflectiveOperationException { + var ctor = ConstructorMatch.findBestConstructor(TestClass.class, String.class, Optional.class); + assertTrue(ctor.isPresent()); + ctor.get().newInstance("test", Optional.empty()); + ctor.get().newInstance(Optional.empty(), "test"); + } + + @Test + void testInvalidConstructorStringOptional() throws ReflectiveOperationException { + var ctor = ConstructorMatch.findBestConstructor(TestClass.class, String.class, Optional.class); + assertTrue(ctor.isPresent()); + assertThrows(IllegalArgumentException.class, () -> ctor.get().newInstance()); + assertThrows(IllegalArgumentException.class, () -> ctor.get().newInstance("test")); + assertThrows(IllegalArgumentException.class, () -> ctor.get().newInstance(Optional.empty())); + } + + // Since this is built for opmodes, we're going to write tests that assume that + // scenario. + + public interface UserControls {} + + public static class DefaultUserControls implements UserControls {} + + public static class CustomUserControls implements UserControls {} + + public static class RobotBase {} + + public static class RobotWithNoUserControls extends RobotBase {} + + public static class RobotWithDefaultUserControls extends RobotBase { + public RobotWithDefaultUserControls(DefaultUserControls controls) {} + } + + @Test + void testRobotWithDefaultUserControls() throws ReflectiveOperationException { + var ctor = + ConstructorMatch.findBestConstructor( + RobotWithDefaultUserControls.class, DefaultUserControls.class); + assertTrue(ctor.isPresent()); + ctor.get().newInstance(new DefaultUserControls()); + assertThrows( + IllegalArgumentException.class, () -> ctor.get().newInstance(new CustomUserControls())); + } + + public static class RobotWithCustomUserControls extends RobotBase { + public RobotWithCustomUserControls(CustomUserControls controls) {} + } + + @Test + void testRobotWithCustomUserControls() throws ReflectiveOperationException { + var ctor = + ConstructorMatch.findBestConstructor( + RobotWithCustomUserControls.class, CustomUserControls.class); + assertTrue(ctor.isPresent()); + ctor.get().newInstance(new CustomUserControls()); + assertThrows( + IllegalArgumentException.class, () -> ctor.get().newInstance(new DefaultUserControls())); + } + + public static class RobotWithUserControls extends RobotBase { + public RobotWithUserControls(UserControls controls) {} + } + + @Test + void testRobotWithUserControls() throws ReflectiveOperationException { + var ctor = + ConstructorMatch.findBestConstructor(RobotWithUserControls.class, UserControls.class); + assertTrue(ctor.isPresent()); + ctor.get().newInstance(new DefaultUserControls()); + ctor.get().newInstance(new CustomUserControls()); + } + + public static class OpModeWithRobotBase { + public OpModeWithRobotBase(RobotBase robot) {} + } + + @Test + void testOpModeWithRobotBase() throws ReflectiveOperationException { + var ctor = ConstructorMatch.findBestConstructor(OpModeWithRobotBase.class, RobotBase.class); + assertTrue(ctor.isPresent()); + var defaultUserControls = new DefaultUserControls(); + var customUserControls = new CustomUserControls(); + ctor.get().newInstance(new RobotWithNoUserControls(), defaultUserControls); + ctor.get() + .newInstance(new RobotWithDefaultUserControls(defaultUserControls), defaultUserControls); + ctor.get().newInstance(new RobotWithCustomUserControls(customUserControls), customUserControls); + ctor.get().newInstance(new RobotWithUserControls(defaultUserControls), defaultUserControls); + } + + public static class OpModeWithRobotWithNoUserControls { + public OpModeWithRobotWithNoUserControls(RobotWithNoUserControls robot) {} + } + + @Test + void testOpModeWithRobotWithNoUserControls() throws ReflectiveOperationException { + var ctor = + ConstructorMatch.findBestConstructor( + OpModeWithRobotWithNoUserControls.class, RobotWithNoUserControls.class); + assertTrue(ctor.isPresent()); + var defaultUserControls = new DefaultUserControls(); + ctor.get().newInstance(new RobotWithNoUserControls(), defaultUserControls); + } + + public static class OpModeWithRobotWithDefaultUserControls { + public OpModeWithRobotWithDefaultUserControls(RobotWithDefaultUserControls robot) {} + + public OpModeWithRobotWithDefaultUserControls( + RobotWithDefaultUserControls robot, DefaultUserControls controls) {} + + public OpModeWithRobotWithDefaultUserControls(DefaultUserControls controls) {} + } + + @Test + void testOpModeWithRobotWithDefaultUserControlsRobotArg() throws ReflectiveOperationException { + var ctor = + ConstructorMatch.findBestConstructor( + OpModeWithRobotWithDefaultUserControls.class, RobotWithDefaultUserControls.class); + assertTrue(ctor.isPresent()); + var defaultUserControls = new DefaultUserControls(); + ctor.get() + .newInstance(new RobotWithDefaultUserControls(defaultUserControls), defaultUserControls); + } + + @Test + void testOpModeWithRobotWithDefaultUserControlsControlsArg() throws ReflectiveOperationException { + var ctor = + ConstructorMatch.findBestConstructor( + OpModeWithRobotWithDefaultUserControls.class, DefaultUserControls.class); + assertTrue(ctor.isPresent()); + var defaultUserControls = new DefaultUserControls(); + ctor.get() + .newInstance(new RobotWithDefaultUserControls(defaultUserControls), defaultUserControls); + } + + @Test + void testOpModeWithRobotWithDefaultUserControlsNoArgs() throws ReflectiveOperationException { + var ctor = + ConstructorMatch.findBestConstructor( + OpModeWithRobotWithDefaultUserControls.class, + RobotWithDefaultUserControls.class, + DefaultUserControls.class); + assertTrue(ctor.isPresent()); + var defaultUserControls = new DefaultUserControls(); + ctor.get() + .newInstance(new RobotWithDefaultUserControls(defaultUserControls), defaultUserControls); + } + + public static class MostSpecificFirstArg { + public MostSpecificFirstArg(RobotBase robot, DefaultUserControls controls) {} + + public MostSpecificFirstArg(RobotWithDefaultUserControls robot, UserControls controls) {} + } + + @Test + void testMostSpecificFirstArg() throws ReflectiveOperationException { + var ctor = + ConstructorMatch.findBestConstructor( + MostSpecificFirstArg.class, + RobotWithDefaultUserControls.class, + DefaultUserControls.class); + assertTrue(ctor.isPresent()); + var parameterTypes = ctor.get().getParameterTypes(); + assertEquals(RobotWithDefaultUserControls.class, parameterTypes.get(0)); + assertEquals(UserControls.class, parameterTypes.get(1)); + } + + public static class MostSpecificSecondArg { + public MostSpecificSecondArg(RobotBase robot, DefaultUserControls controls) {} + + public MostSpecificSecondArg(RobotBase robot, UserControls controls) {} + } + + @Test + void testMostSpecificSecondArg() throws ReflectiveOperationException { + var ctor = + ConstructorMatch.findBestConstructor( + MostSpecificSecondArg.class, + RobotWithDefaultUserControls.class, + DefaultUserControls.class); + assertTrue(ctor.isPresent()); + var parameterTypes = ctor.get().getParameterTypes(); + assertEquals(RobotBase.class, parameterTypes.get(0)); + assertEquals(DefaultUserControls.class, parameterTypes.get(1)); + } +}