[wpilib] Make BooleanEvent more consistent (#5436)

Co-authored-by: Tyler Veness <calcmogul@gmail.com>
Co-authored-by: Joseph Eng <91924258+KangarooKoala@users.noreply.github.com>
This commit is contained in:
Gold856
2023-10-05 00:22:57 -04:00
committed by GitHub
parent 6576d9b474
commit 58141d6eb5
5 changed files with 890 additions and 58 deletions

View File

@@ -7,56 +7,61 @@
using namespace frc;
BooleanEvent::BooleanEvent(EventLoop* loop, std::function<bool()> condition)
: m_loop(loop), m_condition(std::move(condition)) {}
: m_loop(loop), m_condition(std::move(condition)) {
m_state = std::make_shared<bool>(m_condition());
m_loop->Bind(
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
[condition = m_condition, state = m_state] { *state = condition(); });
}
BooleanEvent::operator std::function<bool()>() {
return m_condition;
return [state = m_state] { return *state; };
}
bool BooleanEvent::GetAsBoolean() const {
return m_condition();
return *m_state;
}
void BooleanEvent::IfHigh(std::function<void()> 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<bool()> 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<bool()> 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); });
}

View File

@@ -40,9 +40,10 @@ class BooleanEvent {
BooleanEvent(EventLoop* loop, std::function<bool()> 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<bool()> 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.
*
* <p>The new event will use this event's polling loop.
* <p>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.
*
* <p>The new event will use this event's polling loop.
* <p>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<bool()> m_condition;
std::shared_ptr<bool> m_state; // A programmer's worst nightmare.
};
} // namespace frc

View File

@@ -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 <atomic>
#include <gtest/gtest.h>
#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);
}

View File

@@ -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.
*
* <p>The new event will use this event's polling loop.
* <p>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.
*
* <p>The new event will use this event's polling loop.
* <p>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 extends BooleanSupplier> T castTo(BiFunction<EventLoop, BooleanSupplier, T> ctor) {
return ctor.apply(m_loop, m_signal);
return ctor.apply(m_loop, m_state::get);
}
}

View File

@@ -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());
}
}