From 3d0fb5a7272358e4bae8670a1ff8baaf533c1fd5 Mon Sep 17 00:00:00 2001 From: cuttestkittensrule Date: Wed, 21 Jan 2026 23:13:04 -0800 Subject: [PATCH 1/8] Make gradlew executable --- gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From 5fc25b6497dcd25edac8ddb880b8d8fbcb720df7 Mon Sep 17 00:00:00 2001 From: cuttestkittensrule Date: Wed, 21 Jan 2026 23:14:04 -0800 Subject: [PATCH 2/8] Ensure that the odometry lock is unlocked if an exception is thrown --- .../java/com/team2813/subsystems/drive/Drive.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/team2813/subsystems/drive/Drive.java b/src/main/java/com/team2813/subsystems/drive/Drive.java index 1afc7b48..b0bef1a2 100644 --- a/src/main/java/com/team2813/subsystems/drive/Drive.java +++ b/src/main/java/com/team2813/subsystems/drive/Drive.java @@ -151,13 +151,16 @@ public Drive( @Override public void periodic() { - odometryLock.lock(); // Prevents odometry updates while reading data - gyroIO.updateInputs(gyroInputs); - Logger.processInputs("Drive/Gyro", gyroInputs); - for (var module : modules) { - module.periodic(); + try { + odometryLock.lock(); // Prevents odometry updates while reading data + gyroIO.updateInputs(gyroInputs); + Logger.processInputs("Drive/Gyro", gyroInputs); + for (var module : modules) { + module.periodic(); + } + } finally { + odometryLock.unlock(); } - odometryLock.unlock(); // Stop moving when disabled if (DriverStation.isDisabled()) { From ccfc29510eeb6635ec687e541cb10a8a982a2b09 Mon Sep 17 00:00:00 2001 From: cuttestkittensrule Date: Wed, 21 Jan 2026 23:18:54 -0800 Subject: [PATCH 3/8] Use method references instead of lambdas, and actually make the hopper commands depend on the hopper --- src/main/java/com/team2813/subsystems/hopper/Hopper.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/team2813/subsystems/hopper/Hopper.java b/src/main/java/com/team2813/subsystems/hopper/Hopper.java index 9dcf1f88..315e8bbb 100644 --- a/src/main/java/com/team2813/subsystems/hopper/Hopper.java +++ b/src/main/java/com/team2813/subsystems/hopper/Hopper.java @@ -38,14 +38,14 @@ public void stop() { } public Command intakeCommand() { - return new InstantCommand(() -> intake()); + return new InstantCommand(this::intake, this); } public Command outtakeCommand() { - return new InstantCommand(() -> outtake()); + return new InstantCommand(this::outtake, this); } public Command stopCommand() { - return new InstantCommand(() -> stop()); + return new InstantCommand(this::stop, this); } } From f6c7fd0d352fc0eaecdd65a1a57c35325e723d91 Mon Sep 17 00:00:00 2001 From: cuttestkittensrule Date: Wed, 21 Jan 2026 23:19:36 -0800 Subject: [PATCH 4/8] Remove hopper placeholder file --- .../java/com/team2813/subsystems/hopper/Placeholder.md | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 src/main/java/com/team2813/subsystems/hopper/Placeholder.md diff --git a/src/main/java/com/team2813/subsystems/hopper/Placeholder.md b/src/main/java/com/team2813/subsystems/hopper/Placeholder.md deleted file mode 100644 index 2e729eaf..00000000 --- a/src/main/java/com/team2813/subsystems/hopper/Placeholder.md +++ /dev/null @@ -1,10 +0,0 @@ -## Hardware Devices Include: -- One motor to run the hot dog rollers - - Moves the fuel toward the indexer and shooter. - -## Subsystem Hardware Capabilities: - - -## Other Notes: -- Hot dog roller motor invert should be CCW. - - Fuel will move toward indexer From 4138c302047d4ea906754281b2d6b60a2999f627 Mon Sep 17 00:00:00 2001 From: cuttestkittensrule Date: Wed, 21 Jan 2026 23:26:34 -0800 Subject: [PATCH 5/8] Move autonomous command canceling to teleopExit Makes sure it is canceled if something other than teleop is started, so nothing weird happens --- src/main/java/com/team2813/Robot.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/team2813/Robot.java b/src/main/java/com/team2813/Robot.java index 1b4b9588..c030a6f5 100644 --- a/src/main/java/com/team2813/Robot.java +++ b/src/main/java/com/team2813/Robot.java @@ -112,18 +112,19 @@ public void autonomousInit() { @Override public void autonomousPeriodic() {} - /** This function is called once when teleop is enabled. */ @Override - public void teleopInit() { - // This makes sure that the autonomous stops running when - // teleop starts running. If you want the autonomous to - // continue until interrupted by another command, remove - // this line or comment it out. + public void autonomousExit() { + // This makes sure that the autonomous command isn't running after we exit the autonomous + // period if (autonomousCommand != null) { autonomousCommand.cancel(); } } + /** This function is called once when teleop is enabled. */ + @Override + public void teleopInit() {} + /** This function is called periodically during operator control. */ @Override public void teleopPeriodic() {} From 5272a68b7e6bee890092d93aa5ce5641f46a8987 Mon Sep 17 00:00:00 2001 From: cuttestkittensrule Date: Wed, 21 Jan 2026 23:30:34 -0800 Subject: [PATCH 6/8] Use our time zone for the build constants --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6bd6d9e2..45870e69 100644 --- a/build.gradle +++ b/build.gradle @@ -138,7 +138,7 @@ gversion { classPackage = "com.team2813" className = "BuildConstants" dateFormat = "yyyy-MM-dd HH:mm:ss z" - timeZone = "America/New_York" + timeZone = "America/Los_Angeles" indent = " " } From bd760f509da2589af71dd4fcd43439d348bd385b Mon Sep 17 00:00:00 2001 From: cuttestkittensrule Date: Wed, 21 Jan 2026 23:51:38 -0800 Subject: [PATCH 7/8] Prevent TOCTOU error TOCTOU -> time of check/time of use. The Driver station alliance could theorhetically change from having an alliance to not having an alliance in between the DriverStation#getAlliance() call, especially with JNI being involved, and could throw an exception. To prevent this, it is called only once, and then converted to the information on if it should be flipped. You could change this to be the same as before, but with storing the driver station alliance in a variable, and it would work. --- .../java/com/team2813/commands/DriveCommands.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/team2813/commands/DriveCommands.java b/src/main/java/com/team2813/commands/DriveCommands.java index 05448b80..ab66e026 100644 --- a/src/main/java/com/team2813/commands/DriveCommands.java +++ b/src/main/java/com/team2813/commands/DriveCommands.java @@ -83,9 +83,16 @@ public static Command joystickDrive( linearVelocity.getX() * drive.getMaxLinearSpeedMetersPerSec(), linearVelocity.getY() * drive.getMaxLinearSpeedMetersPerSec(), omega * drive.getMaxAngularSpeedRadPerSec()); + + // We have to call DriverStation#getAlliance() only once, since the return value could + // change in between calls to it. So, we turn the Optional from + // DriverStation#getAlliance() to an Optional by mapping a Red alliance to + // `true`, and defaulting to `false` if the DriverStation#getAlliance() returned an empty + // optional. This achieves the intended behavior of having `true` if, and only if + // DriverStation#getAlliance() returned a non-empty option with Alliance.Red, but avoiding + // accidentally unwrapping an empty optional boolean isFlipped = - DriverStation.getAlliance().isPresent() - && DriverStation.getAlliance().get() == Alliance.Red; + DriverStation.getAlliance().map((alliance) -> alliance == Alliance.Red).orElse(false); drive.runVelocity( ChassisSpeeds.fromFieldRelativeSpeeds( speeds, From 54991b844b415ccb420b29e731cedda9bf5ee837 Mon Sep 17 00:00:00 2001 From: cuttestkittensrule Date: Thu, 22 Jan 2026 20:19:18 -0800 Subject: [PATCH 8/8] Add a test to make sure that we can create a robot container and run a periodic cycle without crashing --- src/test/java/com/team2813/CommandTester.java | 33 +++++ .../java/com/team2813/RobotContainerTest.java | 19 +++ .../java/com/team2813/WPILibExtension.java | 126 ++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 src/test/java/com/team2813/CommandTester.java create mode 100644 src/test/java/com/team2813/RobotContainerTest.java create mode 100644 src/test/java/com/team2813/WPILibExtension.java diff --git a/src/test/java/com/team2813/CommandTester.java b/src/test/java/com/team2813/CommandTester.java new file mode 100644 index 00000000..18ad11cf --- /dev/null +++ b/src/test/java/com/team2813/CommandTester.java @@ -0,0 +1,33 @@ +/* +Copyright 2025-2026 Prospect Robotics SWENext Club + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// This is taken from [lib2813](https://github.com/Prospect-Robotics/lib2813/blob/main/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/CommandTester.java). +// When lib2813 is on maven central, this file can be deleted in favor of adding a dependency on lib2813. +package com.team2813; + +import edu.wpi.first.wpilibj2.command.Command; + +/** + * Allows tests to run commands. + * + *

Tests can get an instance by using {@link WPILibExtension}. + * + * @since 2.0.0 + */ +public interface CommandTester { + + /** Schedules the provided command and runs it until it completes. */ + void runUntilComplete(Command command); +} diff --git a/src/test/java/com/team2813/RobotContainerTest.java b/src/test/java/com/team2813/RobotContainerTest.java new file mode 100644 index 00000000..5ddc4683 --- /dev/null +++ b/src/test/java/com/team2813/RobotContainerTest.java @@ -0,0 +1,19 @@ +package com.team2813; + +import edu.wpi.first.wpilibj2.command.Commands; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(WPILibExtension.class) +public class RobotContainerTest { + @Test + public void canCreateRobotAndRunPeriodic(CommandTester tester) { + Assumptions.assumeTrue( + Constants.simMode == Constants.Mode.SIM, "The sim mode must be sim to run tests!"); + // create a robot container + RobotContainer robotContainer = new RobotContainer(); + // Run one periodic cycle + tester.runUntilComplete(Commands.none()); + } +} diff --git a/src/test/java/com/team2813/WPILibExtension.java b/src/test/java/com/team2813/WPILibExtension.java new file mode 100644 index 00000000..8344c78d --- /dev/null +++ b/src/test/java/com/team2813/WPILibExtension.java @@ -0,0 +1,126 @@ +/* +Copyright 2025-2026 Prospect Robotics SWENext Club + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.team2813; + +import edu.wpi.first.hal.HAL; +import edu.wpi.first.wpilibj.RuntimeType; +import edu.wpi.first.wpilibj.TimedRobot; +import edu.wpi.first.wpilibj.simulation.DriverStationSim; +import edu.wpi.first.wpilibj.simulation.SimHooks; +import edu.wpi.first.wpilibj2.command.CommandScheduler; +import org.junit.jupiter.api.extension.*; + +/** + * JUnit Jupiter extension for testing code that depends on WPILib. + * + *

Also provides a {@link CommandTester} for tests. + * + *

Example use: + * + *

{@code
+ * @ExtendWith(WPILibExtension.class)
+ * public final class FlightSubsystemTest {
+ *
+ *   @Test
+ *   public void initiallyNotInAir() {
+ *     var flight = new FlightSubsystem();
+ *
+ *     assertThat(flight.inAir()).isFalse();
+ *   }
+ *
+ *   @Test
+ *   public void takesFlight(CommandTester commandTester) {
+ *     var flight = new FlightSubsystem();
+ *     Command takeOff = flight.createTakeOffCommandCommand();
+ *
+ *     commandTester.runUntilComplete(takeOff);
+ *
+ *     assertThat(flight.inAir()).isTrue();
+ *   }
+ * }
+ * }
+ * + * @since 2.0.0 + */ +// This is taken from [lib2813](https://github.com/Prospect-Robotics/lib2813/blob/main/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/WPILibExtension.java). +// When lib2813 is on maven central, this file can be deleted in favor of adding a dependency on lib2813. +public final class WPILibExtension + implements Extension, + AfterAllCallback, + AfterEachCallback, + BeforeAllCallback, + ParameterResolver { + private static final double NANOS_PER_SECOND = 1_000_000_000d; + + @Override + public void beforeAll(ExtensionContext context) { + // See https://www.chiefdelphi.com/t/driverstation-getalliance-in-gradle-test/ + if (!HAL.initialize(500, 0)) { + throw new IllegalStateException("Could not initialize Hardware Abstraction Layer"); + } + DriverStationSim.setEnabled(true); + DriverStationSim.notifyNewData(); + SimHooks.setHALRuntimeType(RuntimeType.kSimulation.value); + + CommandScheduler commandScheduler = CommandScheduler.getInstance(); + commandScheduler.enable(); + commandScheduler.cancelAll(); + commandScheduler.unregisterAllSubsystems(); + } + + @Override + public void afterEach(ExtensionContext context) { + CommandScheduler commandScheduler = CommandScheduler.getInstance(); + commandScheduler.cancelAll(); + commandScheduler.unregisterAllSubsystems(); + } + + @Override + public void afterAll(ExtensionContext context) { + CommandScheduler commandScheduler = CommandScheduler.getInstance(); + commandScheduler.cancelAll(); + commandScheduler.unregisterAllSubsystems(); + commandScheduler.disable(); + DriverStationSim.setEnabled(false); + DriverStationSim.notifyNewData(); + } + + @Override + public boolean supportsParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return CommandTester.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public CommandTester resolveParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) { + CommandScheduler scheduler = CommandScheduler.getInstance(); + + return command -> { + SimHooks.pauseTiming(); + try { + scheduler.schedule(command); + do { + scheduler.run(); + SimHooks.stepTiming(TimedRobot.kDefaultPeriod); + } while (scheduler.isScheduled(command)); + } finally { + SimHooks.resumeTiming(); + } + }; + } +}