diff --git a/wpilibc/src/main/native/cpp/event/BooleanEvent.cpp b/wpilibc/src/main/native/cpp/event/BooleanEvent.cpp index 5b8ce63d68..fd9c831fe9 100644 --- a/wpilibc/src/main/native/cpp/event/BooleanEvent.cpp +++ b/wpilibc/src/main/native/cpp/event/BooleanEvent.cpp @@ -7,56 +7,61 @@ using namespace frc; BooleanEvent::BooleanEvent(EventLoop* loop, std::function condition) - : m_loop(loop), m_condition(std::move(condition)) {} + : m_loop(loop), m_condition(std::move(condition)) { + m_state = std::make_shared(m_condition()); + m_loop->Bind( + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) + [condition = m_condition, state = m_state] { *state = condition(); }); +} BooleanEvent::operator std::function() { - return m_condition; + return [state = m_state] { return *state; }; } bool BooleanEvent::GetAsBoolean() const { - return m_condition(); + return *m_state; } void BooleanEvent::IfHigh(std::function action) { - m_loop->Bind([condition = m_condition, action = std::move(action)] { - if (condition()) { + m_loop->Bind([state = m_state, action = std::move(action)] { + if (*state) { action(); } }); } BooleanEvent BooleanEvent::operator!() { - return BooleanEvent(this->m_loop, [lhs = m_condition] { return !lhs(); }); + return BooleanEvent(this->m_loop, [state = m_state] { return !*state; }); } BooleanEvent BooleanEvent::operator&&(std::function rhs) { return BooleanEvent(this->m_loop, - [lhs = m_condition, rhs] { return lhs() && rhs(); }); + [state = m_state, rhs] { return *state && rhs(); }); } BooleanEvent BooleanEvent::operator||(std::function rhs) { return BooleanEvent(this->m_loop, - [lhs = m_condition, rhs] { return lhs() || rhs(); }); + [state = m_state, rhs] { return *state || rhs(); }); } BooleanEvent BooleanEvent::Rising() { - return BooleanEvent( - this->m_loop, [lhs = m_condition, m_previous = m_condition()]() mutable { - bool present = lhs(); - bool past = m_previous; - m_previous = present; - return !past && present; - }); + return BooleanEvent(this->m_loop, + [state = m_state, m_previous = *m_state]() mutable { + bool present = *state; + bool past = m_previous; + m_previous = present; + return !past && present; + }); } BooleanEvent BooleanEvent::Falling() { - return BooleanEvent( - this->m_loop, [lhs = m_condition, m_previous = m_condition()]() mutable { - bool present = lhs(); - bool past = m_previous; - m_previous = present; - return past && !present; - }); + return BooleanEvent(this->m_loop, + [state = m_state, m_previous = *m_state]() mutable { + bool present = *state; + bool past = m_previous; + m_previous = present; + return past && !present; + }); } BooleanEvent BooleanEvent::Debounce(units::second_t debounceTime, @@ -64,5 +69,5 @@ BooleanEvent BooleanEvent::Debounce(units::second_t debounceTime, return BooleanEvent( this->m_loop, [debouncer = frc::Debouncer(debounceTime, type), - lhs = m_condition]() mutable { return debouncer.Calculate(lhs()); }); + state = m_state]() mutable { return debouncer.Calculate(*state); }); } diff --git a/wpilibc/src/main/native/include/frc/event/BooleanEvent.h b/wpilibc/src/main/native/include/frc/event/BooleanEvent.h index 745a53c095..7c19c90e8b 100644 --- a/wpilibc/src/main/native/include/frc/event/BooleanEvent.h +++ b/wpilibc/src/main/native/include/frc/event/BooleanEvent.h @@ -40,9 +40,10 @@ class BooleanEvent { BooleanEvent(EventLoop* loop, std::function condition); /** - * Check whether this event is active or not. + * Check whether this event is active or not as of the last loop poll. * - * @return true if active. + * @return true if active, false if not active. If the event was never polled, + * it returns the state at event construction. */ bool GetAsBoolean() const; @@ -69,7 +70,7 @@ class BooleanEvent { [](EventLoop* loop, std::function condition) { return T(loop, condition); }) { - return ctor(m_loop, m_condition); + return ctor(m_loop, [state = m_state] { return *state; }); } /** @@ -84,7 +85,8 @@ class BooleanEvent { * Composes this event with another event, returning a new event that is * active when both events are active. * - *

The new event will use this event's polling loop. + *

The events must use the same event loop. If the events use different + * event loops, the composed signal won't update until both loops are polled. * * @param rhs the event to compose with * @return the event that is active when both events are active @@ -95,7 +97,8 @@ class BooleanEvent { * Composes this event with another event, returning a new event that is * active when either event is active. * - *

The new event will use this event's polling loop. + *

The events must use the same event loop. If the events use different + * event loops, the composed signal won't update until both loops are polled. * * @param rhs the event to compose with * @return the event that is active when either event is active @@ -131,5 +134,6 @@ class BooleanEvent { private: EventLoop* m_loop; std::function m_condition; + std::shared_ptr m_state; // A programmer's worst nightmare. }; } // namespace frc diff --git a/wpilibc/src/test/native/cpp/event/BooleanEventTest.cpp b/wpilibc/src/test/native/cpp/event/BooleanEventTest.cpp new file mode 100644 index 0000000000..28209b4e7f --- /dev/null +++ b/wpilibc/src/test/native/cpp/event/BooleanEventTest.cpp @@ -0,0 +1,487 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include + +#include + +#include "frc/event/BooleanEvent.h" +#include "frc/event/EventLoop.h" + +using namespace frc; + +TEST(BooleanEventTest, BinaryCompositions) { + EventLoop loop; + int andCounter = 0; + int orCounter = 0; + + EXPECT_EQ(0, andCounter); + EXPECT_EQ(0, orCounter); + + (BooleanEvent(&loop, [] { return true; }) && BooleanEvent(&loop, [] { + return false; + })).IfHigh([&] { ++andCounter; }); + (BooleanEvent(&loop, [] { return true; }) || BooleanEvent(&loop, [] { + return false; + })).IfHigh([&] { ++orCounter; }); + + loop.Poll(); + + EXPECT_EQ(0, andCounter); + EXPECT_EQ(1, orCounter); +} + +/** + * Tests that composed edge events only execute on edges (two + * rising edge events composed with and() should only execute when both signals + * are on the rising edge) + */ +TEST(BooleanEventTest, BinaryCompositionsWithEdgeDecorators) { + EventLoop loop; + bool boolean1 = false; + bool boolean2 = false; + bool boolean3 = false; + bool boolean4 = false; + int counter = 0; + + auto event1 = BooleanEvent(&loop, [&] { return boolean1; }).Rising(); + auto event2 = BooleanEvent(&loop, [&] { return boolean2; }).Rising(); + auto event3 = BooleanEvent(&loop, [&] { return boolean3; }).Rising(); + auto event4 = BooleanEvent(&loop, [&] { return boolean4; }).Rising(); + (event1 && event2).IfHigh([&] { ++counter; }); + (event3 || event4).IfHigh([&] { ++counter; }); + EXPECT_EQ(0, counter); + + boolean1 = true; + boolean2 = true; + boolean3 = true; + boolean4 = true; + loop.Poll(); // Both actions execute + + EXPECT_EQ(2, counter); + + loop.Poll(); // Nothing should happen since nothing is on rising edge + + EXPECT_EQ(2, counter); + + boolean1 = false; + boolean2 = false; + boolean3 = false; + boolean4 = false; + loop.Poll(); // Nothing should happen + + EXPECT_EQ(2, counter); + + boolean1 = true; + loop.Poll(); // Nothing should happen since only Bool 1 is on rising edge + + EXPECT_EQ(2, counter); + + boolean2 = true; + loop.Poll(); // Bool 2 is on rising edge, but Bool 1 isn't, nothing should + // happen + + EXPECT_EQ(2, counter); + + boolean1 = false; + boolean2 = false; + loop.Poll(); // Nothing should happen + + EXPECT_EQ(2, counter); + + boolean1 = true; + boolean2 = true; + loop.Poll(); // Bool 1 and 2 are on rising edge, increments counter once + + EXPECT_EQ(3, counter); + + boolean3 = true; + loop.Poll(); // Bool 3 is on rising edge, increments counter once + + EXPECT_EQ(4, counter); + + loop.Poll(); // Nothing should happen, Bool 3 isn't on rising edge + + EXPECT_EQ(4, counter); + + boolean4 = true; + loop.Poll(); // Bool 4 is on rising edge, increments counter once + + EXPECT_EQ(5, counter); + + loop.Poll(); // Nothing should happen, Bool 4 isn't on rising edge + + EXPECT_EQ(5, counter); +} + +TEST(BooleanEventTest, BinaryCompositionLoopSemantics) { + EventLoop loop1; + EventLoop loop2; + bool boolean1 = true; + bool boolean2 = true; + int counter1 = 0; + int counter2 = 0; + + (BooleanEvent(&loop1, [&] { return boolean1; }) && BooleanEvent(&loop2, [&] { + return boolean2; + })).IfHigh([&] { ++counter1; }); + (BooleanEvent(&loop2, [&] { return boolean2; }) && BooleanEvent(&loop1, [&] { + return boolean1; + })).IfHigh([&] { ++counter2; }); + + EXPECT_EQ(0, counter1); + EXPECT_EQ(0, counter2); + + loop1 + .Poll(); // 1st event executes, Bool 1 and 2 are true, increments counter + + EXPECT_EQ(1, counter1); + EXPECT_EQ(0, counter2); + + loop2 + .Poll(); // 2nd event executes, Bool 1 and 2 are true, increments counter + + EXPECT_EQ(1, counter1); + EXPECT_EQ(1, counter2); + + boolean2 = false; + loop1.Poll(); // 1st event executes, Bool 2 is still true because loop 2 + // hasn't updated it, increments counter + + EXPECT_EQ(2, counter1); + EXPECT_EQ(1, counter2); + + loop2.Poll(); // 2nd event executes, Bool 2 is now false because this loop + // updated it, does nothing + + EXPECT_EQ(2, counter1); + EXPECT_EQ(1, counter2); + + loop1.Poll(); // All bools are updated at this point, nothing should happen + + EXPECT_EQ(2, counter1); + EXPECT_EQ(1, counter2); + + boolean2 = true; + loop2.Poll(); // 2nd event executes, Bool 2 is true because this loop updated + // it, increments counter + + EXPECT_EQ(2, counter1); + EXPECT_EQ(2, counter2); + + loop1.Poll(); // 1st event executes, Bool 2 is true because loop 2 updated + // it, increments counter + + EXPECT_EQ(3, counter1); + EXPECT_EQ(2, counter2); + + boolean1 = false; + loop2.Poll(); // 2nd event executes, Bool 1 is still true because loop 1 + // hasn't updated it, increments counter + + EXPECT_EQ(3, counter1); + EXPECT_EQ(3, counter2); + + loop1.Poll(); // 1st event executes, Bool 1 is false because this loop + // updated it, does nothing + + EXPECT_EQ(3, counter1); + EXPECT_EQ(3, counter2); + + loop2.Poll(); // All bools are updated at this point, nothing should happen + + EXPECT_EQ(3, counter1); + EXPECT_EQ(3, counter2); +} + +/** Tests the order of actions bound to an event loop. */ +TEST(BooleanEventTest, PollOrdering) { + EventLoop loop; + bool boolean1 = true; + bool boolean2 = true; + bool enableAssert = false; + int counter = 0; + + (BooleanEvent( // This event binds an action to the event loop first + &loop, + [&] { + if (enableAssert) { + ++counter; + EXPECT_EQ(1, counter % 3); + } + return boolean1; + }) && // The composed event binds an action to the event loop third + // This event binds an action to the event loop second + BooleanEvent(&loop, [&] { + if (enableAssert) { + ++counter; + EXPECT_EQ(2, counter % 3); + } + return boolean2; + // This binds an action to the event loop fourth + })).IfHigh([&] { + if (enableAssert) { + ++counter; + EXPECT_EQ(0, counter % 3); + } + }); + enableAssert = true; + loop.Poll(); + loop.Poll(); + loop.Poll(); + loop.Poll(); +} + +TEST(BooleanEventTest, EdgeDecorators) { + EventLoop loop; + bool boolean = false; + int counter = 0; + + BooleanEvent(&loop, [&] { return boolean; }).Falling().IfHigh([&] { + --counter; + }); + BooleanEvent(&loop, [&] { return boolean; }).Rising().IfHigh([&] { + ++counter; + }); + + EXPECT_EQ(0, counter); + + boolean = false; + loop.Poll(); + + EXPECT_EQ(0, counter); + + boolean = true; + loop.Poll(); + + EXPECT_EQ(1, counter); + + boolean = true; + loop.Poll(); + + EXPECT_EQ(1, counter); + + boolean = false; + loop.Poll(); + + EXPECT_EQ(0, counter); +} + +/** + * Tests that binding actions to the same edge event will result in all actions + * executing. + */ +TEST(BooleanEventTest, EdgeReuse) { + EventLoop loop; + bool boolean = false; + int counter = 0; + + auto event = BooleanEvent(&loop, [&] { return boolean; }).Rising(); + event.IfHigh([&] { ++counter; }); + event.IfHigh([&] { ++counter; }); + + EXPECT_EQ(0, counter); + + loop.Poll(); + + EXPECT_EQ(0, counter); + + boolean = true; + loop.Poll(); + + EXPECT_EQ(2, counter); + + loop.Poll(); + + EXPECT_EQ(2, counter); + + boolean = false; + loop.Poll(); + + EXPECT_EQ(2, counter); + + boolean = true; + loop.Poll(); + + EXPECT_EQ(4, counter); +} + +/** + * Tests that all actions execute on separate edge events constructed from the + * original event. + */ +TEST(BooleanEventTest, EdgeReconstruct) { + EventLoop loop; + bool boolean = false; + int counter = 0; + + auto event = BooleanEvent(&loop, [&] { return boolean; }); + event.Rising().IfHigh([&] { ++counter; }); + event.Rising().IfHigh([&] { ++counter; }); + + EXPECT_EQ(0, counter); + + loop.Poll(); + + EXPECT_EQ(0, counter); + + boolean = true; + loop.Poll(); + + EXPECT_EQ(2, counter); + + loop.Poll(); + + EXPECT_EQ(2, counter); + + boolean = false; + loop.Poll(); + + EXPECT_EQ(2, counter); + + boolean = true; + loop.Poll(); + + EXPECT_EQ(4, counter); +} + +/** Tests that all actions bound to an event will still execute even if the + * signal is changed during the loop poll */ +TEST(BooleanEventTest, MidLoopBooleanChange) { + EventLoop loop; + bool boolean = false; + int counter = 0; + + auto event = BooleanEvent(&loop, [&] { return boolean; }).Rising(); + event.IfHigh([&] { + boolean = false; + ++counter; + }); + event.IfHigh([&] { ++counter; }); + + EXPECT_EQ(0, counter); + + loop.Poll(); + + EXPECT_EQ(0, counter); + + boolean = true; + loop.Poll(); + + EXPECT_EQ(2, counter); + + loop.Poll(); + + EXPECT_EQ(2, counter); + + boolean = false; + loop.Poll(); + + EXPECT_EQ(2, counter); + + boolean = true; + loop.Poll(); + + EXPECT_EQ(4, counter); +} + +/** + * Tests that all actions bound to composed events will still execute even if + * the composed signal changes during the loop poll. + */ +TEST(BooleanEventTest, MidLoopBooleanChangeWithComposedEvents) { + EventLoop loop; + bool boolean1 = false; + bool boolean2 = false; + bool boolean3 = false; + bool boolean4 = false; + int counter = 0; + + auto event1 = BooleanEvent(&loop, [&] { return boolean1; }); + auto event2 = BooleanEvent(&loop, [&] { return boolean2; }); + auto event3 = BooleanEvent(&loop, [&] { return boolean3; }); + auto event4 = BooleanEvent(&loop, [&] { return boolean4; }); + event1.IfHigh([&] { + boolean2 = false; + boolean3 = false; + ++counter; + }); + (event3 || event4).IfHigh([&] { + boolean1 = false; + ++counter; + }); + (event1 && event2).IfHigh([&] { + boolean4 = false; + ++counter; + }); + + EXPECT_EQ(0, counter); + + boolean1 = true; + boolean2 = true; + boolean3 = true; + boolean4 = true; + loop.Poll(); // All three actions execute, incrementing the counter three + // times and setting all booleans to false + + EXPECT_EQ(3, counter); + + loop.Poll(); // Nothing should happen since everything was set to false + + EXPECT_EQ(3, counter); + + boolean1 = true; + boolean2 = true; + loop.Poll(); // Bool 1 and 2 are true, increments counter twice, Bool 2 gets + // set to false + + EXPECT_EQ(5, counter); + + boolean1 = false; + loop.Poll(); // Nothing should happen + + EXPECT_EQ(5, counter); + + boolean1 = true; + boolean3 = true; + loop.Poll(); // Bool 1 and 3 are true, increments counter twice, Bool 3 gets + // set to false + + EXPECT_EQ(7, counter); + + boolean1 = false; + boolean4 = true; + loop.Poll(); // Bool 4 is true, increments counter once + + EXPECT_EQ(8, counter); +} + +TEST(BooleanEventTest, Negation) { + EventLoop loop; + bool boolean = false; + int counter = 0; + + (!BooleanEvent(&loop, [&] { return boolean; })).IfHigh([&] { ++counter; }); + + EXPECT_EQ(0, counter); + + loop.Poll(); + + EXPECT_EQ(1, counter); + + boolean = true; + loop.Poll(); + + EXPECT_EQ(1, counter); + + boolean = false; + loop.Poll(); + + EXPECT_EQ(2, counter); + + boolean = true; + loop.Poll(); + + EXPECT_EQ(2, counter); +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/event/BooleanEvent.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/event/BooleanEvent.java index 0786f34d05..c8f58461c2 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/event/BooleanEvent.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/event/BooleanEvent.java @@ -7,6 +7,7 @@ package edu.wpi.first.wpilibj.event; import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; import edu.wpi.first.math.filter.Debouncer; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; import java.util.function.BooleanSupplier; @@ -28,6 +29,9 @@ public class BooleanEvent implements BooleanSupplier { /** Condition. */ private final BooleanSupplier m_signal; + /** The state of the condition in the current loop poll. Nightmare to manage. */ + private final AtomicBoolean m_state = new AtomicBoolean(false); + /** * Creates a new event with the given signal determining whether it is active. * @@ -37,16 +41,19 @@ public class BooleanEvent implements BooleanSupplier { public BooleanEvent(EventLoop loop, BooleanSupplier signal) { m_loop = requireNonNullParam(loop, "loop", "BooleanEvent"); m_signal = requireNonNullParam(signal, "signal", "BooleanEvent"); + m_state.set(m_signal.getAsBoolean()); + m_loop.bind(() -> m_state.set(m_signal.getAsBoolean())); } /** - * Check the state of this signal (high or low). + * Check the state of this signal (high or low) as of the last loop poll. * - * @return true for the high state, false for the low state. + * @return true for the high state, false for the low state. If the event was never polled, it + * returns the state at event construction. */ @Override public final boolean getAsBoolean() { - return m_signal.getAsBoolean(); + return m_state.get(); } /** @@ -57,7 +64,7 @@ public class BooleanEvent implements BooleanSupplier { public final void ifHigh(Runnable action) { m_loop.bind( () -> { - if (m_signal.getAsBoolean()) { + if (m_state.get()) { action.run(); } }); @@ -72,11 +79,11 @@ public class BooleanEvent implements BooleanSupplier { return new BooleanEvent( m_loop, new BooleanSupplier() { - private boolean m_previous = m_signal.getAsBoolean(); + private boolean m_previous = m_state.get(); @Override public boolean getAsBoolean() { - boolean present = m_signal.getAsBoolean(); + boolean present = m_state.get(); boolean ret = !m_previous && present; m_previous = present; return ret; @@ -93,11 +100,11 @@ public class BooleanEvent implements BooleanSupplier { return new BooleanEvent( m_loop, new BooleanSupplier() { - private boolean m_previous = m_signal.getAsBoolean(); + private boolean m_previous = m_state.get(); @Override public boolean getAsBoolean() { - boolean present = m_signal.getAsBoolean(); + boolean present = m_state.get(); boolean ret = m_previous && !present; m_previous = present; return ret; @@ -132,7 +139,7 @@ public class BooleanEvent implements BooleanSupplier { @Override public boolean getAsBoolean() { - return m_debouncer.calculate(m_signal.getAsBoolean()); + return m_debouncer.calculate(m_state.get()); } }); } @@ -144,35 +151,37 @@ public class BooleanEvent implements BooleanSupplier { * @return the negated event */ public BooleanEvent negate() { - return new BooleanEvent(m_loop, () -> !m_signal.getAsBoolean()); + return new BooleanEvent(m_loop, () -> !m_state.get()); } /** * Composes this event with another event, returning a new signal that is in the high state when * both signals are in the high state. * - *

The new event will use this event's polling loop. + *

The events must use the same event loop. If the events use different event loops, the + * composed signal won't update until both loops are polled. * * @param other the event to compose with * @return the event that is active when both events are active */ public BooleanEvent and(BooleanSupplier other) { requireNonNullParam(other, "other", "and"); - return new BooleanEvent(m_loop, () -> m_signal.getAsBoolean() && other.getAsBoolean()); + return new BooleanEvent(m_loop, () -> m_state.get() && other.getAsBoolean()); } /** * Composes this event with another event, returning a new signal that is high when either signal * is high. * - *

The new event will use this event's polling loop. + *

The events must use the same event loop. If the events use different event loops, the + * composed signal won't update until both loops are polled. * * @param other the event to compose with * @return a signal that is high when either signal is high. */ public BooleanEvent or(BooleanSupplier other) { requireNonNullParam(other, "other", "or"); - return new BooleanEvent(m_loop, () -> m_signal.getAsBoolean() || other.getAsBoolean()); + return new BooleanEvent(m_loop, () -> m_state.get() || other.getAsBoolean()); } /** @@ -185,6 +194,6 @@ public class BooleanEvent implements BooleanSupplier { * @return an instance of the subclass. */ public T castTo(BiFunction ctor) { - return ctor.apply(m_loop, m_signal); + return ctor.apply(m_loop, m_state::get); } } diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/event/BooleanEventTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/event/BooleanEventTest.java index 59caf23e5b..834a0ef3f2 100644 --- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/event/BooleanEventTest.java +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/event/BooleanEventTest.java @@ -30,43 +30,218 @@ class BooleanEventTest { assertEquals(1, orCounter.get()); } + /** + * Tests that composed edge events only execute on edges (two rising edge events composed with + * and() should only execute when both signals are on the rising edge). + */ + @Test + void testBinaryCompositionsWithEdgeDecorators() { + var loop = new EventLoop(); + var bool1 = new AtomicBoolean(false); + var bool2 = new AtomicBoolean(false); + var bool3 = new AtomicBoolean(false); + var bool4 = new AtomicBoolean(false); + var counter = new AtomicInteger(0); + + var event1 = new BooleanEvent(loop, bool1::get).rising(); + var event2 = new BooleanEvent(loop, bool2::get).rising(); + var event3 = new BooleanEvent(loop, bool3::get).rising(); + var event4 = new BooleanEvent(loop, bool4::get).rising(); + event1.and(event2).ifHigh(counter::incrementAndGet); + event3.or(event4).ifHigh(counter::incrementAndGet); + assertEquals(0, counter.get()); + + bool1.set(true); + bool2.set(true); + bool3.set(true); + bool4.set(true); + loop.poll(); // Both actions execute + + assertEquals(2, counter.get()); + + loop.poll(); // Nothing should happen since nothing is on rising edge + + assertEquals(2, counter.get()); + + bool1.set(false); + bool2.set(false); + bool3.set(false); + bool4.set(false); + loop.poll(); // Nothing should happen + + assertEquals(2, counter.get()); + + bool1.set(true); + loop.poll(); // Nothing should happen since only Bool 1 is on rising edge + + assertEquals(2, counter.get()); + + bool2.set(true); + loop.poll(); // Bool 2 is on rising edge, but Bool 1 isn't, nothing should happen + + assertEquals(2, counter.get()); + + bool1.set(false); + bool2.set(false); + loop.poll(); // Nothing should happen + + assertEquals(2, counter.get()); + + bool1.set(true); + bool2.set(true); + loop.poll(); // Bool 1 and 2 are on rising edge, increments counter once + + assertEquals(3, counter.get()); + + bool3.set(true); + loop.poll(); // Bool 3 is on rising edge, increments counter once + + assertEquals(4, counter.get()); + + loop.poll(); // Nothing should happen, Bool 3 isn't on rising edge + + assertEquals(4, counter.get()); + + bool4.set(true); + loop.poll(); // Bool 4 is on rising edge, increments counter once + + assertEquals(5, counter.get()); + + loop.poll(); // Nothing should happen, Bool 4 isn't on rising edge + + assertEquals(5, counter.get()); + } + @Test void testBinaryCompositionLoopSemantics() { var loop1 = new EventLoop(); var loop2 = new EventLoop(); - + var bool1 = new AtomicBoolean(true); + var bool2 = new AtomicBoolean(true); var counter1 = new AtomicInteger(0); var counter2 = new AtomicInteger(0); - new BooleanEvent(loop1, () -> true) - .and(new BooleanEvent(loop2, () -> true)) + new BooleanEvent(loop1, bool1::get) + .and(new BooleanEvent(loop2, bool2::get)) .ifHigh(counter1::incrementAndGet); - new BooleanEvent(loop2, () -> true) - .and(new BooleanEvent(loop1, () -> true)) + new BooleanEvent(loop2, bool2::get) + .and(new BooleanEvent(loop1, bool1::get)) .ifHigh(counter2::incrementAndGet); assertEquals(0, counter1.get()); assertEquals(0, counter2.get()); - loop1.poll(); + loop1.poll(); // 1st event executes, Bool 1 and 2 are true, increments counter assertEquals(1, counter1.get()); assertEquals(0, counter2.get()); - loop2.poll(); + loop2.poll(); // 2nd event executes, Bool 1 and 2 are true, increments counter assertEquals(1, counter1.get()); assertEquals(1, counter2.get()); + + bool2.set(false); + loop1.poll(); // 1st event executes, Bool 2 is still true because loop 2 hasn't updated it, + // increments counter + + assertEquals(2, counter1.get()); + assertEquals(1, counter2.get()); + + loop2.poll(); // 2nd event executes, Bool 2 is now false because this loop updated it, does + // nothing + + assertEquals(2, counter1.get()); + assertEquals(1, counter2.get()); + + loop1.poll(); // All bools are updated at this point, nothing should happen + + assertEquals(2, counter1.get()); + assertEquals(1, counter2.get()); + + bool2.set(true); + loop2.poll(); // 2nd event executes, Bool 2 is true because this loop updated it, increments + // counter + + assertEquals(2, counter1.get()); + assertEquals(2, counter2.get()); + + loop1 + .poll(); // 1st event executes, Bool 2 is true because loop 2 updated it, increments counter + + assertEquals(3, counter1.get()); + assertEquals(2, counter2.get()); + + bool1.set(false); + loop2.poll(); // 2nd event executes, Bool 1 is still true because loop 1 hasn't updated it, + // increments counter + + assertEquals(3, counter1.get()); + assertEquals(3, counter2.get()); + + loop1.poll(); // 1st event executes, Bool 1 is false because this loop updated it, does nothing + + assertEquals(3, counter1.get()); + assertEquals(3, counter2.get()); + + loop2.poll(); // All bools are updated at this point, nothing should happen + + assertEquals(3, counter1.get()); + assertEquals(3, counter2.get()); + } + + /** Tests the order of actions bound to an event loop. */ + @Test + void testPollOrdering() { + var loop = new EventLoop(); + var bool1 = new AtomicBoolean(true); + var bool2 = new AtomicBoolean(true); + var enableAssert = new AtomicBoolean(false); + var counter = new AtomicInteger(0); + // This event binds an action to the event loop first + new BooleanEvent( + loop, + () -> { + if (enableAssert.get()) { + counter.incrementAndGet(); + assertEquals(1, counter.get() % 3); + } + return bool1.get(); + }) + // The composed event binds an action to the event loop third + .and( + // This event binds an action to the event loop second + new BooleanEvent( + loop, + () -> { + if (enableAssert.get()) { + counter.incrementAndGet(); + assertEquals(2, counter.get() % 3); + } + return bool2.get(); + })) + // This binds an action to the event loop fourth + .ifHigh( + () -> { + if (enableAssert.get()) { + counter.incrementAndGet(); + assertEquals(0, counter.get() % 3); + } + }); + enableAssert.set(true); + loop.poll(); + loop.poll(); + loop.poll(); + loop.poll(); } @Test void testEdgeDecorators() { + var loop = new EventLoop(); var bool = new AtomicBoolean(false); var counter = new AtomicInteger(0); - var loop = new EventLoop(); - new BooleanEvent(loop, bool::get).falling().ifHigh(counter::decrementAndGet); new BooleanEvent(loop, bool::get).rising().ifHigh(counter::incrementAndGet); @@ -93,6 +268,7 @@ class BooleanEventTest { assertEquals(0, counter.get()); } + /** Tests that binding actions to the same edge event will result in all actions executing. */ @Test void testEdgeReuse() { var loop = new EventLoop(); @@ -112,23 +288,24 @@ class BooleanEventTest { bool.set(true); loop.poll(); - assertEquals(1, counter.get()); // FIXME?: natural sense dictates counter == 2!! + assertEquals(2, counter.get()); loop.poll(); - assertEquals(1, counter.get()); + assertEquals(2, counter.get()); bool.set(false); loop.poll(); - assertEquals(1, counter.get()); + assertEquals(2, counter.get()); bool.set(true); loop.poll(); - assertEquals(2, counter.get()); + assertEquals(4, counter.get()); } + /** Tests that all actions execute on separate edge events constructed from the original event. */ @Test void testEdgeReconstruct() { var loop = new EventLoop(); @@ -148,8 +325,7 @@ class BooleanEventTest { bool.set(true); loop.poll(); - // unlike the previous test ... - assertEquals(2, counter.get()); // as natural sense dictates, counter == 2 + assertEquals(2, counter.get()); loop.poll(); @@ -165,4 +341,155 @@ class BooleanEventTest { assertEquals(4, counter.get()); } + + /** + * Tests that all actions bound to an event will still execute even if the signal is changed + * during the loop poll. + */ + @Test + void testMidLoopBooleanChange() { + var loop = new EventLoop(); + var bool = new AtomicBoolean(false); + var counter = new AtomicInteger(0); + + var event = new BooleanEvent(loop, bool::get).rising(); + event.ifHigh( + () -> { + bool.set(false); + counter.incrementAndGet(); + }); + event.ifHigh(counter::incrementAndGet); + + assertEquals(0, counter.get()); + + loop.poll(); + + assertEquals(0, counter.get()); + + bool.set(true); + loop.poll(); + + assertEquals(2, counter.get()); + + loop.poll(); + + assertEquals(2, counter.get()); + + bool.set(false); + loop.poll(); + + assertEquals(2, counter.get()); + + bool.set(true); + loop.poll(); + + assertEquals(4, counter.get()); + } + + /** + * Tests that all actions bound to composed events will still execute even if the composed signal + * changes during the loop poll. + */ + @Test + void testMidLoopBooleanChangeWithComposedEvents() { + var loop = new EventLoop(); + var bool1 = new AtomicBoolean(false); + var bool2 = new AtomicBoolean(false); + var bool3 = new AtomicBoolean(false); + var bool4 = new AtomicBoolean(false); + var counter = new AtomicInteger(0); + + var event1 = new BooleanEvent(loop, bool1::get); + var event2 = new BooleanEvent(loop, bool2::get); + var event3 = new BooleanEvent(loop, bool3::get); + var event4 = new BooleanEvent(loop, bool4::get); + event1.ifHigh( + () -> { + bool2.set(false); + bool3.set(false); + counter.incrementAndGet(); + }); + event3 + .or(event4) + .ifHigh( + () -> { + bool1.set(false); + counter.incrementAndGet(); + }); + event1 + .and(event2) + .ifHigh( + () -> { + bool4.set(false); + counter.incrementAndGet(); + }); + + assertEquals(0, counter.get()); + + bool1.set(true); + bool2.set(true); + bool3.set(true); + bool4.set(true); + loop.poll(); // All three actions execute, incrementing the counter three times and setting all + // booleans to false + + assertEquals(3, counter.get()); + + loop.poll(); // Nothing should happen since everything was set to false + + assertEquals(3, counter.get()); + + bool1.set(true); + bool2.set(true); + loop.poll(); // Bool 1 and 2 are true, increments counter twice, Bool 2 gets set to false + + assertEquals(5, counter.get()); + + bool1.set(false); + loop.poll(); // Nothing should happen + + assertEquals(5, counter.get()); + + bool1.set(true); + bool3.set(true); + loop.poll(); // Bool 1 and 3 are true, increments counter twice, Bool 3 gets set to false + + assertEquals(7, counter.get()); + + bool1.set(false); + bool4.set(true); + loop.poll(); // Bool 4 is true, increments counter once + + assertEquals(8, counter.get()); + } + + @Test + void testNegation() { + var loop = new EventLoop(); + var bool = new AtomicBoolean(false); + var counter = new AtomicInteger(0); + + new BooleanEvent(loop, bool::get).negate().ifHigh(counter::incrementAndGet); + + assertEquals(0, counter.get()); + + loop.poll(); + + assertEquals(1, counter.get()); + + bool.set(true); + loop.poll(); + + assertEquals(1, counter.get()); + + bool.set(false); + loop.poll(); + + assertEquals(2, counter.get()); + + bool.set(true); + loop.poll(); + + assertEquals(2, counter.get()); + } }