diff --git a/build.gradle b/build.gradle index 6dd7f87..41ca077 100644 --- a/build.gradle +++ b/build.gradle @@ -91,6 +91,8 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.mockito:mockito-core:5.8.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.8.0' def akitJson = new groovy.json.JsonSlurper().parseText(new File(projectDir.getAbsolutePath() + "/vendordeps/AdvantageKit.json").text) annotationProcessor "org.littletonrobotics.akit:akit-autolog:$akitJson.version" @@ -278,4 +280,4 @@ spotless { indentWithSpaces(2) endWithNewline() } -} +} \ No newline at end of file diff --git a/gradlew b/gradlew index f5feea6..b26d411 100644 --- a/gradlew +++ b/gradlew @@ -249,4 +249,4 @@ eval "set -- $( tr '\n' ' ' )" '"$@"' -exec "$JAVACMD" "$@" +exec "$JAVACMD" "$@" \ No newline at end of file diff --git a/src/test/java/frc/alotobots/ConstantsTest.java b/src/test/java/frc/alotobots/ConstantsTest.java new file mode 100644 index 0000000..fdc1721 --- /dev/null +++ b/src/test/java/frc/alotobots/ConstantsTest.java @@ -0,0 +1,127 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class ConstantsTest { + + @Test + void testRobotEnumValues() { + // Verify Robot enum has expected values + assertEquals(2, Constants.Robot.values().length); + assertNotNull(Constants.Robot.COMPETITION); + assertNotNull(Constants.Robot.DEV); + } + + @Test + void testModeEnumValues() { + // Verify Mode enum has expected values + assertEquals(3, Constants.Mode.values().length); + assertNotNull(Constants.Mode.REAL); + assertNotNull(Constants.Mode.SIM); + assertNotNull(Constants.Mode.REPLAY); + } + + @Test + void testCurrentRobotIsNotNull() { + assertNotNull(Constants.currentRobot); + } + + @Test + void testCurrentModeIsNotNull() { + assertNotNull(Constants.currentMode); + } + + @Test + void testSimModeIsNotNull() { + assertNotNull(Constants.simMode); + } + + @Test + void testTunerConstantsIsNotNull() { + assertNotNull(Constants.tunerConstants); + } + + @Test + void testCanIdConstants() { + // Verify CAN bus constants + assertNotNull(Constants.CanId.SWERVE_CAN_BUS); + assertNotNull(Constants.CanId.RIO_CAN_BUS); + + // Verify CAN frequency is positive + assertTrue(Constants.CanId.DEFAULT_CAN_FREQUENCY > 0); + + // Verify PDH CAN ID + assertEquals(1, Constants.CanId.PDH_CAN_ID); + + // Verify other CAN IDs are in valid range (1-63) + assertTrue(Constants.CanId.CANDLE_CAN_ID >= 1 && Constants.CanId.CANDLE_CAN_ID <= 63); + assertTrue(Constants.CanId.CLIMBER_CAN_ID >= 1 && Constants.CanId.CLIMBER_CAN_ID <= 63); + assertTrue( + Constants.CanId.INTAKE_EXTENDO_CAN_ID >= 1 && Constants.CanId.INTAKE_EXTENDO_CAN_ID <= 63); + assertTrue( + Constants.CanId.INTAKE_ROLLER_CAN_ID >= 1 && Constants.CanId.INTAKE_ROLLER_CAN_ID <= 63); + assertTrue(Constants.CanId.CONVEYOR_CAN_ID >= 1 && Constants.CanId.CONVEYOR_CAN_ID <= 63); + assertTrue(Constants.CanId.TURRET_CAN_ID >= 1 && Constants.CanId.TURRET_CAN_ID <= 63); + assertTrue( + Constants.CanId.DEFLECTOR_MOTOR_CAN_ID >= 1 + && Constants.CanId.DEFLECTOR_MOTOR_CAN_ID <= 63); + assertTrue( + Constants.CanId.SHOOTER_LEFT_CAN_ID >= 1 && Constants.CanId.SHOOTER_LEFT_CAN_ID <= 63); + assertTrue( + Constants.CanId.SHOOTER_RIGHT_CAN_ID >= 1 && Constants.CanId.SHOOTER_RIGHT_CAN_ID <= 63); + assertTrue(Constants.CanId.KICKER_CAN_ID >= 1 && Constants.CanId.KICKER_CAN_ID <= 63); + assertTrue( + Constants.CanId.DEFLECTOR_ENCODER_CAN_ID >= 1 + && Constants.CanId.DEFLECTOR_ENCODER_CAN_ID <= 63); + } + + @Test + void testCanIdUniqueness() { + // Verify all motor CAN IDs are unique + int[] motorIds = { + Constants.CanId.CLIMBER_CAN_ID, + Constants.CanId.INTAKE_EXTENDO_CAN_ID, + Constants.CanId.INTAKE_ROLLER_CAN_ID, + Constants.CanId.CONVEYOR_CAN_ID, + Constants.CanId.TURRET_CAN_ID, + Constants.CanId.DEFLECTOR_MOTOR_CAN_ID, + Constants.CanId.SHOOTER_LEFT_CAN_ID, + Constants.CanId.SHOOTER_RIGHT_CAN_ID, + Constants.CanId.KICKER_CAN_ID + }; + + for (int i = 0; i < motorIds.length; i++) { + for (int j = i + 1; j < motorIds.length; j++) { + assertNotEquals(motorIds[i], motorIds[j], "CAN IDs must be unique"); + } + } + } + + @Test + void testDIOConstants() { + // Verify DIO channel is in valid range (0-25 for RoboRIO 2.0) + assertTrue( + Constants.DIO.TURRET_RESET_LIMIT_SWITCH_CHANNEL >= 0 + && Constants.DIO.TURRET_RESET_LIMIT_SWITCH_CHANNEL <= 25); + } + + @Test + void testCurrentRobotMatchesTunerConstants() { + // Verify that tuner constants are initialized based on current robot + assertNotNull(Constants.tunerConstants); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/OITest.java b/src/test/java/frc/alotobots/OITest.java new file mode 100644 index 0000000..aca15ce --- /dev/null +++ b/src/test/java/frc/alotobots/OITest.java @@ -0,0 +1,92 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class OITest { + + @Test + void testDeadbandValue() { + // Verify deadband is a reasonable value + assertTrue(OI.DEADBAND > 0.0); + assertTrue(OI.DEADBAND < 0.2); // Typical deadband is less than 0.2 + } + + @Test + void testAxisLimits() { + // Verify axis limits are correct + assertEquals(1.0, OI.AxisLimits.MAX_AXIS_LIMIT, 0.001); + assertEquals(-1.0, OI.AxisLimits.MIN_AXIS_LIMIT, 0.001); + } + + @Test + void testHasDriverInputTriggerExists() { + // Verify hasDriverInput trigger is not null + assertNotNull(OI.hasDriverInput); + } + + @Test + void testButtonTriggersExist() { + // Verify all button triggers are not null + assertNotNull(OI.resetGyroButton); + assertNotNull(OI.retractClimber); + assertNotNull(OI.extendClimber); + assertNotNull(OI.intakeOut); + assertNotNull(OI.intakeIn); + assertNotNull(OI.intake); + assertNotNull(OI.shoot); + assertNotNull(OI.rotateTurretRight); + assertNotNull(OI.rotateTurretLeft); + assertNotNull(OI.turretAimPass); + assertNotNull(OI.turretAimShoot); + } + + @Test + void testTurretAxisReturnsValidRange() { + // Test that getTurretAxis returns a value (even if it's 0 in test environment) + double axis = OI.getTurretAxis(); + assertTrue( + axis >= OI.AxisLimits.MIN_AXIS_LIMIT && axis <= OI.AxisLimits.MAX_AXIS_LIMIT, + "Turret axis should be within valid range"); + } + + @Test + void testAxisMethodsReturnValues() { + // These methods should return values without throwing exceptions + assertDoesNotThrow(() -> OI.getTranslateForwardAxis()); + assertDoesNotThrow(() -> OI.getTranslateStrafeAxis()); + assertDoesNotThrow(() -> OI.getRotationAxis()); + assertDoesNotThrow(() -> OI.getTurtleSpeedTrigger()); + assertDoesNotThrow(() -> OI.getTurboSpeedTrigger()); + assertDoesNotThrow(() -> OI.getTurretAxis()); + } + + @Test + void testAxisMethodsReturnValidRange() { + // In test environment, these should return 0 but be within valid range + double translateForward = OI.getTranslateForwardAxis(); + double translateStrafe = OI.getTranslateStrafeAxis(); + double rotation = OI.getRotationAxis(); + double turtleSpeed = OI.getTurtleSpeedTrigger(); + double turboSpeed = OI.getTurboSpeedTrigger(); + + assertTrue(translateForward >= -1.0 && translateForward <= 1.0); + assertTrue(translateStrafe >= -1.0 && translateStrafe <= 1.0); + assertTrue(rotation >= -1.0 && rotation <= 1.0); + assertTrue(turtleSpeed >= 0.0 && turtleSpeed <= 1.0); + assertTrue(turboSpeed >= 0.0 && turboSpeed <= 1.0); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/RobotContainerTest.java b/src/test/java/frc/alotobots/RobotContainerTest.java new file mode 100644 index 0000000..48164ff --- /dev/null +++ b/src/test/java/frc/alotobots/RobotContainerTest.java @@ -0,0 +1,61 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class RobotContainerTest { + + @Test + void testRobotContainerCanBeInstantiated() { + assertDoesNotThrow(() -> new RobotContainer()); + } + + @Test + void testGetAutonomousCommand() { + RobotContainer container = new RobotContainer(); + // Should return a command (may be null if no auto selected) + assertDoesNotThrow(() -> container.getAutonomousCommand()); + } + + @Test + void testResetSimulationFieldDoesNotThrow() { + RobotContainer container = new RobotContainer(); + assertDoesNotThrow(() -> container.resetSimulationField()); + } + + @Test + void testDisplaySimFieldToAdvantageScopeDoesNotThrow() { + RobotContainer container = new RobotContainer(); + assertDoesNotThrow(() -> container.displaySimFieldToAdvantageScope()); + } + + @Test + void testMultipleInstantiation() { + // Test that multiple instances can be created (though typically only one exists) + assertDoesNotThrow( + () -> { + new RobotContainer(); + new RobotContainer(); + }); + } + + @Test + void testAutonomousCommandIsNotNullAfterInstantiation() { + RobotContainer container = new RobotContainer(); + // Get autonomous command should not throw + assertDoesNotThrow(() -> container.getAutonomousCommand()); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/RobotTest.java b/src/test/java/frc/alotobots/RobotTest.java new file mode 100644 index 0000000..0d5c583 --- /dev/null +++ b/src/test/java/frc/alotobots/RobotTest.java @@ -0,0 +1,116 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class RobotTest { + + @Test + void testRobotCanBeInstantiated() { + // Test that Robot can be instantiated + // Note: This will initialize logging and subsystems + assertDoesNotThrow(() -> new Robot()); + } + + @Test + void testRobotPeriodicDoesNotThrow() { + Robot robot = new Robot(); + assertDoesNotThrow(() -> robot.robotPeriodic()); + } + + @Test + void testRobotInitDoesNotThrow() { + Robot robot = new Robot(); + assertDoesNotThrow(() -> robot.robotInit()); + } + + @Test + void testDisabledInitDoesNotThrow() { + Robot robot = new Robot(); + assertDoesNotThrow(() -> robot.disabledInit()); + } + + @Test + void testDisabledPeriodicDoesNotThrow() { + Robot robot = new Robot(); + assertDoesNotThrow(() -> robot.disabledPeriodic()); + } + + @Test + void testAutonomousInitDoesNotThrow() { + Robot robot = new Robot(); + assertDoesNotThrow(() -> robot.autonomousInit()); + } + + @Test + void testAutonomousPeriodicDoesNotThrow() { + Robot robot = new Robot(); + assertDoesNotThrow(() -> robot.autonomousPeriodic()); + } + + @Test + void testTeleopInitDoesNotThrow() { + Robot robot = new Robot(); + assertDoesNotThrow(() -> robot.teleopInit()); + } + + @Test + void testTeleopPeriodicDoesNotThrow() { + Robot robot = new Robot(); + assertDoesNotThrow(() -> robot.teleopPeriodic()); + } + + @Test + void testTestInitDoesNotThrow() { + Robot robot = new Robot(); + assertDoesNotThrow(() -> robot.testInit()); + } + + @Test + void testTestPeriodicDoesNotThrow() { + Robot robot = new Robot(); + assertDoesNotThrow(() -> robot.testPeriodic()); + } + + @Test + void testSimulationInitDoesNotThrow() { + Robot robot = new Robot(); + assertDoesNotThrow(() -> robot.simulationInit()); + } + + @Test + void testSimulationPeriodicDoesNotThrow() { + Robot robot = new Robot(); + assertDoesNotThrow(() -> robot.simulationPeriodic()); + } + + @Test + void testRobotLifecycleSequence() { + Robot robot = new Robot(); + // Test a typical lifecycle sequence + assertDoesNotThrow( + () -> { + robot.robotInit(); + robot.robotPeriodic(); + robot.autonomousInit(); + robot.autonomousPeriodic(); + robot.teleopInit(); + robot.teleopPeriodic(); + robot.disabledInit(); + robot.disabledPeriodic(); + }); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/commands/groups/IndexIntoShooterAndShootTest.java b/src/test/java/frc/alotobots/rebuilt/commands/groups/IndexIntoShooterAndShootTest.java new file mode 100644 index 0000000..b7df7c8 --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/commands/groups/IndexIntoShooterAndShootTest.java @@ -0,0 +1,98 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.commands.groups; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import frc.alotobots.rebuilt.subsystems.belt.BeltSubsystem; +import frc.alotobots.rebuilt.subsystems.kicker.KickerSubsystem; +import frc.alotobots.rebuilt.subsystems.launcher.shooter.ShooterSubsystem; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class IndexIntoShooterAndShootTest { + + private BeltSubsystem mockBeltSubsystem; + private KickerSubsystem mockKickerSubsystem; + private ShooterSubsystem mockShooterSubsystem; + private IndexIntoShooterAndShoot command; + + @BeforeEach + void setUp() { + mockBeltSubsystem = mock(BeltSubsystem.class); + mockKickerSubsystem = mock(KickerSubsystem.class); + mockShooterSubsystem = mock(ShooterSubsystem.class); + command = + new IndexIntoShooterAndShoot(mockBeltSubsystem, mockKickerSubsystem, mockShooterSubsystem); + } + + @Test + void testCommandCreation() { + assertNotNull(command); + } + + @Test + void testCommandRequiresAllSubsystems() { + assertTrue(command.getRequirements().contains(mockBeltSubsystem)); + assertTrue(command.getRequirements().contains(mockKickerSubsystem)); + assertTrue(command.getRequirements().contains(mockShooterSubsystem)); + } + + @Test + void testCommandIsSequential() { + // SequentialCommandGroup is a composite command + assertTrue(command.getRequirements().size() >= 3); + } + + @Test + void testCommandCanBeInstantiated() { + // Test that command can be created without exceptions + assertDoesNotThrow( + () -> + new IndexIntoShooterAndShoot( + mockBeltSubsystem, mockKickerSubsystem, mockShooterSubsystem)); + } + + @Test + void testCommandWithNullSubsystemsThrows() { + // Test that null subsystems cause appropriate exceptions + assertThrows( + NullPointerException.class, + () -> new IndexIntoShooterAndShoot(null, mockKickerSubsystem, mockShooterSubsystem)); + assertThrows( + NullPointerException.class, + () -> new IndexIntoShooterAndShoot(mockBeltSubsystem, null, mockShooterSubsystem)); + assertThrows( + NullPointerException.class, + () -> new IndexIntoShooterAndShoot(mockBeltSubsystem, mockKickerSubsystem, null)); + } + + @Test + void testCommandInitializeDoesNotThrow() { + assertDoesNotThrow(() -> command.initialize()); + } + + @Test + void testMultipleCommandInstances() { + // Test that multiple instances can be created independently + IndexIntoShooterAndShoot command1 = + new IndexIntoShooterAndShoot(mockBeltSubsystem, mockKickerSubsystem, mockShooterSubsystem); + IndexIntoShooterAndShoot command2 = + new IndexIntoShooterAndShoot(mockBeltSubsystem, mockKickerSubsystem, mockShooterSubsystem); + + assertNotNull(command1); + assertNotNull(command2); + assertNotSame(command1, command2); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/commands/manual/KickerShooterManualTest.java b/src/test/java/frc/alotobots/rebuilt/commands/manual/KickerShooterManualTest.java new file mode 100644 index 0000000..30d0d80 --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/commands/manual/KickerShooterManualTest.java @@ -0,0 +1,154 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.commands.manual; + +import static edu.wpi.first.units.Units.RadiansPerSecond; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import edu.wpi.first.units.measure.AngularVelocity; +import edu.wpi.first.wpilibj2.command.button.Trigger; +import frc.alotobots.rebuilt.subsystems.belt.BeltSubsystem; +import frc.alotobots.rebuilt.subsystems.kicker.KickerSubsystem; +import frc.alotobots.rebuilt.subsystems.launcher.shooter.ShooterSubsystem; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class KickerShooterManualTest { + + private ShooterSubsystem mockShooterSubsystem; + private KickerSubsystem mockKickerSubsystem; + private BeltSubsystem mockBeltSubsystem; + private Supplier velocitySupplier; + private Trigger mockTrigger; + private KickerShooterManual command; + + @BeforeEach + void setUp() { + mockShooterSubsystem = mock(ShooterSubsystem.class); + mockKickerSubsystem = mock(KickerSubsystem.class); + mockBeltSubsystem = mock(BeltSubsystem.class); + velocitySupplier = () -> RadiansPerSecond.of(100); + mockTrigger = new Trigger(() -> false); + command = + new KickerShooterManual( + mockShooterSubsystem, + mockKickerSubsystem, + mockBeltSubsystem, + velocitySupplier, + mockTrigger); + } + + @Test + void testCommandCreation() { + assertNotNull(command); + } + + @Test + void testCommandRequiresAllSubsystems() { + assertTrue(command.getRequirements().contains(mockShooterSubsystem)); + assertTrue(command.getRequirements().contains(mockKickerSubsystem)); + assertTrue(command.getRequirements().contains(mockBeltSubsystem)); + } + + @Test + void testCommandIsSequential() { + // SequentialCommandGroup is a composite command + assertTrue(command.getRequirements().size() >= 3); + } + + @Test + void testCommandCanBeInstantiated() { + assertDoesNotThrow( + () -> + new KickerShooterManual( + mockShooterSubsystem, + mockKickerSubsystem, + mockBeltSubsystem, + velocitySupplier, + mockTrigger)); + } + + @Test + void testCommandWithNullSubsystemsThrows() { + assertThrows( + NullPointerException.class, + () -> + new KickerShooterManual( + null, mockKickerSubsystem, mockBeltSubsystem, velocitySupplier, mockTrigger)); + assertThrows( + NullPointerException.class, + () -> + new KickerShooterManual( + mockShooterSubsystem, null, mockBeltSubsystem, velocitySupplier, mockTrigger)); + assertThrows( + NullPointerException.class, + () -> + new KickerShooterManual( + mockShooterSubsystem, + mockKickerSubsystem, + null, + velocitySupplier, + mockTrigger)); + } + + @Test + void testCommandWithNullVelocitySupplierThrows() { + assertThrows( + NullPointerException.class, + () -> + new KickerShooterManual( + mockShooterSubsystem, mockKickerSubsystem, mockBeltSubsystem, null, mockTrigger)); + } + + @Test + void testCommandWithNullTriggerThrows() { + assertThrows( + NullPointerException.class, + () -> + new KickerShooterManual( + mockShooterSubsystem, + mockKickerSubsystem, + mockBeltSubsystem, + velocitySupplier, + null)); + } + + @Test + void testCommandInitializeDoesNotThrow() { + assertDoesNotThrow(() -> command.initialize()); + } + + @Test + void testMultipleCommandInstances() { + KickerShooterManual command1 = + new KickerShooterManual( + mockShooterSubsystem, + mockKickerSubsystem, + mockBeltSubsystem, + velocitySupplier, + mockTrigger); + KickerShooterManual command2 = + new KickerShooterManual( + mockShooterSubsystem, + mockKickerSubsystem, + mockBeltSubsystem, + velocitySupplier, + mockTrigger); + + assertNotNull(command1); + assertNotNull(command2); + assertNotSame(command1, command2); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/subsystems/belt/BeltSubsystemTest.java b/src/test/java/frc/alotobots/rebuilt/subsystems/belt/BeltSubsystemTest.java new file mode 100644 index 0000000..4163f1a --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/subsystems/belt/BeltSubsystemTest.java @@ -0,0 +1,131 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.subsystems.belt; + +import static edu.wpi.first.units.Units.RadiansPerSecond; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import edu.wpi.first.units.measure.AngularVelocity; +import frc.alotobots.rebuilt.subsystems.belt.constants.BeltConstants; +import frc.alotobots.rebuilt.subsystems.belt.io.BeltIO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BeltSubsystemTest { + + private BeltIO mockIO; + private BeltSubsystem subsystem; + + @BeforeEach + void setUp() { + mockIO = mock(BeltIO.class); + subsystem = new BeltSubsystem(mockIO); + } + + @Test + void testRunBeltToTargetVelocity() { + AngularVelocity velocity = RadiansPerSecond.of(50); + subsystem.runBeltToTargetVelocity(velocity); + + verify(mockIO).setBeltVelocity(any(AngularVelocity.class)); + } + + @Test + void testRunBeltToTargetVelocityWithinLimits() { + AngularVelocity velocity = RadiansPerSecond.of(50); + subsystem.runBeltToTargetVelocity(velocity); + + verify(mockIO).setBeltVelocity(argThat(v -> v.in(RadiansPerSecond) <= 100)); + } + + @Test + void testRunBeltToTargetVelocityClampedAtMax() { + // Test that velocity above max is clamped + AngularVelocity highVelocity = + RadiansPerSecond.of(BeltConstants.Limits.MAX_SPEED.in(RadiansPerSecond) * 2); + subsystem.runBeltToTargetVelocity(highVelocity); + + verify(mockIO) + .setBeltVelocity( + argThat( + v -> v.in(RadiansPerSecond) <= BeltConstants.Limits.MAX_SPEED.in(RadiansPerSecond))); + } + + @Test + void testRunBeltToTargetVelocityClampedAtMin() { + // Test that velocity below min is clamped + AngularVelocity lowVelocity = + RadiansPerSecond.of(-BeltConstants.Limits.MAX_SPEED.in(RadiansPerSecond) * 2); + subsystem.runBeltToTargetVelocity(lowVelocity); + + verify(mockIO) + .setBeltVelocity( + argThat( + v -> + v.in(RadiansPerSecond) >= -BeltConstants.Limits.MAX_SPEED.in(RadiansPerSecond))); + } + + @Test + void testRunBeltPercentOutput() { + subsystem.runBeltPercentOutput(0.5); + + verify(mockIO).setBeltOpenLoop(0.5); + } + + @Test + void testRunBeltPercentOutputClampedAtMax() { + subsystem.runBeltPercentOutput(1.5); + + verify(mockIO) + .setBeltOpenLoop(argThat(v -> v <= BeltConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE)); + } + + @Test + void testRunBeltPercentOutputClampedAtMin() { + subsystem.runBeltPercentOutput(-1.5); + + verify(mockIO) + .setBeltOpenLoop(argThat(v -> v >= -BeltConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE)); + } + + @Test + void testStop() { + subsystem.stop(); + + verify(mockIO).stop(); + } + + @Test + void testPeriodicCallsUpdateInputs() { + subsystem.periodic(); + + verify(mockIO).updateInputs(any()); + } + + @Test + void testNegativeVelocity() { + AngularVelocity velocity = RadiansPerSecond.of(-30); + subsystem.runBeltToTargetVelocity(velocity); + + verify(mockIO).setBeltVelocity(any(AngularVelocity.class)); + } + + @Test + void testZeroVelocity() { + AngularVelocity velocity = RadiansPerSecond.of(0); + subsystem.runBeltToTargetVelocity(velocity); + + verify(mockIO).setBeltVelocity(velocity); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/subsystems/belt/commands/DefaultBeltRunAtVelocityTest.java b/src/test/java/frc/alotobots/rebuilt/subsystems/belt/commands/DefaultBeltRunAtVelocityTest.java new file mode 100644 index 0000000..1cc4084 --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/subsystems/belt/commands/DefaultBeltRunAtVelocityTest.java @@ -0,0 +1,94 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.subsystems.belt.commands; + +import static edu.wpi.first.units.Units.RadiansPerSecond; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import edu.wpi.first.units.measure.AngularVelocity; +import frc.alotobots.rebuilt.subsystems.belt.BeltSubsystem; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DefaultBeltRunAtVelocityTest { + + private BeltSubsystem mockSubsystem; + private Supplier velocitySupplier; + private DefaultBeltRunAtVelocity command; + + @BeforeEach + void setUp() { + mockSubsystem = mock(BeltSubsystem.class); + velocitySupplier = () -> RadiansPerSecond.of(50); + command = new DefaultBeltRunAtVelocity(mockSubsystem, velocitySupplier); + } + + @Test + void testCommandRequiresSubsystem() { + assertTrue(command.getRequirements().contains(mockSubsystem)); + } + + @Test + void testExecuteCallsSubsystemWithVelocity() { + command.execute(); + + verify(mockSubsystem).runBeltToTargetVelocity(any(AngularVelocity.class)); + } + + @Test + void testExecuteUsesSuppliedVelocity() { + AngularVelocity testVelocity = RadiansPerSecond.of(75); + Supplier testSupplier = () -> testVelocity; + DefaultBeltRunAtVelocity testCommand = + new DefaultBeltRunAtVelocity(mockSubsystem, testSupplier); + + testCommand.execute(); + + verify(mockSubsystem).runBeltToTargetVelocity(testVelocity); + } + + @Test + void testEndCallsStop() { + command.end(false); + + verify(mockSubsystem).stop(); + } + + @Test + void testEndCallsStopWhenInterrupted() { + command.end(true); + + verify(mockSubsystem).stop(); + } + + @Test + void testIsFinishedReturnsFalse() { + assertFalse(command.isFinished()); + } + + @Test + void testInitializeDoesNotThrow() { + assertDoesNotThrow(() -> command.initialize()); + } + + @Test + void testMultipleExecuteCalls() { + command.execute(); + command.execute(); + command.execute(); + + verify(mockSubsystem, times(3)).runBeltToTargetVelocity(any(AngularVelocity.class)); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/subsystems/belt/constants/BeltConstantsTest.java b/src/test/java/frc/alotobots/rebuilt/subsystems/belt/constants/BeltConstantsTest.java new file mode 100644 index 0000000..c575700 --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/subsystems/belt/constants/BeltConstantsTest.java @@ -0,0 +1,66 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.subsystems.belt.constants; + +import static edu.wpi.first.units.Units.DegreesPerSecond; +import static edu.wpi.first.units.Units.RotationsPerSecond; +import static edu.wpi.first.units.Units.Seconds; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class BeltConstantsTest { + + @Test + void testThresholdConstants() { + // Verify threshold constants are positive + assertTrue( + BeltConstants.Thresholds.AT_TARGET_VELOCITY_SPEED_THRESHOLD.in(DegreesPerSecond) > 0); + assertTrue(BeltConstants.Thresholds.AT_TARGET_VELOCITY_TIME_THRESHOLD.in(Seconds) > 0); + } + + @Test + void testLimitConstants() { + // Verify limits are positive and reasonable + assertTrue(BeltConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE > 0); + assertTrue(BeltConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE <= 1.0); + assertTrue(BeltConstants.Limits.MAX_SPEED.in(RotationsPerSecond) > 0); + } + + @Test + void testSetpointConstants() { + // Verify setpoint velocities are positive + assertTrue(BeltConstants.Setpoints.LOAD_INTO_SHOOTER_VELOCITY.in(RotationsPerSecond) > 0); + } + + @Test + void testLimitsEnabled() { + // Verify limits enabled is a boolean + assertNotNull(BeltConstants.Limits.LIMITS_ENABLED); + } + + @Test + void testSetpointWithinLimits() { + // Verify setpoint is within MAX_SPEED + assertTrue( + BeltConstants.Setpoints.LOAD_INTO_SHOOTER_VELOCITY.in(RotationsPerSecond) + <= BeltConstants.Limits.MAX_SPEED.in(RotationsPerSecond)); + } + + @Test + void testMaxOpenLoopPercentageReasonable() { + // Max open loop percentage should be between 0 and 1 + assertTrue(BeltConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE >= 0.0); + assertTrue(BeltConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE <= 1.0); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/subsystems/intake/extendo/IntakeExtendoSubsystemTest.java b/src/test/java/frc/alotobots/rebuilt/subsystems/intake/extendo/IntakeExtendoSubsystemTest.java new file mode 100644 index 0000000..9bb718a --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/subsystems/intake/extendo/IntakeExtendoSubsystemTest.java @@ -0,0 +1,176 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.subsystems.intake.extendo; + +import static edu.wpi.first.units.Units.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import edu.wpi.first.units.measure.Distance; +import edu.wpi.first.units.measure.LinearVelocity; +import frc.alotobots.rebuilt.subsystems.intake.extendo.constants.IntakeExtendoConstants; +import frc.alotobots.rebuilt.subsystems.intake.extendo.io.IntakeExtendoIO; +import frc.alotobots.rebuilt.subsystems.intake.extendo.io.IntakeExtendoIOInputsAutoLogged; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class IntakeExtendoSubsystemTest { + + private IntakeExtendoIO mockIO; + private IntakeExtendoSubsystem subsystem; + + @BeforeEach + void setUp() { + mockIO = mock(IntakeExtendoIO.class); + subsystem = new IntakeExtendoSubsystem(mockIO); + } + + @Test + void testRunToTargetPosition() { + Distance position = Meters.of(0.5); + subsystem.runToTargetPosition(position); + + verify(mockIO).setIntakeExtendoPosition(any(Distance.class), any()); + } + + @Test + void testRunToTargetPositionClampedAtMax() { + Distance highPosition = + Meters.of(IntakeExtendoConstants.Limits.MAX_EXTENSION.in(Meters) * 2); + subsystem.runToTargetPosition(highPosition); + + verify(mockIO) + .setIntakeExtendoPosition( + argThat( + d -> + d.in(Meters) + <= IntakeExtendoConstants.Limits.MAX_EXTENSION.in(Meters)), + any()); + } + + @Test + void testRunToTargetPositionClampedAtMin() { + Distance lowPosition = Meters.of(-1.0); + subsystem.runToTargetPosition(lowPosition); + + verify(mockIO) + .setIntakeExtendoPosition( + argThat( + d -> + d.in(Meters) + >= IntakeExtendoConstants.Limits.MIN_EXTENSION.in(Meters)), + any()); + } + + @Test + void testRunToTargetVelocity() { + LinearVelocity velocity = MetersPerSecond.of(0.5); + subsystem.runToTargetVelocity(velocity); + + verify(mockIO).setIntakeExtendoVelocity(any(LinearVelocity.class)); + } + + @Test + void testRunToTargetVelocityClampedAtMax() { + LinearVelocity highVelocity = + MetersPerSecond.of(IntakeExtendoConstants.Limits.MAX_OPERATOR_VELOCITY.in(MetersPerSecond) * 2); + subsystem.runToTargetVelocity(highVelocity); + + verify(mockIO) + .setIntakeExtendoVelocity( + argThat( + v -> + v.in(MetersPerSecond) + <= IntakeExtendoConstants.Limits.MAX_OPERATOR_VELOCITY.in( + MetersPerSecond))); + } + + @Test + void testRunAtPercentOutput() { + subsystem.runAtPercentOutput(0.5); + + verify(mockIO).setIntakeExtendoOpenLoop(0.5); + } + + @Test + void testRunAtPercentOutputClampedAtMax() { + subsystem.runAtPercentOutput(1.5); + + verify(mockIO) + .setIntakeExtendoOpenLoop( + argThat(v -> v <= IntakeExtendoConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE)); + } + + @Test + void testRunAtPercentOutputClampedAtMin() { + subsystem.runAtPercentOutput(-1.5); + + verify(mockIO) + .setIntakeExtendoOpenLoop( + argThat(v -> v >= -IntakeExtendoConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE)); + } + + @Test + void testStop() { + subsystem.stop(); + + verify(mockIO).stop(); + } + + @Test + void testPeriodicCallsUpdateInputs() { + subsystem.periodic(); + + verify(mockIO).updateInputs(any()); + } + + @Test + void testGetCurrentExtension() { + IntakeExtendoIOInputsAutoLogged inputs = new IntakeExtendoIOInputsAutoLogged(); + inputs.intakeExtendoDistance = Meters.of(0.3); + + when(mockIO.updateInputs(any())) + .thenAnswer( + invocation -> { + IntakeExtendoIOInputsAutoLogged arg = invocation.getArgument(0); + arg.intakeExtendoDistance = inputs.intakeExtendoDistance; + return null; + }); + + subsystem.periodic(); + Distance extension = subsystem.getCurrentExtension(); + + assertNotNull(extension); + } + + @Test + void testIsAtTargetExtensionInitiallyFalse() { + assertFalse(subsystem.isAtTargetExtension()); + } + + @Test + void testZeroVelocity() { + LinearVelocity velocity = MetersPerSecond.of(0); + subsystem.runToTargetVelocity(velocity); + + verify(mockIO).setIntakeExtendoVelocity(velocity); + } + + @Test + void testNegativeVelocity() { + LinearVelocity velocity = MetersPerSecond.of(-0.3); + subsystem.runToTargetVelocity(velocity); + + verify(mockIO).setIntakeExtendoVelocity(any(LinearVelocity.class)); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/subsystems/intake/extendo/constants/IntakeExtendoTalonFXConstantsTest.java b/src/test/java/frc/alotobots/rebuilt/subsystems/intake/extendo/constants/IntakeExtendoTalonFXConstantsTest.java new file mode 100644 index 0000000..65235ad --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/subsystems/intake/extendo/constants/IntakeExtendoTalonFXConstantsTest.java @@ -0,0 +1,106 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.subsystems.intake.extendo.constants; + +import static edu.wpi.first.units.Units.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class IntakeExtendoTalonFXConstantsTest { + + @Test + void testVelocityPIDConstants() { + // Verify velocity PID constants are non-negative + assertTrue(IntakeExtendoTalonFXConstants.PIDConstants.VelocityPIDConstants.KP >= 0); + assertTrue(IntakeExtendoTalonFXConstants.PIDConstants.VelocityPIDConstants.KI >= 0); + assertTrue(IntakeExtendoTalonFXConstants.PIDConstants.VelocityPIDConstants.KD >= 0); + assertTrue(IntakeExtendoTalonFXConstants.PIDConstants.VelocityPIDConstants.KG >= 0); + assertTrue(IntakeExtendoTalonFXConstants.PIDConstants.VelocityPIDConstants.KS >= 0); + assertTrue(IntakeExtendoTalonFXConstants.PIDConstants.VelocityPIDConstants.KV >= 0); + } + + @Test + void testPositionPIDConstants() { + // Verify position PID constants are non-negative + assertTrue(IntakeExtendoTalonFXConstants.PIDConstants.PositionPIDConstants.KP >= 0); + assertTrue(IntakeExtendoTalonFXConstants.PIDConstants.PositionPIDConstants.KI >= 0); + assertTrue(IntakeExtendoTalonFXConstants.PIDConstants.PositionPIDConstants.KD >= 0); + assertTrue(IntakeExtendoTalonFXConstants.PIDConstants.PositionPIDConstants.KA >= 0); + assertTrue(IntakeExtendoTalonFXConstants.PIDConstants.PositionPIDConstants.KG >= 0); + assertTrue(IntakeExtendoTalonFXConstants.PIDConstants.PositionPIDConstants.KS >= 0); + assertTrue(IntakeExtendoTalonFXConstants.PIDConstants.PositionPIDConstants.KV >= 0); + } + + @Test + void testMotionMagicConstants() { + // Verify motion magic constants are positive + assertTrue( + IntakeExtendoTalonFXConstants.MotionMagicConstants.CRUISE_VELOCITY.in(MetersPerSecond) > 0); + assertTrue( + IntakeExtendoTalonFXConstants.MotionMagicConstants.ACCELERATION.in( + MetersPerSecondPerSecond) + > 0); + assertTrue(IntakeExtendoTalonFXConstants.MotionMagicConstants.JERK >= 0); + } + + @Test + void testMotorSafetyLimits() { + // Verify motor safety limits are positive and reasonable + assertTrue( + IntakeExtendoTalonFXConstants.MotorSafetyLimits.TORQUE_FORWARD_AMP_LIMIT.in(Amps) > 0); + assertTrue( + IntakeExtendoTalonFXConstants.MotorSafetyLimits.TORQUE_REVERSE_AMP_LIMIT.in(Amps) < 0); + assertTrue(IntakeExtendoTalonFXConstants.MotorSafetyLimits.STATOR_AMP_LIMIT.in(Amps) > 0); + + // Verify limits are reasonable (typical for FRC motors) + assertTrue( + IntakeExtendoTalonFXConstants.MotorSafetyLimits.TORQUE_FORWARD_AMP_LIMIT.in(Amps) < 100); + assertTrue( + IntakeExtendoTalonFXConstants.MotorSafetyLimits.TORQUE_REVERSE_AMP_LIMIT.in(Amps) > -100); + assertTrue(IntakeExtendoTalonFXConstants.MotorSafetyLimits.STATOR_AMP_LIMIT.in(Amps) < 100); + } + + @Test + void testNeutralModeNotNull() { + assertNotNull(IntakeExtendoTalonFXConstants.MECHANISM_NEUTRAL_MODE); + } + + @Test + void testMotorDirectionNotNull() { + assertNotNull(IntakeExtendoTalonFXConstants.MOTOR_DIRECTION); + } + + @Test + void testExtensionPerRotationPositive() { + // Extension per rotation should be positive + assertTrue(IntakeExtendoTalonFXConstants.EXTENSION_PER_ROTATION > 0); + } + + @Test + void testExtensionPerRotationReasonable() { + // Extension per rotation should be a reasonable value (in meters) + // Typical values would be between 0.001 and 0.1 meters per rotation + assertTrue(IntakeExtendoTalonFXConstants.EXTENSION_PER_ROTATION > 0.001); + assertTrue(IntakeExtendoTalonFXConstants.EXTENSION_PER_ROTATION < 0.1); + } + + @Test + void testCruiseVelocityReasonable() { + // Cruise velocity should be reasonable for an intake extendo + double cruiseVelocity = + IntakeExtendoTalonFXConstants.MotionMagicConstants.CRUISE_VELOCITY.in(MetersPerSecond); + assertTrue(cruiseVelocity > 0.1); // At least 0.1 m/s + assertTrue(cruiseVelocity < 10.0); // Less than 10 m/s + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/subsystems/intake/extendo/io/IntakeExtendoIOTalonFXConversionTest.java b/src/test/java/frc/alotobots/rebuilt/subsystems/intake/extendo/io/IntakeExtendoIOTalonFXConversionTest.java new file mode 100644 index 0000000..0bd1c28 --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/subsystems/intake/extendo/io/IntakeExtendoIOTalonFXConversionTest.java @@ -0,0 +1,142 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.subsystems.intake.extendo.io; + +import static edu.wpi.first.units.Units.*; +import static frc.alotobots.rebuilt.subsystems.intake.extendo.constants.IntakeExtendoConstants.Limits.MIN_EXTENSION; +import static frc.alotobots.rebuilt.subsystems.intake.extendo.constants.IntakeExtendoTalonFXConstants.EXTENSION_PER_ROTATION; +import static org.junit.jupiter.api.Assertions.*; + +import edu.wpi.first.units.measure.*; +import org.junit.jupiter.api.Test; + +/** + * Test class for IntakeExtendoIOTalonFX conversion methods. Note: These tests verify the conversion + * logic mathematically without requiring hardware. The actual TalonFX hardware initialization is + * skipped in unit tests. + */ +class IntakeExtendoIOTalonFXConversionTest { + + @Test + void testExtensionPerRotationIsPositive() { + assertTrue(EXTENSION_PER_ROTATION > 0, "Extension per rotation must be positive"); + } + + @Test + void testMinExtensionIsNonNegative() { + assertTrue(MIN_EXTENSION.in(Meters) >= 0, "Minimum extension should be non-negative"); + } + + @Test + void testConversionConstantsAreConsistent() { + // Verify that the conversion constant is reasonable + // For a typical linear actuator, this should be in a reasonable range + assertTrue(EXTENSION_PER_ROTATION > 0.001, "Extension per rotation too small"); + assertTrue(EXTENSION_PER_ROTATION < 0.1, "Extension per rotation too large"); + } + + @Test + void testZeroRotationsEqualsMinExtension() { + // When the motor is at 0 rotations, the extension should be MIN_EXTENSION + // Formula: extension = EXTENSION_PER_ROTATION * rotations + MIN_EXTENSION + double expectedExtension = MIN_EXTENSION.in(Meters); + double actualExtension = EXTENSION_PER_ROTATION * 0 + MIN_EXTENSION.in(Meters); + + assertEquals(expectedExtension, actualExtension, 0.0001); + } + + @Test + void testPositiveRotationsIncreasesExtension() { + // Positive rotations should increase extension + double rotations = 10.0; + double extension = EXTENSION_PER_ROTATION * rotations + MIN_EXTENSION.in(Meters); + + assertTrue(extension > MIN_EXTENSION.in(Meters)); + } + + @Test + void testLinearVelocityConversion() { + // Test that linear velocity scales with extension per rotation + double rotationsPerSecond = 5.0; + double expectedLinearVelocity = rotationsPerSecond * EXTENSION_PER_ROTATION; + + assertEquals(expectedLinearVelocity, rotationsPerSecond * EXTENSION_PER_ROTATION, 0.0001); + } + + @Test + void testInverseConversionExtensionToRotations() { + // Test that converting extension to rotations is the inverse operation + Distance testExtension = Meters.of(0.5); + double expectedRotations = (testExtension.in(Meters) - MIN_EXTENSION.in(Meters)) / EXTENSION_PER_ROTATION; + + assertTrue(expectedRotations >= 0, "Rotations should be non-negative for valid extension"); + } + + @Test + void testLinearAccelerationConversion() { + // Test that linear acceleration scales the same way as velocity + double rotationsPerSecondSquared = 3.0; + double expectedLinearAcceleration = rotationsPerSecondSquared * EXTENSION_PER_ROTATION; + + assertEquals( + expectedLinearAcceleration, rotationsPerSecondSquared * EXTENSION_PER_ROTATION, 0.0001); + } + + @Test + void testConversionRoundTrip() { + // Test that converting from extension to rotations and back yields the same value + Distance originalExtension = Meters.of(0.3); + + // Convert to rotations + double rotations = (originalExtension.in(Meters) - MIN_EXTENSION.in(Meters)) / EXTENSION_PER_ROTATION; + + // Convert back to extension + double reconstructedExtension = EXTENSION_PER_ROTATION * rotations + MIN_EXTENSION.in(Meters); + + assertEquals(originalExtension.in(Meters), reconstructedExtension, 0.0001); + } + + @Test + void testVelocityConversionRoundTrip() { + // Test that converting velocity is consistent both ways + double originalRotationsPerSecond = 2.5; + + // Convert to linear velocity + double linearVelocity = originalRotationsPerSecond * EXTENSION_PER_ROTATION; + + // Convert back to rotational velocity + double reconstructedRotationsPerSecond = linearVelocity / EXTENSION_PER_ROTATION; + + assertEquals(originalRotationsPerSecond, reconstructedRotationsPerSecond, 0.0001); + } + + @Test + void testLargeExtensionValue() { + // Test with a larger extension value + double rotations = 100.0; + double extension = EXTENSION_PER_ROTATION * rotations + MIN_EXTENSION.in(Meters); + + assertTrue(extension > MIN_EXTENSION.in(Meters)); + assertTrue(extension < 10.0); // Reasonable upper bound for intake extension + } + + @Test + void testNegativeRotationsNotAllowed() { + // Negative rotations would result in extension less than MIN_EXTENSION + double negativeRotations = -10.0; + double extension = EXTENSION_PER_ROTATION * negativeRotations + MIN_EXTENSION.in(Meters); + + assertTrue(extension < MIN_EXTENSION.in(Meters), + "Negative rotations should result in extension below minimum"); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/subsystems/intake/roller/IntakeRollerSubsystemTest.java b/src/test/java/frc/alotobots/rebuilt/subsystems/intake/roller/IntakeRollerSubsystemTest.java new file mode 100644 index 0000000..561da5f --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/subsystems/intake/roller/IntakeRollerSubsystemTest.java @@ -0,0 +1,100 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.subsystems.intake.roller; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import frc.alotobots.rebuilt.subsystems.intake.roller.constants.IntakeRollerConstants; +import frc.alotobots.rebuilt.subsystems.intake.roller.io.IntakeRollerIO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class IntakeRollerSubsystemTest { + + private IntakeRollerIO mockIO; + private IntakeRollerSubsystem subsystem; + + @BeforeEach + void setUp() { + mockIO = mock(IntakeRollerIO.class); + subsystem = new IntakeRollerSubsystem(mockIO); + } + + @Test + void testRunAtPercentOutput() { + subsystem.runAtPercentOutput(0.5); + + verify(mockIO).setIntakeRollerOpenLoop(0.5); + } + + @Test + void testRunAtPercentOutputClampedAtMax() { + subsystem.runAtPercentOutput(1.5); + + verify(mockIO) + .setIntakeRollerOpenLoop( + argThat(v -> v <= IntakeRollerConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE)); + } + + @Test + void testRunAtPercentOutputClampedAtMin() { + subsystem.runAtPercentOutput(-1.5); + + verify(mockIO) + .setIntakeRollerOpenLoop( + argThat(v -> v >= -IntakeRollerConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE)); + } + + @Test + void testStop() { + subsystem.stop(); + + verify(mockIO).stop(); + } + + @Test + void testPeriodicCallsUpdateInputs() { + subsystem.periodic(); + + verify(mockIO).updateInputs(any()); + } + + @Test + void testZeroPercentOutput() { + subsystem.runAtPercentOutput(0.0); + + verify(mockIO).setIntakeRollerOpenLoop(0.0); + } + + @Test + void testNegativePercentOutput() { + subsystem.runAtPercentOutput(-0.5); + + verify(mockIO).setIntakeRollerOpenLoop(-0.5); + } + + @Test + void testFullForwardOutput() { + subsystem.runAtPercentOutput(1.0); + + verify(mockIO).setIntakeRollerOpenLoop(1.0); + } + + @Test + void testFullReverseOutput() { + subsystem.runAtPercentOutput(-1.0); + + verify(mockIO).setIntakeRollerOpenLoop(-1.0); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/subsystems/intake/roller/constants/IntakeRollerConstantsTest.java b/src/test/java/frc/alotobots/rebuilt/subsystems/intake/roller/constants/IntakeRollerConstantsTest.java new file mode 100644 index 0000000..fcd7b89 --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/subsystems/intake/roller/constants/IntakeRollerConstantsTest.java @@ -0,0 +1,73 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.subsystems.intake.roller.constants; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class IntakeRollerConstantsTest { + + @Test + void testLimitConstants() { + assertTrue(IntakeRollerConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE > 0); + assertTrue(IntakeRollerConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE <= 1.0); + assertTrue(IntakeRollerConstants.Limits.MAX_OPEN_LOOP_INTAKE_PERCENTAGE > 0); + assertTrue(IntakeRollerConstants.Limits.MAX_OPEN_LOOP_INTAKE_PERCENTAGE <= 1.0); + assertTrue(IntakeRollerConstants.Limits.MAX_OPEN_LOOP_EJECT_PERCENTAGE > 0); + assertTrue(IntakeRollerConstants.Limits.MAX_OPEN_LOOP_EJECT_PERCENTAGE <= 1.0); + } + + @Test + void testLimitsEnabled() { + assertNotNull(IntakeRollerConstants.Limits.LIMITS_ENABLED); + } + + @Test + void testSetpointConstants() { + // Intake percentage should be within valid range + assertTrue(IntakeRollerConstants.Setpoints.OpenLoop.INTAKE_PERCENTAGE >= 0); + assertTrue(IntakeRollerConstants.Setpoints.OpenLoop.INTAKE_PERCENTAGE <= 1.0); + + // Eject percentage should be within valid range + assertTrue(IntakeRollerConstants.Setpoints.OpenLoop.EJECT_PERCENTAGE >= 0); + assertTrue(IntakeRollerConstants.Setpoints.OpenLoop.EJECT_PERCENTAGE <= 1.0); + } + + @Test + void testSetpointsWithinLimits() { + // Intake setpoint should be within intake limit + assertTrue( + IntakeRollerConstants.Setpoints.OpenLoop.INTAKE_PERCENTAGE + <= IntakeRollerConstants.Limits.MAX_OPEN_LOOP_INTAKE_PERCENTAGE); + + // Eject setpoint should be within eject limit + assertTrue( + IntakeRollerConstants.Setpoints.OpenLoop.EJECT_PERCENTAGE + <= IntakeRollerConstants.Limits.MAX_OPEN_LOOP_EJECT_PERCENTAGE); + } + + @Test + void testIntakeLimitNotExceedingGlobalLimit() { + assertTrue( + IntakeRollerConstants.Limits.MAX_OPEN_LOOP_INTAKE_PERCENTAGE + <= IntakeRollerConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE); + } + + @Test + void testEjectLimitNotExceedingGlobalLimit() { + assertTrue( + IntakeRollerConstants.Limits.MAX_OPEN_LOOP_EJECT_PERCENTAGE + <= IntakeRollerConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/subsystems/kicker/KickerSubsystemTest.java b/src/test/java/frc/alotobots/rebuilt/subsystems/kicker/KickerSubsystemTest.java new file mode 100644 index 0000000..f6835b8 --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/subsystems/kicker/KickerSubsystemTest.java @@ -0,0 +1,124 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.subsystems.kicker; + +import static edu.wpi.first.units.Units.RadiansPerSecond; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import edu.wpi.first.units.measure.AngularVelocity; +import frc.alotobots.rebuilt.subsystems.kicker.constants.KickerConstants; +import frc.alotobots.rebuilt.subsystems.kicker.io.KickerIO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class KickerSubsystemTest { + + private KickerIO mockIO; + private KickerSubsystem subsystem; + + @BeforeEach + void setUp() { + mockIO = mock(KickerIO.class); + subsystem = new KickerSubsystem(mockIO); + } + + @Test + void testRunToTargetVelocity() { + AngularVelocity velocity = RadiansPerSecond.of(50); + subsystem.runToTargetVelocity(velocity); + + verify(mockIO).setKickerVelocity(any(AngularVelocity.class)); + } + + @Test + void testRunToTargetVelocityClampedAtMax() { + AngularVelocity highVelocity = + RadiansPerSecond.of(KickerConstants.Limits.MAX_SPEED.in(RadiansPerSecond) * 2); + subsystem.runToTargetVelocity(highVelocity); + + verify(mockIO) + .setKickerVelocity( + argThat( + v -> + v.in(RadiansPerSecond) + <= KickerConstants.Limits.MAX_SPEED.in(RadiansPerSecond))); + } + + @Test + void testRunToTargetVelocityClampedAtMin() { + AngularVelocity lowVelocity = + RadiansPerSecond.of(-KickerConstants.Limits.MAX_SPEED.in(RadiansPerSecond) * 2); + subsystem.runToTargetVelocity(lowVelocity); + + verify(mockIO) + .setKickerVelocity( + argThat( + v -> + v.in(RadiansPerSecond) + >= -KickerConstants.Limits.MAX_SPEED.in(RadiansPerSecond))); + } + + @Test + void testRunAtPercentOutput() { + subsystem.runAtPercentOutput(0.5); + + verify(mockIO).setKickerOpenLoop(0.5); + } + + @Test + void testRunAtPercentOutputClampedAtMax() { + subsystem.runAtPercentOutput(1.5); + + verify(mockIO) + .setKickerOpenLoop(argThat(v -> v <= KickerConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE)); + } + + @Test + void testRunAtPercentOutputClampedAtMin() { + subsystem.runAtPercentOutput(-1.5); + + verify(mockIO) + .setKickerOpenLoop(argThat(v -> v >= -KickerConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE)); + } + + @Test + void testStop() { + subsystem.stop(); + + verify(mockIO).stop(); + } + + @Test + void testPeriodicCallsUpdateInputs() { + subsystem.periodic(); + + verify(mockIO).updateInputs(any()); + } + + @Test + void testZeroVelocity() { + AngularVelocity velocity = RadiansPerSecond.of(0); + subsystem.runToTargetVelocity(velocity); + + verify(mockIO).setKickerVelocity(velocity); + } + + @Test + void testNegativeVelocity() { + AngularVelocity velocity = RadiansPerSecond.of(-30); + subsystem.runToTargetVelocity(velocity); + + verify(mockIO).setKickerVelocity(any(AngularVelocity.class)); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/subsystems/kicker/commands/DefaultKickerRunAtVelocityTest.java b/src/test/java/frc/alotobots/rebuilt/subsystems/kicker/commands/DefaultKickerRunAtVelocityTest.java new file mode 100644 index 0000000..9469234 --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/subsystems/kicker/commands/DefaultKickerRunAtVelocityTest.java @@ -0,0 +1,115 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.subsystems.kicker.commands; + +import static edu.wpi.first.units.Units.RadiansPerSecond; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import edu.wpi.first.units.measure.AngularVelocity; +import frc.alotobots.rebuilt.subsystems.kicker.KickerSubsystem; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DefaultKickerRunAtVelocityTest { + + private KickerSubsystem mockSubsystem; + private Supplier velocitySupplier; + private DefaultKickerRunAtVelocity command; + + @BeforeEach + void setUp() { + mockSubsystem = mock(KickerSubsystem.class); + velocitySupplier = () -> RadiansPerSecond.of(50); + command = new DefaultKickerRunAtVelocity(mockSubsystem, velocitySupplier); + } + + @Test + void testCommandRequiresSubsystem() { + assertTrue(command.getRequirements().contains(mockSubsystem)); + } + + @Test + void testExecuteCallsSubsystemWithVelocity() { + command.execute(); + + verify(mockSubsystem).runToTargetVelocity(any(AngularVelocity.class)); + } + + @Test + void testExecuteUsesSuppliedVelocity() { + AngularVelocity testVelocity = RadiansPerSecond.of(75); + Supplier testSupplier = () -> testVelocity; + DefaultKickerRunAtVelocity testCommand = + new DefaultKickerRunAtVelocity(mockSubsystem, testSupplier); + + testCommand.execute(); + + verify(mockSubsystem).runToTargetVelocity(testVelocity); + } + + @Test + void testEndCallsStop() { + command.end(false); + + verify(mockSubsystem).stop(); + } + + @Test + void testEndCallsStopWhenInterrupted() { + command.end(true); + + verify(mockSubsystem).stop(); + } + + @Test + void testIsFinishedReturnsFalse() { + assertFalse(command.isFinished()); + } + + @Test + void testInitializeDoesNotThrow() { + assertDoesNotThrow(() -> command.initialize()); + } + + @Test + void testMultipleExecuteCalls() { + command.execute(); + command.execute(); + command.execute(); + + verify(mockSubsystem, times(3)).runToTargetVelocity(any(AngularVelocity.class)); + } + + @Test + void testDynamicVelocityChange() { + // Test that the command uses a fresh velocity value each execute + AngularVelocity[] velocities = { + RadiansPerSecond.of(10), RadiansPerSecond.of(20), RadiansPerSecond.of(30) + }; + int[] index = {0}; + Supplier dynamicSupplier = () -> velocities[index[0]++]; + DefaultKickerRunAtVelocity dynamicCommand = + new DefaultKickerRunAtVelocity(mockSubsystem, dynamicSupplier); + + dynamicCommand.execute(); + verify(mockSubsystem).runToTargetVelocity(velocities[0]); + + dynamicCommand.execute(); + verify(mockSubsystem).runToTargetVelocity(velocities[1]); + + dynamicCommand.execute(); + verify(mockSubsystem).runToTargetVelocity(velocities[2]); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/subsystems/kicker/constants/KickerConstantsTest.java b/src/test/java/frc/alotobots/rebuilt/subsystems/kicker/constants/KickerConstantsTest.java new file mode 100644 index 0000000..2fd3923 --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/subsystems/kicker/constants/KickerConstantsTest.java @@ -0,0 +1,66 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.subsystems.kicker.constants; + +import static edu.wpi.first.units.Units.DegreesPerSecond; +import static edu.wpi.first.units.Units.RotationsPerSecond; +import static edu.wpi.first.units.Units.Seconds; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class KickerConstantsTest { + + @Test + void testThresholdConstants() { + assertTrue( + KickerConstants.Thresholds.AT_TARGET_VELOCITY_SPEED_THRESHOLD.in(DegreesPerSecond) > 0); + assertTrue(KickerConstants.Thresholds.AT_TARGET_VELOCITY_TIME_THRESHOLD.in(Seconds) > 0); + } + + @Test + void testLimitConstants() { + assertTrue(KickerConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE > 0); + assertTrue(KickerConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE <= 1.0); + assertTrue(KickerConstants.Limits.MAX_SPEED.in(RotationsPerSecond) > 0); + } + + @Test + void testSetpointConstants() { + assertTrue(KickerConstants.Setpoints.LOAD_INTO_SHOOTER_VELOCITY.in(RotationsPerSecond) > 0); + } + + @Test + void testLimitsEnabled() { + assertNotNull(KickerConstants.Limits.LIMITS_ENABLED); + } + + @Test + void testSetpointWithinLimits() { + assertTrue( + KickerConstants.Setpoints.LOAD_INTO_SHOOTER_VELOCITY.in(RotationsPerSecond) + <= KickerConstants.Limits.MAX_SPEED.in(RotationsPerSecond)); + } + + @Test + void testMaxOpenLoopPercentageReasonable() { + assertTrue(KickerConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE >= 0.0); + assertTrue(KickerConstants.Limits.MAX_OPEN_LOOP_PERCENTAGE <= 1.0); + } + + @Test + void testTimeThresholdReasonable() { + // Time threshold should be less than 1 second for responsiveness + assertTrue(KickerConstants.Thresholds.AT_TARGET_VELOCITY_TIME_THRESHOLD.in(Seconds) < 1.0); + } +} \ No newline at end of file diff --git a/src/test/java/frc/alotobots/rebuilt/subsystems/launcher/deflector/DeflectorSubsystemTest.java b/src/test/java/frc/alotobots/rebuilt/subsystems/launcher/deflector/DeflectorSubsystemTest.java new file mode 100644 index 0000000..5bda112 --- /dev/null +++ b/src/test/java/frc/alotobots/rebuilt/subsystems/launcher/deflector/DeflectorSubsystemTest.java @@ -0,0 +1,175 @@ +/* + * ALOTOBOTS - FRC Team 5152 + https://github.com/5152Alotobots + * Copyright (C) 2026 ALOTOBOTS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Source code must be publicly available on GitHub or an alternative web accessible site + */ +package frc.alotobots.rebuilt.subsystems.launcher.deflector; + +import static edu.wpi.first.units.Units.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import edu.wpi.first.units.measure.Angle; +import edu.wpi.first.units.measure.AngularVelocity; +import frc.alotobots.rebuilt.subsystems.launcher.deflector.constants.DeflectorConstants; +import frc.alotobots.rebuilt.subsystems.launcher.deflector.io.DeflectorIO; +import frc.alotobots.rebuilt.subsystems.launcher.deflector.io.DeflectorIOInputsAutoLogged; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DeflectorSubsystemTest { + + private DeflectorIO mockIO; + private DeflectorSubsystem subsystem; + + @BeforeEach + void setUp() { + mockIO = mock(DeflectorIO.class); + subsystem = new DeflectorSubsystem(mockIO); + } + + @Test + void testRunToTargetAngle() { + Angle angle = Radians.of(0.5); + subsystem.runToTargetAngle(angle); + + verify(mockIO).setDeflectorPosition(any(Angle.class), any()); + } + + @Test + void testRunToTargetAngleClampedAtMax() { + Angle highAngle = Radians.of(DeflectorConstants.Limits.DEFLECTOR_MAX_ANGLE.in(Radians) * 2); + subsystem.runToTargetAngle(highAngle); + + verify(mockIO) + .setDeflectorPosition( + argThat( + a -> + a.in(Radians) + <= DeflectorConstants.Limits.DEFLECTOR_MAX_ANGLE.in(Radians)), + any()); + } + + @Test + void testRunToTargetAngleClampedAtMin() { + Angle lowAngle = Radians.of(-5.0); + subsystem.runToTargetAngle(lowAngle); + + verify(mockIO) + .setDeflectorPosition( + argThat( + a -> + a.in(Radians) + >= DeflectorConstants.Limits.DEFLECTOR_MIN_ANGLE.in(Radians)), + any()); + } + + @Test + void testRunToTargetVelocity() { + AngularVelocity velocity = RadiansPerSecond.of(0.5); + subsystem.runToTargetVelocity(velocity); + + verify(mockIO).setDeflectorVelocity(any(AngularVelocity.class)); + } + + @Test + void testRunToTargetVelocityClampedAtMax() { + AngularVelocity highVelocity = + RadiansPerSecond.of(DeflectorConstants.Limits.DEFLECTOR_MAX_VELOCITY.in(RadiansPerSecond) * 2); + subsystem.runToTargetVelocity(highVelocity); + + verify(mockIO) + .setDeflectorVelocity( + argThat( + v -> + v.in(RadiansPerSecond) + <= DeflectorConstants.Limits.DEFLECTOR_MAX_VELOCITY.in( + RadiansPerSecond))); + } + + @Test + void testRunAtPercentOutput() { + subsystem.runAtPercentOutput(0.5); + + verify(mockIO).setDeflectorOpenLoop(0.5); + } + + @Test + void testRunAtPercentOutputClampedAtMax() { + subsystem.runAtPercentOutput(1.5); + + verify(mockIO) + .setDeflectorOpenLoop( + argThat(v -> v <= DeflectorConstants.Limits.DEFLECTOR_MAX_OPEN_LOOP_PERCENTAGE)); + } + + @Test + void testRunAtPercentOutputClampedAtMin() { + subsystem.runAtPercentOutput(-1.5); + + verify(mockIO) + .setDeflectorOpenLoop( + argThat(v -> v >= -DeflectorConstants.Limits.DEFLECTOR_MAX_OPEN_LOOP_PERCENTAGE)); + } + + @Test + void testStop() { + subsystem.stop(); + + verify(mockIO).stop(); + } + + @Test + void testPeriodicCallsUpdateInputs() { + subsystem.periodic(); + + verify(mockIO).updateInputs(any()); + } + + @Test + void testGetCurrentAngle() { + DeflectorIOInputsAutoLogged inputs = new DeflectorIOInputsAutoLogged(); + inputs.deflectorAngle = Radians.of(0.3); + + when(mockIO.updateInputs(any())) + .thenAnswer( + invocation -> { + DeflectorIOInputsAutoLogged arg = invocation.getArgument(0); + arg.deflectorAngle = inputs.deflectorAngle; + return null; + }); + + subsystem.periodic(); + Angle angle = subsystem.getCurrentAngle(); + + assertNotNull(angle); + } + + @Test + void testIsAtTargetAngleInitiallyFalse() { + assertFalse(subsystem.isAtTargetAngle()); + } + + @Test + void testZeroAngle() { + Angle angle = Radians.of(0); + subsystem.runToTargetAngle(angle); + + verify(mockIO).setDeflectorPosition(any(Angle.class), any()); + } + + @Test + void testNegativeVelocity() { + AngularVelocity velocity = RadiansPerSecond.of(-0.3); + subsystem.runToTargetVelocity(velocity); + + verify(mockIO).setDeflectorVelocity(any(AngularVelocity.class)); + } +} \ No newline at end of file