From 893953d15114fef8eedc3d50fd131a42d1e6634e Mon Sep 17 00:00:00 2001 From: Kevin Cooney Date: Fri, 26 Sep 2025 17:02:59 -0700 Subject: [PATCH 1/3] Introduce FakeMotor --- .../ParameterizedIntakeSubsystemTest.java | 27 ++++------ .../team2813/lib2813/testing/FakeMotor.java | 54 +++++++++++++++++++ .../lib2813/testing/FakePIDMotor.java | 28 ++-------- 3 files changed, 68 insertions(+), 41 deletions(-) create mode 100644 testing/src/main/java/com/team2813/lib2813/testing/FakeMotor.java diff --git a/lib/src/test/java/com/team2813/lib2813/subsystems/ParameterizedIntakeSubsystemTest.java b/lib/src/test/java/com/team2813/lib2813/subsystems/ParameterizedIntakeSubsystemTest.java index a7059c4e..45eea151 100644 --- a/lib/src/test/java/com/team2813/lib2813/subsystems/ParameterizedIntakeSubsystemTest.java +++ b/lib/src/test/java/com/team2813/lib2813/subsystems/ParameterizedIntakeSubsystemTest.java @@ -1,12 +1,11 @@ package com.team2813.lib2813.subsystems; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verifyNoInteractions; +import static org.junit.jupiter.api.Assumptions.*; import com.team2813.lib2813.control.ControlMode; -import com.team2813.lib2813.control.PIDMotor; -import com.team2813.lib2813.testing.FakePIDMotor; +import com.team2813.lib2813.control.Motor; +import com.team2813.lib2813.testing.FakeMotor; import com.team2813.lib2813.testing.junit.jupiter.CommandTester; import com.team2813.lib2813.testing.junit.jupiter.WPILibExtension; import edu.wpi.first.wpilibj2.command.Command; @@ -14,7 +13,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.provider.EnumSource; -import org.mockito.Answers; @ParameterizedClass @EnumSource(ControlMode.class) @@ -22,12 +20,12 @@ public final class ParameterizedIntakeSubsystemTest { private static class ConcreteParameterizedIntakeSubsystem extends ParameterizedIntakeSubsystem { - protected ConcreteParameterizedIntakeSubsystem(PIDMotor intakeMotor, Params params) { + protected ConcreteParameterizedIntakeSubsystem(Motor intakeMotor, Params params) { super(intakeMotor, params); } } - final FakePIDMotor fakeMotor = mock(FakePIDMotor.class, Answers.CALLS_REAL_METHODS); + private final FakeMotor fakeMotor = new FakeMotor(); private final ParameterizedIntakeSubsystem.Params params; public ParameterizedIntakeSubsystemTest(ControlMode controlMode) { @@ -42,8 +40,7 @@ public ParameterizedIntakeSubsystemTest(ControlMode controlMode) { @Test public void initialState() { try (var ignored = new ConcreteParameterizedIntakeSubsystem(fakeMotor, params)) { - assertMotorIsStopped(); - verifyNoInteractions(fakeMotor); + fakeMotor.assertIsStopped(); } } @@ -51,7 +48,7 @@ public void initialState() { public void intakeItem(CommandTester commandTester) { try (var intake = new ConcreteParameterizedIntakeSubsystem(fakeMotor, params)) { Command command = intake.intakeItemCommand(); - assertMotorIsStopped(); + fakeMotor.assertIsStopped(); commandTester.runUntilComplete(command); @@ -61,6 +58,7 @@ public void intakeItem(CommandTester commandTester) { @Test public void stopAfterIntakingItem(CommandTester commandTester) { + assumeTrue(!ControlMode.MOTION_MAGIC.equals(params.controlMode())); try (var intake = new ConcreteParameterizedIntakeSubsystem(fakeMotor, params)) { Command command = intake.intakeItemCommand(); commandTester.runUntilComplete(command); @@ -69,7 +67,7 @@ public void stopAfterIntakingItem(CommandTester commandTester) { commandTester.runUntilComplete(command); - assertMotorIsStopped(); + fakeMotor.assertIsStopped(); } } @@ -88,6 +86,7 @@ public void outtakeItem(CommandTester commandTester) { @Test public void stopAfterOuttakingItem(CommandTester commandTester) { + assumeTrue(!ControlMode.MOTION_MAGIC.equals(params.controlMode())); try (var intake = new ConcreteParameterizedIntakeSubsystem(fakeMotor, params)) { intake.intakeGamePiece(); Command command = intake.outtakeItemCommand(); @@ -97,14 +96,10 @@ public void stopAfterOuttakingItem(CommandTester commandTester) { commandTester.runUntilComplete(command); - assertMotorIsStopped(); + fakeMotor.assertIsStopped(); } } - private void assertMotorIsStopped() { - assertThat(fakeMotor.demand).isWithin(0.01).of(0.0); - } - private void assertMotorIsRunning() { assertThat(fakeMotor.demand).isNotWithin(0.01).of(0.0); } diff --git a/testing/src/main/java/com/team2813/lib2813/testing/FakeMotor.java b/testing/src/main/java/com/team2813/lib2813/testing/FakeMotor.java new file mode 100644 index 00000000..e4cbf377 --- /dev/null +++ b/testing/src/main/java/com/team2813/lib2813/testing/FakeMotor.java @@ -0,0 +1,54 @@ +package com.team2813.lib2813.testing; + +import static com.google.common.truth.Truth.assertThat; + +import com.team2813.lib2813.control.ControlMode; +import com.team2813.lib2813.control.Motor; +import edu.wpi.first.units.Units; +import edu.wpi.first.units.measure.Current; +import edu.wpi.first.units.measure.Resistance; +import edu.wpi.first.units.measure.Voltage; +import java.util.Objects; + +public class FakeMotor implements Motor { + private boolean isStopped = true; + public double demand = 0.0f; + public Resistance resistance = Resistance.ofBaseUnits(0.025f, Units.Ohms); + private ControlMode controlMode = ControlMode.VOLTAGE; + + public Voltage getMotorVoltage() { + if (!ControlMode.VOLTAGE.equals(controlMode)) { + throw new IllegalStateException("Cannot get voltage when controlMode is " + controlMode); + } + return Voltage.ofBaseUnits(demand, Units.Volts); + } + + public final void assertIsStopped() { + assertThat(isStopped).isTrue(); + } + + @Override + public final void set(ControlMode mode, double demand) { + controlMode = Objects.requireNonNull(mode, "mode should not be null"); + if (controlMode != ControlMode.MOTION_MAGIC) { + isStopped = (Math.abs(demand) <= 0.0001); + } + this.demand = demand; + } + + @Override + public void set(ControlMode mode, double demand, double feedForward) { + set(mode, demand); + } + + @Override + public final Current getAppliedCurrent() { + try { + return getMotorVoltage().div(resistance); + } catch (IllegalStateException e) { + throw new IllegalStateException("Cannot calculate voltage", e); + } catch (RuntimeException e) { + throw new RuntimeException("Cannot calculate voltage", e); + } + } +} diff --git a/testing/src/main/java/com/team2813/lib2813/testing/FakePIDMotor.java b/testing/src/main/java/com/team2813/lib2813/testing/FakePIDMotor.java index b3d90037..407903b9 100644 --- a/testing/src/main/java/com/team2813/lib2813/testing/FakePIDMotor.java +++ b/testing/src/main/java/com/team2813/lib2813/testing/FakePIDMotor.java @@ -1,35 +1,13 @@ package com.team2813.lib2813.testing; -import com.google.common.truth.Truth; -import com.team2813.lib2813.control.ControlMode; import com.team2813.lib2813.control.PIDMotor; import edu.wpi.first.units.Units; -import edu.wpi.first.units.measure.AngularVelocity; -public abstract class FakePIDMotor implements PIDMotor { - public double demand = 0.0f; - private ControlMode controlMode; +public abstract class FakePIDMotor extends FakeMotor implements PIDMotor { + @Deprecated public double getVoltage() { - Truth.assertThat(controlMode).isEqualTo(ControlMode.VOLTAGE); - return demand; - } - - @Override - public void set(ControlMode mode, double demand) { - Truth.assertThat(mode).isNotNull(); - controlMode = mode; - this.demand = demand; - } - - @Override - public void set(ControlMode mode, double demand, double feedForward) { - set(mode, demand); - } - - @Override - public AngularVelocity getVelocityMeasure() { - return Units.RadiansPerSecond.of(demand * 20); + return getMotorVoltage().in(Units.Volts); } @Override From 631bf962be97622497c6f7033255f5c3d8fa47a2 Mon Sep 17 00:00:00 2001 From: Kevin Cooney Date: Sat, 27 Sep 2025 12:38:14 -0700 Subject: [PATCH 2/3] Add getFeedForward() --- .../team2813/lib2813/testing/FakeMotor.java | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/testing/src/main/java/com/team2813/lib2813/testing/FakeMotor.java b/testing/src/main/java/com/team2813/lib2813/testing/FakeMotor.java index e4cbf377..040383a8 100644 --- a/testing/src/main/java/com/team2813/lib2813/testing/FakeMotor.java +++ b/testing/src/main/java/com/team2813/lib2813/testing/FakeMotor.java @@ -12,7 +12,9 @@ public class FakeMotor implements Motor { private boolean isStopped = true; + private double feedForward = 0.0f; public double demand = 0.0f; + public Resistance resistance = Resistance.ofBaseUnits(0.025f, Units.Ohms); private ControlMode controlMode = ControlMode.VOLTAGE; @@ -23,22 +25,44 @@ public Voltage getMotorVoltage() { return Voltage.ofBaseUnits(demand, Units.Volts); } + /** + * Gets the feedforward that was applied. + * + * @return feedforward, in fractional units between -1 and +1. + */ + public double getFeedForward() { + return switch (controlMode) { + case VELOCITY, MOTION_MAGIC -> feedForward; + default -> + throw new IllegalStateException( + "Cannot get feedforward when controlMode is " + controlMode); + }; + } + public final void assertIsStopped() { assertThat(isStopped).isTrue(); } @Override public final void set(ControlMode mode, double demand) { - controlMode = Objects.requireNonNull(mode, "mode should not be null"); - if (controlMode != ControlMode.MOTION_MAGIC) { - isStopped = (Math.abs(demand) <= 0.0001); - } - this.demand = demand; + set(mode, demand, 0); } @Override public void set(ControlMode mode, double demand, double feedForward) { - set(mode, demand); + Objects.requireNonNull(mode, "mode should not be null"); + if (feedForward >= 0) { + assertThat(feedForward).isAtMost(1.01); + } else { + assertThat(feedForward).isAtLeast(-1.01); + } + + if (controlMode != ControlMode.MOTION_MAGIC) { + isStopped = (Math.abs(demand) <= 0.0001 && Math.abs(feedForward) <= 0.0001); + } + this.controlMode = mode; + this.demand = demand; + this.feedForward = feedForward; } @Override From 871401fcd413612123930c5bc99ddb14bab7697a Mon Sep 17 00:00:00 2001 From: Kevin Cooney Date: Sat, 11 Oct 2025 15:39:16 -0700 Subject: [PATCH 3/3] Improve Javadoc --- .../team2813/lib2813/testing/FakeMotor.java | 28 +++++++++++++++++++ .../lib2813/testing/FakePIDMotor.java | 6 ++++ 2 files changed, 34 insertions(+) diff --git a/testing/src/main/java/com/team2813/lib2813/testing/FakeMotor.java b/testing/src/main/java/com/team2813/lib2813/testing/FakeMotor.java index 040383a8..93cf31aa 100644 --- a/testing/src/main/java/com/team2813/lib2813/testing/FakeMotor.java +++ b/testing/src/main/java/com/team2813/lib2813/testing/FakeMotor.java @@ -10,6 +10,12 @@ import edu.wpi.first.units.measure.Voltage; import java.util.Objects; +/** + * A fake implementation of {@link Motor}; used for testing. + * + *

This class simulates motor behavior by storing the most recent control mode and demand value. + * It also includes methods that make it easier to verify the current state of the motor. + */ public class FakeMotor implements Motor { private boolean isStopped = true; private double feedForward = 0.0f; @@ -18,6 +24,12 @@ public class FakeMotor implements Motor { public Resistance resistance = Resistance.ofBaseUnits(0.025f, Units.Ohms); private ControlMode controlMode = ControlMode.VOLTAGE; + /** + * Gets the current voltage being applied to the motor. + * + * @return applied voltage. + * @throws IllegalStateException if the motor's control mode is not {@link ControlMode#VOLTAGE}. + */ public Voltage getMotorVoltage() { if (!ControlMode.VOLTAGE.equals(controlMode)) { throw new IllegalStateException("Cannot get voltage when controlMode is " + controlMode); @@ -29,6 +41,7 @@ public Voltage getMotorVoltage() { * Gets the feedforward that was applied. * * @return feedforward, in fractional units between -1 and +1. + * @throws IllegalStateException if the current control mode does not support feedforward. */ public double getFeedForward() { return switch (controlMode) { @@ -39,6 +52,11 @@ public double getFeedForward() { }; } + /** + * Assert that the motor is stopped. + * + * @throws AssertionError if the motor is running. + */ public final void assertIsStopped() { assertThat(isStopped).isTrue(); } @@ -48,6 +66,11 @@ public final void set(ControlMode mode, double demand) { set(mode, demand, 0); } + /** + * {@inheritDoc} + * + * @throws AssertionError if the feedForward is not in the range [-1, 1]. + */ @Override public void set(ControlMode mode, double demand, double feedForward) { Objects.requireNonNull(mode, "mode should not be null"); @@ -65,6 +88,11 @@ public void set(ControlMode mode, double demand, double feedForward) { this.feedForward = feedForward; } + /** + * {@inheritDoc} + * + * @throws IllegalStateException if the current control mode does not support feedforward. + */ @Override public final Current getAppliedCurrent() { try { diff --git a/testing/src/main/java/com/team2813/lib2813/testing/FakePIDMotor.java b/testing/src/main/java/com/team2813/lib2813/testing/FakePIDMotor.java index 407903b9..9c346086 100644 --- a/testing/src/main/java/com/team2813/lib2813/testing/FakePIDMotor.java +++ b/testing/src/main/java/com/team2813/lib2813/testing/FakePIDMotor.java @@ -3,6 +3,12 @@ import com.team2813.lib2813.control.PIDMotor; import edu.wpi.first.units.Units; +/** + * A fake implementation of {@link PIDMotor}; used for testing. + * + *

This class simulates motor behavior by storing the most recent control mode and demand value. + * It also includes methods that make it easier to verify the current state of the motor. + */ public abstract class FakePIDMotor extends FakeMotor implements PIDMotor { @Deprecated