diff --git a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/button/Trigger.java b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/button/Trigger.java index d7b1dd08937..c8d16e627ca 100644 --- a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/button/Trigger.java +++ b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/button/Trigger.java @@ -24,6 +24,34 @@ *

This class is provided by the NewCommands VendorDep */ public class Trigger implements BooleanSupplier { + /** + * Enum specifying the initial state to use for a binding. This impacts whether or not the binding + * will be triggered immediately. + */ + public enum InitialState { + /** + * Indicates the binding should use false as the initial value. This causes a rising edge at the + * start if and only if the condition starts true. + */ + FALSE, + /** + * Indicates the binding should use true as the initial value. This causes a falling edge at the + * start if and only if the condition starts false. + */ + TRUE, + /** + * Indicates the binding should use the trigger's condition as the initial value. This never + * causes an edge at the start. + */ + CONDITION, + /** + * Indicates the binding should use the negated trigger's condition as the initial value. This + * always causes an edge at the start. Rising or falling depends on if the condition starts true + * or false, respectively. + */ + NEG_CONDITION; + } + private final BooleanSupplier m_condition; private final EventLoop m_loop; @@ -50,16 +78,42 @@ public Trigger(BooleanSupplier condition) { } /** - * Starts the command when the condition changes. + * Gets the initial state for a binding based on an initial state policy. + * + * @param initialState Initial state policy. + * @return The initial state to use. + */ + private boolean getInitialState(InitialState initialState) { + return switch (initialState) { + case FALSE -> false; + case TRUE -> true; + case CONDITION -> m_condition.getAsBoolean(); + case NEG_CONDITION -> !m_condition.getAsBoolean(); + }; + } + + /** + * Starts the command when the condition changes. The command is never started immediately. * * @param command the command to start * @return this trigger, so calls can be chained */ public Trigger onChange(Command command) { + return onChange(command, InitialState.CONDITION); + } + + /** + * Starts the command when the condition changes. + * + * @param command the command to start + * @param initialState the initial state to use + * @return this trigger, so calls can be chained + */ + public Trigger onChange(Command command, InitialState initialState) { requireNonNullParam(command, "command", "onChange"); m_loop.bind( new Runnable() { - private boolean m_pressedLast = m_condition.getAsBoolean(); + private boolean m_pressedLast = getInitialState(initialState); @Override public void run() { @@ -76,16 +130,28 @@ public void run() { } /** - * Starts the given command whenever the condition changes from `false` to `true`. + * Starts the given command whenever the condition changes from `false` to `true`. The command is + * never started immediately. * * @param command the command to start * @return this trigger, so calls can be chained */ public Trigger onTrue(Command command) { + return onTrue(command, InitialState.CONDITION); + } + + /** + * Starts the given command whenever the condition changes from `false` to `true`. + * + * @param command the command to start + * @param initialState the initial state to use + * @return this trigger, so calls can be chained + */ + public Trigger onTrue(Command command, InitialState initialState) { requireNonNullParam(command, "command", "onTrue"); m_loop.bind( new Runnable() { - private boolean m_pressedLast = m_condition.getAsBoolean(); + private boolean m_pressedLast = getInitialState(initialState); @Override public void run() { @@ -102,16 +168,28 @@ public void run() { } /** - * Starts the given command whenever the condition changes from `true` to `false`. + * Starts the given command whenever the condition changes from `true` to `false`. The command is + * never started immediately. * * @param command the command to start * @return this trigger, so calls can be chained */ public Trigger onFalse(Command command) { + return onFalse(command, InitialState.CONDITION); + } + + /** + * Starts the given command whenever the condition changes from `true` to `false`. + * + * @param command the command to start + * @param initialState the initial state to use + * @return this trigger, so calls can be chained + */ + public Trigger onFalse(Command command, InitialState initialState) { requireNonNullParam(command, "command", "onFalse"); m_loop.bind( new Runnable() { - private boolean m_pressedLast = m_condition.getAsBoolean(); + private boolean m_pressedLast = getInitialState(initialState); @Override public void run() { @@ -129,7 +207,7 @@ public void run() { /** * Starts the given command when the condition changes to `true` and cancels it when the condition - * changes to `false`. + * changes to `false`. The command is never started immediately. * *

Doesn't re-start the command if it ends while the condition is still `true`. If the command * should restart, see {@link edu.wpi.first.wpilibj2.command.RepeatCommand}. @@ -138,10 +216,25 @@ public void run() { * @return this trigger, so calls can be chained */ public Trigger whileTrue(Command command) { + return whileTrue(command, InitialState.CONDITION); + } + + /** + * Starts the given command when the condition changes to `true` and cancels it when the condition + * changes to `false`. + * + *

Doesn't re-start the command if it ends while the condition is still `true`. If the command + * should restart, see {@link edu.wpi.first.wpilibj2.command.RepeatCommand}. + * + * @param command the command to start + * @param initialState the initial state to use + * @return this trigger, so calls can be chained + */ + public Trigger whileTrue(Command command, InitialState initialState) { requireNonNullParam(command, "command", "whileTrue"); m_loop.bind( new Runnable() { - private boolean m_pressedLast = m_condition.getAsBoolean(); + private boolean m_pressedLast = getInitialState(initialState); @Override public void run() { @@ -161,7 +254,7 @@ public void run() { /** * Starts the given command when the condition changes to `false` and cancels it when the - * condition changes to `true`. + * condition changes to `true`. The command is never started immediately. * *

Doesn't re-start the command if it ends while the condition is still `false`. If the command * should restart, see {@link edu.wpi.first.wpilibj2.command.RepeatCommand}. @@ -170,10 +263,25 @@ public void run() { * @return this trigger, so calls can be chained */ public Trigger whileFalse(Command command) { + return whileFalse(command, InitialState.CONDITION); + } + + /** + * Starts the given command when the condition changes to `false` and cancels it when the + * condition changes to `true`. + * + *

Doesn't re-start the command if it ends while the condition is still `false`. If the command + * should restart, see {@link edu.wpi.first.wpilibj2.command.RepeatCommand}. + * + * @param command the command to start + * @param initialState the initial state to use + * @return this trigger, so calls can be chained + */ + public Trigger whileFalse(Command command, InitialState initialState) { requireNonNullParam(command, "command", "whileFalse"); m_loop.bind( new Runnable() { - private boolean m_pressedLast = m_condition.getAsBoolean(); + private boolean m_pressedLast = getInitialState(initialState); @Override public void run() { @@ -192,16 +300,28 @@ public void run() { } /** - * Toggles a command when the condition changes from `false` to `true`. + * Toggles a command when the condition changes from `false` to `true`. The command is never + * toggled immediately. * * @param command the command to toggle * @return this trigger, so calls can be chained */ public Trigger toggleOnTrue(Command command) { + return toggleOnTrue(command, InitialState.CONDITION); + } + + /** + * Toggles a command when the condition changes from `false` to `true`. + * + * @param command the command to toggle + * @param initialState the initial state to use + * @return this trigger, so calls can be chained + */ + public Trigger toggleOnTrue(Command command, InitialState initialState) { requireNonNullParam(command, "command", "toggleOnTrue"); m_loop.bind( new Runnable() { - private boolean m_pressedLast = m_condition.getAsBoolean(); + private boolean m_pressedLast = getInitialState(initialState); @Override public void run() { @@ -222,16 +342,28 @@ public void run() { } /** - * Toggles a command when the condition changes from `true` to `false`. + * Toggles a command when the condition changes from `true` to `false`. The command is never + * toggled immediately. * * @param command the command to toggle * @return this trigger, so calls can be chained */ public Trigger toggleOnFalse(Command command) { + return toggleOnFalse(command, InitialState.CONDITION); + } + + /** + * Toggles a command when the condition changes from `true` to `false`. + * + * @param command the command to toggle + * @param initialState the initial state to use + * @return this trigger, so calls can be chained + */ + public Trigger toggleOnFalse(Command command, InitialState initialState) { requireNonNullParam(command, "command", "toggleOnFalse"); m_loop.bind( new Runnable() { - private boolean m_pressedLast = m_condition.getAsBoolean(); + private boolean m_pressedLast = getInitialState(initialState); @Override public void run() { diff --git a/wpilibNewCommands/src/main/native/cpp/frc2/command/button/Trigger.cpp b/wpilibNewCommands/src/main/native/cpp/frc2/command/button/Trigger.cpp index a3b02d18f3a..420ff488d19 100644 --- a/wpilibNewCommands/src/main/native/cpp/frc2/command/button/Trigger.cpp +++ b/wpilibNewCommands/src/main/native/cpp/frc2/command/button/Trigger.cpp @@ -15,22 +15,23 @@ using namespace frc2; Trigger::Trigger(const Trigger& other) = default; -Trigger Trigger::OnChange(Command* command) { - m_loop->Bind( - [condition = m_condition, previous = m_condition(), command]() mutable { - bool current = condition(); +Trigger Trigger::OnChange(Command* command, InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command]() mutable { + bool current = condition(); - if (previous != current) { - command->Schedule(); - } + if (previous != current) { + command->Schedule(); + } - previous = current; - }); + previous = current; + }); return *this; } -Trigger Trigger::OnChange(CommandPtr&& command) { - m_loop->Bind([condition = m_condition, previous = m_condition(), +Trigger Trigger::OnChange(CommandPtr&& command, InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command = std::move(command)]() mutable { bool current = condition(); @@ -43,22 +44,23 @@ Trigger Trigger::OnChange(CommandPtr&& command) { return *this; } -Trigger Trigger::OnTrue(Command* command) { - m_loop->Bind( - [condition = m_condition, previous = m_condition(), command]() mutable { - bool current = condition(); +Trigger Trigger::OnTrue(Command* command, InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command]() mutable { + bool current = condition(); - if (!previous && current) { - command->Schedule(); - } + if (!previous && current) { + command->Schedule(); + } - previous = current; - }); + previous = current; + }); return *this; } -Trigger Trigger::OnTrue(CommandPtr&& command) { - m_loop->Bind([condition = m_condition, previous = m_condition(), +Trigger Trigger::OnTrue(CommandPtr&& command, InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command = std::move(command)]() mutable { bool current = condition(); @@ -71,22 +73,23 @@ Trigger Trigger::OnTrue(CommandPtr&& command) { return *this; } -Trigger Trigger::OnFalse(Command* command) { - m_loop->Bind( - [condition = m_condition, previous = m_condition(), command]() mutable { - bool current = condition(); +Trigger Trigger::OnFalse(Command* command, InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command]() mutable { + bool current = condition(); - if (previous && !current) { - command->Schedule(); - } + if (previous && !current) { + command->Schedule(); + } - previous = current; - }); + previous = current; + }); return *this; } -Trigger Trigger::OnFalse(CommandPtr&& command) { - m_loop->Bind([condition = m_condition, previous = m_condition(), +Trigger Trigger::OnFalse(CommandPtr&& command, InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command = std::move(command)]() mutable { bool current = condition(); @@ -99,24 +102,25 @@ Trigger Trigger::OnFalse(CommandPtr&& command) { return *this; } -Trigger Trigger::WhileTrue(Command* command) { - m_loop->Bind( - [condition = m_condition, previous = m_condition(), command]() mutable { - bool current = condition(); +Trigger Trigger::WhileTrue(Command* command, InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command]() mutable { + bool current = condition(); - if (!previous && current) { - command->Schedule(); - } else if (previous && !current) { - command->Cancel(); - } + if (!previous && current) { + command->Schedule(); + } else if (previous && !current) { + command->Cancel(); + } - previous = current; - }); + previous = current; + }); return *this; } -Trigger Trigger::WhileTrue(CommandPtr&& command) { - m_loop->Bind([condition = m_condition, previous = m_condition(), +Trigger Trigger::WhileTrue(CommandPtr&& command, InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command = std::move(command)]() mutable { bool current = condition(); @@ -131,24 +135,25 @@ Trigger Trigger::WhileTrue(CommandPtr&& command) { return *this; } -Trigger Trigger::WhileFalse(Command* command) { - m_loop->Bind( - [condition = m_condition, previous = m_condition(), command]() mutable { - bool current = condition(); +Trigger Trigger::WhileFalse(Command* command, InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command]() mutable { + bool current = condition(); - if (previous && !current) { - command->Schedule(); - } else if (!previous && current) { - command->Cancel(); - } + if (previous && !current) { + command->Schedule(); + } else if (!previous && current) { + command->Cancel(); + } - previous = current; - }); + previous = current; + }); return *this; } -Trigger Trigger::WhileFalse(CommandPtr&& command) { - m_loop->Bind([condition = m_condition, previous = m_condition(), +Trigger Trigger::WhileFalse(CommandPtr&& command, InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command = std::move(command)]() mutable { bool current = condition(); @@ -163,8 +168,9 @@ Trigger Trigger::WhileFalse(CommandPtr&& command) { return *this; } -Trigger Trigger::ToggleOnTrue(Command* command) { - m_loop->Bind([condition = m_condition, previous = m_condition(), +Trigger Trigger::ToggleOnTrue(Command* command, InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command = command]() mutable { bool current = condition(); @@ -181,8 +187,9 @@ Trigger Trigger::ToggleOnTrue(Command* command) { return *this; } -Trigger Trigger::ToggleOnTrue(CommandPtr&& command) { - m_loop->Bind([condition = m_condition, previous = m_condition(), +Trigger Trigger::ToggleOnTrue(CommandPtr&& command, InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command = std::move(command)]() mutable { bool current = condition(); @@ -199,8 +206,9 @@ Trigger Trigger::ToggleOnTrue(CommandPtr&& command) { return *this; } -Trigger Trigger::ToggleOnFalse(Command* command) { - m_loop->Bind([condition = m_condition, previous = m_condition(), +Trigger Trigger::ToggleOnFalse(Command* command, InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command = command]() mutable { bool current = condition(); @@ -217,8 +225,10 @@ Trigger Trigger::ToggleOnFalse(Command* command) { return *this; } -Trigger Trigger::ToggleOnFalse(CommandPtr&& command) { - m_loop->Bind([condition = m_condition, previous = m_condition(), +Trigger Trigger::ToggleOnFalse(CommandPtr&& command, + InitialState initialState) { + m_loop->Bind([condition = m_condition, + previous = GetInitialState(initialState), command = std::move(command)]() mutable { bool current = condition(); diff --git a/wpilibNewCommands/src/main/native/include/frc2/command/button/Trigger.h b/wpilibNewCommands/src/main/native/include/frc2/command/button/Trigger.h index a2382631334..067317b4d7c 100644 --- a/wpilibNewCommands/src/main/native/include/frc2/command/button/Trigger.h +++ b/wpilibNewCommands/src/main/native/include/frc2/command/button/Trigger.h @@ -30,6 +30,34 @@ class Command; */ class Trigger { public: + /** + * Enum specifying the initial state to use for a binding. This impacts + * whether or not the binding will be triggered immediately. + */ + enum struct InitialState { + /** + * Indicates the binding should use false as the initial value. This causes + * a rising edge at the start if and only if the condition starts true. + */ + FALSE, + /** + * Indicates the binding should use true as the initial value. This causes a + * falling edge at the start if and only if the condition starts false. + */ + TRUE, + /** + * Indicates the binding should use the trigger's condition as the initial + * value. This never causes an edge at the start. + */ + CONDITION, + /** + * Indicates the binding should use the negated trigger's condition as the + * initial value. This always causes an edge at the start. Rising or falling + * depends on if the condition starts true or false, respectively. + */ + NEG_CONDITION + }; + /** * Creates a new trigger based on the given condition. * @@ -61,18 +89,22 @@ class Trigger { * Starts the command when the condition changes. * * @param command the command to start + * @param initialState the initial state to use * @return this trigger, so calls can be chained */ - Trigger OnChange(Command* command); + Trigger OnChange(Command* command, + InitialState initialState = InitialState::CONDITION); /** * Starts the command when the condition changes. Moves command ownership to * the button scheduler. * * @param command the command to start + * @param initialState the initial state to use * @return this trigger, so calls can be chained */ - Trigger OnChange(CommandPtr&& command); + Trigger OnChange(CommandPtr&& command, + InitialState initialState = InitialState::CONDITION); /** * Starts the given command whenever the condition changes from `false` to @@ -82,18 +114,22 @@ class Trigger { * lifespan of the command. * * @param command the command to start + * @param initialState the initial state to use * @return this trigger, so calls can be chained */ - Trigger OnTrue(Command* command); + Trigger OnTrue(Command* command, + InitialState initialState = InitialState::CONDITION); /** * Starts the given command whenever the condition changes from `false` to * `true`. Moves command ownership to the button scheduler. * * @param command The command to bind. + * @param initialState the initial state to use * @return The trigger, for chained calls. */ - Trigger OnTrue(CommandPtr&& command); + Trigger OnTrue(CommandPtr&& command, + InitialState initialState = InitialState::CONDITION); /** * Starts the given command whenever the condition changes from `true` to @@ -103,18 +139,22 @@ class Trigger { * lifespan of the command. * * @param command the command to start + * @param initialState the initial state to use * @return this trigger, so calls can be chained */ - Trigger OnFalse(Command* command); + Trigger OnFalse(Command* command, + InitialState initialState = InitialState::CONDITION); /** * Starts the given command whenever the condition changes from `true` to * `false`. * * @param command The command to bind. + * @param initialState the initial state to use * @return The trigger, for chained calls. */ - Trigger OnFalse(CommandPtr&& command); + Trigger OnFalse(CommandPtr&& command, + InitialState initialState = InitialState::CONDITION); /** * Starts the given command when the condition changes to `true` and cancels @@ -127,9 +167,11 @@ class Trigger { * lifespan of the command. * * @param command the command to start + * @param initialState the initial state to use * @return this trigger, so calls can be chained */ - Trigger WhileTrue(Command* command); + Trigger WhileTrue(Command* command, + InitialState initialState = InitialState::CONDITION); /** * Starts the given command when the condition changes to `true` and cancels @@ -140,9 +182,11 @@ class Trigger { * `true`. If the command should restart, see RepeatCommand. * * @param command The command to bind. + * @param initialState the initial state to use * @return The trigger, for chained calls. */ - Trigger WhileTrue(CommandPtr&& command); + Trigger WhileTrue(CommandPtr&& command, + InitialState initialState = InitialState::CONDITION); /** * Starts the given command when the condition changes to `false` and cancels @@ -155,9 +199,11 @@ class Trigger { * lifespan of the command. * * @param command the command to start + * @param initialState the initial state to use * @return this trigger, so calls can be chained */ - Trigger WhileFalse(Command* command); + Trigger WhileFalse(Command* command, + InitialState initialState = InitialState::CONDITION); /** * Starts the given command when the condition changes to `false` and cancels @@ -168,9 +214,11 @@ class Trigger { * `false`. If the command should restart, see RepeatCommand. * * @param command The command to bind. + * @param initialState the initial state to use * @return The trigger, for chained calls. */ - Trigger WhileFalse(CommandPtr&& command); + Trigger WhileFalse(CommandPtr&& command, + InitialState initialState = InitialState::CONDITION); /** * Toggles a command when the condition changes from `false` to `true`. @@ -179,9 +227,11 @@ class Trigger { * lifespan of the command. * * @param command the command to toggle + * @param initialState the initial state to use * @return this trigger, so calls can be chained */ - Trigger ToggleOnTrue(Command* command); + Trigger ToggleOnTrue(Command* command, + InitialState initialState = InitialState::CONDITION); /** * Toggles a command when the condition changes from `false` to `true`. @@ -190,9 +240,11 @@ class Trigger { * lifespan of the command. * * @param command the command to toggle + * @param initialState the initial state to use * @return this trigger, so calls can be chained */ - Trigger ToggleOnTrue(CommandPtr&& command); + Trigger ToggleOnTrue(CommandPtr&& command, + InitialState initialState = InitialState::CONDITION); /** * Toggles a command when the condition changes from `true` to the low @@ -202,9 +254,11 @@ class Trigger { * lifespan of the command. * * @param command the command to toggle + * @param initialState the initial state to use * @return this trigger, so calls can be chained */ - Trigger ToggleOnFalse(Command* command); + Trigger ToggleOnFalse(Command* command, + InitialState initialState = InitialState::CONDITION); /** * Toggles a command when the condition changes from `true` to `false`. @@ -213,9 +267,11 @@ class Trigger { * lifespan of the command. * * @param command the command to toggle + * @param initialState the initial state to use * @return this trigger, so calls can be chained */ - Trigger ToggleOnFalse(CommandPtr&& command); + Trigger ToggleOnFalse(CommandPtr&& command, + InitialState initialState = InitialState::CONDITION); /** * Composes two triggers with logical AND. @@ -291,6 +347,20 @@ class Trigger { bool Get() const; private: + bool GetInitialState(InitialState initialState) const { + switch (initialState) { + case InitialState::FALSE: + return false; + case InitialState::TRUE: + return true; + case InitialState::CONDITION: + return m_condition(); + case InitialState::NEG_CONDITION: + return !m_condition(); + } + return false; + } + frc::EventLoop* m_loop; std::function m_condition; }; diff --git a/wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/button/TriggerTest.java b/wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/button/TriggerTest.java index 8cd5fdea2f1..331f7f36095 100644 --- a/wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/button/TriggerTest.java +++ b/wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/button/TriggerTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -14,14 +15,21 @@ import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.CommandScheduler; import edu.wpi.first.wpilibj2.command.CommandTestBase; +import edu.wpi.first.wpilibj2.command.Commands; import edu.wpi.first.wpilibj2.command.FunctionalCommand; import edu.wpi.first.wpilibj2.command.RunCommand; import edu.wpi.first.wpilibj2.command.StartEndCommand; import edu.wpi.first.wpilibj2.command.WaitUntilCommand; +import edu.wpi.first.wpilibj2.command.button.Trigger.InitialState; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BooleanSupplier; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class TriggerTest extends CommandTestBase { @Test @@ -43,6 +51,24 @@ void onTrueTest() { assertFalse(command1.isScheduled()); } + @ParameterizedTest + @MethodSource("initialStates") + void onTrueInitialStateTest( + InitialState initialState, boolean pressed, Map results) { + final CommandScheduler scheduler = CommandScheduler.getInstance(); + final Command command1 = Commands.idle(); + final InternalButton button = new InternalButton(); + final boolean shouldBeScheduled = results.get("onTrue"); + + button.setPressed(pressed); + button.onTrue(command1, initialState); + + assertFalse(command1.isScheduled()); + + scheduler.run(); + assertEquals(shouldBeScheduled, command1.isScheduled()); + } + @Test void onFalseTest() { CommandScheduler scheduler = CommandScheduler.getInstance(); @@ -62,6 +88,24 @@ void onFalseTest() { assertFalse(command1.isScheduled()); } + @ParameterizedTest + @MethodSource("initialStates") + void onFalseInitialStateTest( + InitialState initialState, boolean pressed, Map results) { + final CommandScheduler scheduler = CommandScheduler.getInstance(); + final Command command1 = Commands.idle(); + final InternalButton button = new InternalButton(); + final boolean shouldBeScheduled = results.get("onFalse"); + + button.setPressed(pressed); + button.onFalse(command1, initialState); + + assertFalse(command1.isScheduled()); + + scheduler.run(); + assertEquals(shouldBeScheduled, command1.isScheduled()); + } + @Test void onChangeTest() { CommandScheduler scheduler = CommandScheduler.getInstance(); @@ -88,6 +132,24 @@ void onChangeTest() { assertFalse(command1.isScheduled()); } + @ParameterizedTest + @MethodSource("initialStates") + void onChangeInitialStateTest( + InitialState initialState, boolean pressed, Map results) { + final CommandScheduler scheduler = CommandScheduler.getInstance(); + final Command command1 = Commands.idle(); + final InternalButton button = new InternalButton(); + final boolean shouldBeScheduled = results.get("onTrue") || results.get("onFalse"); + + button.setPressed(pressed); + button.onChange(command1, initialState); + + assertFalse(command1.isScheduled()); + + scheduler.run(); + assertEquals(shouldBeScheduled, command1.isScheduled()); + } + @Test void whileTrueRepeatedlyTest() { CommandScheduler scheduler = CommandScheduler.getInstance(); @@ -195,6 +257,24 @@ void toggleOnTrueTest() { assertEquals(1, endCounter.get()); } + @ParameterizedTest + @MethodSource("initialStates") + void toggleOnTrueInitialStateTest( + InitialState initialState, boolean pressed, Map results) { + final CommandScheduler scheduler = CommandScheduler.getInstance(); + final Command command1 = Commands.idle(); + final InternalButton button = new InternalButton(); + final boolean shouldBeScheduled = results.get("onTrue"); + + button.setPressed(pressed); + button.onTrue(command1, initialState); + + assertFalse(command1.isScheduled()); + + scheduler.run(); + assertEquals(shouldBeScheduled, command1.isScheduled()); + } + @Test void cancelWhenActiveTest() { CommandScheduler scheduler = CommandScheduler.getInstance(); @@ -275,4 +355,16 @@ void booleanSupplierTest() { button.setPressed(true); assertTrue(button.getAsBoolean()); } + + static Stream initialStates() { + return Stream.of( + arguments(InitialState.FALSE, true, Map.of("onTrue", true, "onFalse", false)), + arguments(InitialState.FALSE, false, Map.of("onTrue", false, "onFalse", false)), + arguments(InitialState.TRUE, true, Map.of("onTrue", false, "onFalse", false)), + arguments(InitialState.TRUE, false, Map.of("onTrue", false, "onFalse", true)), + arguments(InitialState.CONDITION, true, Map.of("onTrue", false, "onFalse", false)), + arguments(InitialState.CONDITION, false, Map.of("onTrue", false, "onFalse", false)), + arguments(InitialState.NEG_CONDITION, true, Map.of("onTrue", true, "onFalse", false)), + arguments(InitialState.NEG_CONDITION, false, Map.of("onTrue", false, "onFalse", true))); + } } diff --git a/wpilibNewCommands/src/test/native/cpp/frc2/command/button/TriggerTest.cpp b/wpilibNewCommands/src/test/native/cpp/frc2/command/button/TriggerTest.cpp index 9b0899edb78..c4008fc7630 100644 --- a/wpilibNewCommands/src/test/native/cpp/frc2/command/button/TriggerTest.cpp +++ b/wpilibNewCommands/src/test/native/cpp/frc2/command/button/TriggerTest.cpp @@ -2,6 +2,7 @@ // Open Source Software; you can modify and/or share it under the terms of // the WPILib BSD license file in the root directory of this project. +#include #include #include @@ -18,6 +19,14 @@ using namespace frc2; class TriggerTest : public CommandTestBase {}; +// Initial state, pressed, rising, falling +using InitialStateTestData = + std::tuple; + +class TriggerInitialStateTest + : public TriggerTest, + public testing::WithParamInterface {}; + TEST_F(TriggerTest, OnTrue) { auto& scheduler = CommandScheduler::GetInstance(); bool finished = false; @@ -36,6 +45,22 @@ TEST_F(TriggerTest, OnTrue) { EXPECT_FALSE(scheduler.IsScheduled(&command)); } +TEST_P(TriggerInitialStateTest, OnTrue) { + auto [initialState, pressed, rising, falling] = GetParam(); + auto& scheduler = CommandScheduler::GetInstance(); + RunCommand command([] {}); + bool shouldBeScheduled = rising; + + Trigger([pressed = pressed] { + return pressed; + }).OnTrue(&command, initialState); + + EXPECT_FALSE(scheduler.IsScheduled(&command)); + + scheduler.Run(); + EXPECT_EQ(shouldBeScheduled, scheduler.IsScheduled(&command)); +} + TEST_F(TriggerTest, OnFalse) { auto& scheduler = CommandScheduler::GetInstance(); bool finished = false; @@ -54,6 +79,22 @@ TEST_F(TriggerTest, OnFalse) { EXPECT_FALSE(scheduler.IsScheduled(&command)); } +TEST_P(TriggerInitialStateTest, OnFalse) { + auto [initialState, pressed, rising, falling] = GetParam(); + auto& scheduler = CommandScheduler::GetInstance(); + RunCommand command([] {}); + bool shouldBeScheduled = falling; + + Trigger([pressed = pressed] { + return pressed; + }).OnFalse(&command, initialState); + + EXPECT_FALSE(scheduler.IsScheduled(&command)); + + scheduler.Run(); + EXPECT_EQ(shouldBeScheduled, scheduler.IsScheduled(&command)); +} + TEST_F(TriggerTest, OnChange) { auto& scheduler = CommandScheduler::GetInstance(); bool finished = false; @@ -78,6 +119,22 @@ TEST_F(TriggerTest, OnChange) { EXPECT_FALSE(command.IsScheduled()); } +TEST_P(TriggerInitialStateTest, OnChange) { + auto [initialState, pressed, rising, falling] = GetParam(); + auto& scheduler = CommandScheduler::GetInstance(); + RunCommand command([] {}); + bool shouldBeScheduled = rising || falling; + + Trigger([pressed = pressed] { + return pressed; + }).OnChange(&command, initialState); + + EXPECT_FALSE(scheduler.IsScheduled(&command)); + + scheduler.Run(); + EXPECT_EQ(shouldBeScheduled, scheduler.IsScheduled(&command)); +} + TEST_F(TriggerTest, WhileTrueRepeatedly) { auto& scheduler = CommandScheduler::GetInstance(); int inits = 0; @@ -175,6 +232,22 @@ TEST_F(TriggerTest, ToggleOnTrue) { EXPECT_EQ(1, endCounter); } +TEST_P(TriggerInitialStateTest, ToggleOnTrue) { + auto [initialState, pressed, rising, falling] = GetParam(); + auto& scheduler = CommandScheduler::GetInstance(); + RunCommand command([] {}); + bool shouldBeScheduled = rising; + + Trigger([pressed = pressed] { + return pressed; + }).ToggleOnTrue(&command, initialState); + + EXPECT_FALSE(scheduler.IsScheduled(&command)); + + scheduler.Run(); + EXPECT_EQ(shouldBeScheduled, scheduler.IsScheduled(&command)); +} + TEST_F(TriggerTest, And) { auto& scheduler = CommandScheduler::GetInstance(); bool finished = false; @@ -247,3 +320,16 @@ TEST_F(TriggerTest, Debounce) { scheduler.Run(); EXPECT_TRUE(scheduler.IsScheduled(&command)); } + +INSTANTIATE_TEST_SUITE_P( + InitialStateTests, TriggerInitialStateTest, + testing::Values( + // Initial state, pressed, rising, falling + std::tuple{Trigger::InitialState::FALSE, false, false, false}, + std::tuple{Trigger::InitialState::FALSE, true, true, false}, + std::tuple{Trigger::InitialState::TRUE, false, false, true}, + std::tuple{Trigger::InitialState::TRUE, true, false, false}, + std::tuple{Trigger::InitialState::CONDITION, false, false, false}, + std::tuple{Trigger::InitialState::CONDITION, true, false, false}, + std::tuple{Trigger::InitialState::NEG_CONDITION, false, false, true}, + std::tuple{Trigger::InitialState::NEG_CONDITION, true, true, false}));