2025-10-10 16:47:22 -04:00
|
|
|
// 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.
|
|
|
|
|
|
2025-11-07 19:55:43 -05:00
|
|
|
package org.wpilib.command3;
|
2025-10-10 16:47:22 -04:00
|
|
|
|
2026-05-07 19:32:34 -04:00
|
|
|
import static org.junit.jupiter.api.Assertions.assertAll;
|
2025-10-10 16:47:22 -04:00
|
|
|
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 java.util.List;
|
|
|
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
2026-05-07 19:32:34 -04:00
|
|
|
import java.util.function.BooleanSupplier;
|
2025-10-10 16:47:22 -04:00
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
|
|
|
|
|
|
class TriggerTest extends CommandTestBase {
|
|
|
|
|
@Test
|
|
|
|
|
void onTrue() {
|
|
|
|
|
var signal = new AtomicBoolean(false);
|
|
|
|
|
var trigger = new Trigger(m_scheduler, signal::get);
|
2026-04-19 18:40:50 -04:00
|
|
|
var command = Command.noRequirements(Coroutine::park).named("Command");
|
2025-10-10 16:47:22 -04:00
|
|
|
trigger.onTrue(command);
|
|
|
|
|
|
|
|
|
|
signal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
|
|
|
|
|
assertTrue(m_scheduler.isRunning(command), "Command was not scheduled on rising edge");
|
|
|
|
|
|
|
|
|
|
signal.set(false);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(m_scheduler.isRunning(command), "Command should still be running on falling edge");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void onFalse() {
|
|
|
|
|
var signal = new AtomicBoolean(false);
|
|
|
|
|
var trigger = new Trigger(m_scheduler, signal::get);
|
2026-04-19 18:40:50 -04:00
|
|
|
var command = Command.noRequirements(Coroutine::park).named("Command");
|
2025-10-10 16:47:22 -04:00
|
|
|
trigger.onFalse(command);
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(
|
|
|
|
|
m_scheduler.isRunning(command), "Command should be scheduled when signal starts low");
|
|
|
|
|
|
|
|
|
|
signal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(
|
|
|
|
|
m_scheduler.isRunning(command), "Command should still be running rising falling edge");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void whileTrue() {
|
|
|
|
|
var signal = new AtomicBoolean(false);
|
|
|
|
|
var trigger = new Trigger(m_scheduler, signal::get);
|
2026-04-19 18:40:50 -04:00
|
|
|
var command = Command.noRequirements(Coroutine::park).named("Command");
|
2025-10-10 16:47:22 -04:00
|
|
|
trigger.whileTrue(command);
|
|
|
|
|
|
|
|
|
|
signal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
|
|
|
|
|
assertTrue(m_scheduler.isRunning(command), "Command was not scheduled on rising edge");
|
|
|
|
|
|
|
|
|
|
signal.set(false);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(m_scheduler.isRunning(command), "Command should be canceled on falling edge");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void whileFalse() {
|
|
|
|
|
var signal = new AtomicBoolean(false);
|
|
|
|
|
var trigger = new Trigger(m_scheduler, signal::get);
|
2026-04-19 18:40:50 -04:00
|
|
|
var command = Command.noRequirements(Coroutine::park).named("Command");
|
2025-10-10 16:47:22 -04:00
|
|
|
trigger.whileFalse(command);
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(
|
|
|
|
|
m_scheduler.isRunning(command), "Command should be scheduled when signal starts low");
|
|
|
|
|
|
|
|
|
|
signal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(m_scheduler.isRunning(command), "Command should be canceled on rising edge");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void toggleOnTrue() {
|
|
|
|
|
var signal = new AtomicBoolean(false);
|
|
|
|
|
var trigger = new Trigger(m_scheduler, signal::get);
|
2026-04-19 18:40:50 -04:00
|
|
|
var command = Command.noRequirements(Coroutine::park).named("Command");
|
2025-10-10 16:47:22 -04:00
|
|
|
trigger.toggleOnTrue(command);
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(m_scheduler.isRunning(command));
|
|
|
|
|
|
|
|
|
|
signal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(m_scheduler.isRunning(command));
|
|
|
|
|
|
|
|
|
|
signal.set(false);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(m_scheduler.isRunning(command));
|
|
|
|
|
|
|
|
|
|
signal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(m_scheduler.isRunning(command));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void toggleOnFalse() {
|
|
|
|
|
var signal = new AtomicBoolean(false);
|
|
|
|
|
var trigger = new Trigger(m_scheduler, signal::get);
|
2026-04-19 18:40:50 -04:00
|
|
|
var command = Command.noRequirements(Coroutine::park).named("Command");
|
2025-10-10 16:47:22 -04:00
|
|
|
trigger.toggleOnFalse(command);
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(m_scheduler.isRunning(command));
|
|
|
|
|
|
|
|
|
|
signal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(m_scheduler.isRunning(command));
|
|
|
|
|
|
|
|
|
|
signal.set(false);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(m_scheduler.isRunning(command));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void commandScoping() {
|
|
|
|
|
var innerRan = new AtomicBoolean(false);
|
|
|
|
|
var innerSignal = new AtomicBoolean(false);
|
|
|
|
|
|
|
|
|
|
var inner =
|
2026-04-19 18:40:50 -04:00
|
|
|
Command.noRequirements(
|
2025-10-10 16:47:22 -04:00
|
|
|
co -> {
|
|
|
|
|
while (true) {
|
|
|
|
|
innerRan.set(true);
|
|
|
|
|
co.park();
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.named("Inner");
|
|
|
|
|
|
|
|
|
|
var outer =
|
2026-04-19 18:40:50 -04:00
|
|
|
Command.noRequirements(
|
2025-10-10 16:47:22 -04:00
|
|
|
co -> {
|
|
|
|
|
new Trigger(m_scheduler, innerSignal::get).onTrue(inner);
|
|
|
|
|
// If we yield, then the outer command exits and immediately cancels the
|
|
|
|
|
// on-deck inner command before it can run
|
|
|
|
|
co.park();
|
|
|
|
|
})
|
|
|
|
|
.named("Outer");
|
|
|
|
|
|
|
|
|
|
m_scheduler.schedule(outer);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(innerRan.get(), "The bound command should not run before the signal is set");
|
|
|
|
|
|
|
|
|
|
innerSignal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(innerRan.get(), "The nested trigger should have run the bound command");
|
|
|
|
|
|
|
|
|
|
innerRan.set(false);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(innerRan.get(), "Trigger should not have fired again");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void scopeGoingInactiveCancelsBoundCommand() {
|
|
|
|
|
var activeScope = new AtomicBoolean(true);
|
|
|
|
|
BindingScope scope = activeScope::get;
|
|
|
|
|
|
|
|
|
|
var triggerSignal = new AtomicBoolean(false);
|
|
|
|
|
var trigger = new Trigger(m_scheduler, triggerSignal::get);
|
|
|
|
|
|
2026-04-19 18:40:50 -04:00
|
|
|
var command = Command.noRequirements(Coroutine::park).named("Command");
|
2025-10-10 16:47:22 -04:00
|
|
|
trigger.addBinding(scope, BindingType.RUN_WHILE_HIGH, command);
|
|
|
|
|
|
|
|
|
|
triggerSignal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(m_scheduler.isRunning(command), "Command should have started when triggered");
|
|
|
|
|
|
|
|
|
|
activeScope.set(false);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(
|
|
|
|
|
m_scheduler.isRunning(command),
|
|
|
|
|
"Command should have been canceled when scope became inactive");
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 01:24:58 -05:00
|
|
|
@Test
|
|
|
|
|
void bindingScopesToOpmodeIfAvailable() {
|
|
|
|
|
var fetcher =
|
|
|
|
|
new OpModeFetcher() {
|
|
|
|
|
long m_id = 12345;
|
|
|
|
|
|
|
|
|
|
void clear() {
|
|
|
|
|
m_id = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
long getOpModeId() {
|
|
|
|
|
return m_id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
String getOpModeName() {
|
|
|
|
|
return "This is an opmode!";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
OpModeFetcher.setFetcher(fetcher);
|
|
|
|
|
|
|
|
|
|
var triggerSignal = new AtomicBoolean(false);
|
|
|
|
|
var trigger = new Trigger(m_scheduler, triggerSignal::get);
|
|
|
|
|
|
2026-04-19 18:40:50 -04:00
|
|
|
var command = Command.noRequirements(Coroutine::park).named("Command");
|
2025-12-17 01:24:58 -05:00
|
|
|
trigger.whileTrue(command);
|
|
|
|
|
|
|
|
|
|
triggerSignal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(m_scheduler.isRunning(command), "Command should have started when triggered");
|
|
|
|
|
|
|
|
|
|
fetcher.clear();
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(m_scheduler.isRunning(command), "Command should have stopped when opmode exited");
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-10 16:47:22 -04:00
|
|
|
// The scheduler lifecycle polls triggers at the start of `run()`
|
|
|
|
|
// Even though the trigger condition is set, the command exits and the trigger's scope goes
|
|
|
|
|
// inactive before the next `run()` call can poll the trigger
|
|
|
|
|
@Test
|
|
|
|
|
void triggerFromExitingCommandDoesNotFire() {
|
|
|
|
|
var condition = new AtomicBoolean(false);
|
|
|
|
|
var triggeredCommandRan = new AtomicBoolean(false);
|
|
|
|
|
|
|
|
|
|
var inner =
|
2026-04-19 18:40:50 -04:00
|
|
|
Command.noRequirements(
|
2025-10-10 16:47:22 -04:00
|
|
|
co -> {
|
|
|
|
|
triggeredCommandRan.set(true);
|
|
|
|
|
co.park();
|
|
|
|
|
})
|
|
|
|
|
.named("Inner");
|
|
|
|
|
|
|
|
|
|
var awaited =
|
2026-04-19 18:40:50 -04:00
|
|
|
Command.noRequirements(
|
2025-10-10 16:47:22 -04:00
|
|
|
co -> {
|
|
|
|
|
co.yield();
|
|
|
|
|
condition.set(true);
|
|
|
|
|
})
|
|
|
|
|
.named("Awaited");
|
|
|
|
|
|
|
|
|
|
var outer =
|
2026-04-19 18:40:50 -04:00
|
|
|
Command.noRequirements(
|
2025-10-10 16:47:22 -04:00
|
|
|
co -> {
|
|
|
|
|
new Trigger(m_scheduler, condition::get).onTrue(inner);
|
|
|
|
|
co.await(awaited);
|
|
|
|
|
})
|
|
|
|
|
.named("Outer");
|
|
|
|
|
|
|
|
|
|
m_scheduler.schedule(outer);
|
|
|
|
|
|
|
|
|
|
// First run: schedules `awaited`, yields
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(m_scheduler.isRunning(outer));
|
|
|
|
|
assertTrue(m_scheduler.isRunning(awaited));
|
|
|
|
|
assertEquals(
|
|
|
|
|
List.of("Outer", "Awaited"),
|
|
|
|
|
m_scheduler.getRunningCommands().stream().map(Command::name).toList());
|
|
|
|
|
|
|
|
|
|
// Second run: `awaited` resumes, sets the condition, exits. `outer` exits its final `yield`
|
|
|
|
|
// and will exit on the next run. The trigger condition has been set, but the trigger is checked
|
|
|
|
|
// on the next call to `run()`
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertEquals(List.of(), m_scheduler.getRunningCommands().stream().map(Command::name).toList());
|
|
|
|
|
assertTrue(condition.get(), "Condition wasn't set");
|
|
|
|
|
assertFalse(triggeredCommandRan.get(), "Command was unexpectedly triggered");
|
|
|
|
|
|
|
|
|
|
// Third run: trigger binding fires (outside a running command) and queues up `inner`.
|
|
|
|
|
// However, the inner command's lifetime is bound to `outer`, and is immediately canceled before
|
|
|
|
|
// it can run when the outer command exits.
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertEquals(List.of(), m_scheduler.getRunningCommands().stream().map(Command::name).toList());
|
|
|
|
|
assertFalse(triggeredCommandRan.get(), "Command was unexpectedly triggered");
|
|
|
|
|
}
|
2026-05-07 19:32:34 -04:00
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void risingEdge() {
|
|
|
|
|
var signal = new AtomicBoolean(false);
|
|
|
|
|
var baseTrigger = new Trigger(m_scheduler, signal::get);
|
|
|
|
|
var risingEdgeTrigger = baseTrigger.risingEdge();
|
|
|
|
|
|
|
|
|
|
assertAll(
|
|
|
|
|
"Signals start null",
|
|
|
|
|
() -> assertEquals(null, baseTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(null, baseTrigger.getPreviousSignal()),
|
|
|
|
|
() -> assertEquals(null, risingEdgeTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(null, risingEdgeTrigger.getPreviousSignal()));
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertAll(
|
|
|
|
|
"First run (base signal stays low)",
|
|
|
|
|
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(null, baseTrigger.getPreviousSignal()),
|
|
|
|
|
() -> assertEquals(Trigger.Signal.LOW, risingEdgeTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(null, risingEdgeTrigger.getPreviousSignal()));
|
|
|
|
|
|
|
|
|
|
signal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertAll(
|
|
|
|
|
"Second run (base signal goes high)",
|
|
|
|
|
() -> assertEquals(Trigger.Signal.HIGH, baseTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getPreviousSignal()),
|
|
|
|
|
() ->
|
|
|
|
|
assertEquals(
|
|
|
|
|
Trigger.Signal.HIGH,
|
|
|
|
|
risingEdgeTrigger.getCachedSignal(),
|
|
|
|
|
"Rising edge trigger did not go high"),
|
|
|
|
|
() -> assertEquals(Trigger.Signal.LOW, risingEdgeTrigger.getPreviousSignal()));
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertAll(
|
|
|
|
|
"Third run (base signal stays high)",
|
|
|
|
|
() -> assertEquals(Trigger.Signal.HIGH, baseTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(Trigger.Signal.HIGH, baseTrigger.getPreviousSignal()),
|
|
|
|
|
() ->
|
|
|
|
|
assertEquals(
|
|
|
|
|
Trigger.Signal.LOW,
|
|
|
|
|
risingEdgeTrigger.getCachedSignal(),
|
|
|
|
|
"Rising edge trigger did not go low"),
|
|
|
|
|
() ->
|
|
|
|
|
assertEquals(
|
|
|
|
|
Trigger.Signal.HIGH,
|
|
|
|
|
risingEdgeTrigger.getPreviousSignal(),
|
|
|
|
|
"Rising edge trigger was not previously high"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void fallingEdge() {
|
|
|
|
|
var signal = new AtomicBoolean(false);
|
|
|
|
|
var baseTrigger = new Trigger(m_scheduler, signal::get);
|
|
|
|
|
var fallingEdgeTrigger = baseTrigger.fallingEdge();
|
|
|
|
|
|
|
|
|
|
assertAll(
|
|
|
|
|
"Signals start null",
|
|
|
|
|
() -> assertEquals(null, baseTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(null, baseTrigger.getPreviousSignal()),
|
|
|
|
|
() -> assertEquals(null, fallingEdgeTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(null, fallingEdgeTrigger.getPreviousSignal()));
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertAll(
|
|
|
|
|
"First run (base signal stays low)",
|
|
|
|
|
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(null, baseTrigger.getPreviousSignal()),
|
|
|
|
|
() -> assertEquals(Trigger.Signal.LOW, fallingEdgeTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(null, fallingEdgeTrigger.getPreviousSignal()));
|
|
|
|
|
|
|
|
|
|
signal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertAll(
|
|
|
|
|
"Second run (base signal goes high)",
|
|
|
|
|
() -> assertEquals(Trigger.Signal.HIGH, baseTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getPreviousSignal()),
|
|
|
|
|
() -> assertEquals(Trigger.Signal.LOW, fallingEdgeTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(Trigger.Signal.LOW, fallingEdgeTrigger.getPreviousSignal()));
|
|
|
|
|
|
|
|
|
|
signal.set(false);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertAll(
|
|
|
|
|
"Third run (base signal goes low)",
|
|
|
|
|
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(Trigger.Signal.HIGH, baseTrigger.getPreviousSignal()),
|
|
|
|
|
() ->
|
|
|
|
|
assertEquals(
|
|
|
|
|
Trigger.Signal.HIGH,
|
|
|
|
|
fallingEdgeTrigger.getCachedSignal(),
|
|
|
|
|
"Falling edge trigger did not go high"),
|
|
|
|
|
() ->
|
|
|
|
|
assertEquals(
|
|
|
|
|
Trigger.Signal.LOW,
|
|
|
|
|
fallingEdgeTrigger.getPreviousSignal(),
|
|
|
|
|
"Falling edge trigger was not previously low"));
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertAll(
|
|
|
|
|
"Fourth run (base signal stays low)",
|
|
|
|
|
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getCachedSignal()),
|
|
|
|
|
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getPreviousSignal()),
|
|
|
|
|
() ->
|
|
|
|
|
assertEquals(
|
|
|
|
|
Trigger.Signal.LOW,
|
|
|
|
|
fallingEdgeTrigger.getCachedSignal(),
|
|
|
|
|
"Falling edge trigger did not go low"),
|
|
|
|
|
() ->
|
|
|
|
|
assertEquals(
|
|
|
|
|
Trigger.Signal.HIGH,
|
|
|
|
|
fallingEdgeTrigger.getPreviousSignal(),
|
|
|
|
|
"Falling edge trigger was not previously high"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void ensureBoundBindsDependencies() {
|
|
|
|
|
var a = new AtomicBoolean(false);
|
|
|
|
|
var b = new AtomicBoolean(false);
|
|
|
|
|
|
|
|
|
|
var baseA = new Trigger(m_scheduler, a::get);
|
|
|
|
|
var baseB = new Trigger(m_scheduler, b::get);
|
|
|
|
|
|
|
|
|
|
// Compose a trigger that depends on an intermediate, unbound risingEdge() trigger
|
|
|
|
|
var composed = baseA.and(baseB.risingEdge());
|
|
|
|
|
|
|
|
|
|
var command = Command.noRequirements(Coroutine::park).named("Cmd");
|
|
|
|
|
// Bind only the composed trigger; ensureBound() must bind dependencies first so polling order
|
|
|
|
|
// updates base triggers before evaluating the composed condition.
|
|
|
|
|
composed.onTrue(command);
|
|
|
|
|
|
|
|
|
|
// First run initializes all signals to LOW
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(
|
|
|
|
|
m_scheduler.isRunning(command), "Command should not run on first initialization run");
|
|
|
|
|
|
|
|
|
|
// Cause both conditions to be true in the same cycle: A is true, and B has a rising edge
|
|
|
|
|
a.set(true);
|
|
|
|
|
b.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
|
|
|
|
|
assertTrue(
|
|
|
|
|
m_scheduler.isRunning(command),
|
|
|
|
|
"Top-level composed trigger did not fire when dependency rising edge occurred");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void ensureBoundDeeplyNestedDependencies() {
|
|
|
|
|
var a = new AtomicBoolean(false);
|
|
|
|
|
var b = new AtomicBoolean(false);
|
|
|
|
|
var c = new AtomicBoolean(false);
|
|
|
|
|
|
|
|
|
|
var baseA = new Trigger(m_scheduler, a::get);
|
|
|
|
|
var baseB = new Trigger(m_scheduler, b::get);
|
|
|
|
|
var baseC = new Trigger(m_scheduler, c::get);
|
|
|
|
|
|
|
|
|
|
// Two levels of nesting: baseA AND (baseB.risingEdge() AND baseC.risingEdge())
|
|
|
|
|
final var nested = baseA.and(baseB.risingEdge().and(baseC.risingEdge()));
|
|
|
|
|
|
|
|
|
|
// Initialize signals
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
|
|
|
|
|
// Trigger both rising edges and set A high in the same cycle
|
|
|
|
|
a.set(true);
|
|
|
|
|
b.set(true);
|
|
|
|
|
c.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
|
|
|
|
|
assertTrue(
|
|
|
|
|
nested.getAsBoolean(),
|
|
|
|
|
"Deeply nested composed trigger did not fire; dependencies may not have been bound first");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void composedAnd() {
|
|
|
|
|
var signalA = new AtomicBoolean(false);
|
|
|
|
|
var signalB = new AtomicBoolean(false);
|
|
|
|
|
var triggerA = new Trigger(m_scheduler, flickering(signalA));
|
|
|
|
|
var triggerB = new Trigger(m_scheduler, flickering(signalB));
|
|
|
|
|
|
|
|
|
|
var andTrigger = triggerA.and(triggerB);
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(andTrigger.getAsBoolean());
|
|
|
|
|
|
|
|
|
|
signalA.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(andTrigger.getAsBoolean());
|
|
|
|
|
|
|
|
|
|
signalA.set(true);
|
|
|
|
|
signalB.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(andTrigger.getAsBoolean());
|
|
|
|
|
|
|
|
|
|
signalA.set(false);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(andTrigger.getAsBoolean());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void composedAndWithSupplier() {
|
|
|
|
|
var signalA = new AtomicBoolean(false);
|
|
|
|
|
var signalB = new AtomicBoolean(false);
|
|
|
|
|
var triggerA = new Trigger(m_scheduler, flickering(signalA));
|
|
|
|
|
|
|
|
|
|
var andTrigger = triggerA.and(flickering(signalB));
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(andTrigger.getAsBoolean());
|
|
|
|
|
|
|
|
|
|
signalA.set(true);
|
|
|
|
|
signalB.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(andTrigger.getAsBoolean());
|
|
|
|
|
|
|
|
|
|
signalB.set(false);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(andTrigger.getAsBoolean());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void composedOr() {
|
|
|
|
|
var signalA = new AtomicBoolean(false);
|
|
|
|
|
var signalB = new AtomicBoolean(false);
|
|
|
|
|
var triggerA = new Trigger(m_scheduler, flickering(signalA));
|
|
|
|
|
var triggerB = new Trigger(m_scheduler, flickering(signalB));
|
|
|
|
|
|
|
|
|
|
var orTrigger = triggerA.or(triggerB);
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(orTrigger.getAsBoolean());
|
|
|
|
|
|
|
|
|
|
signalA.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(orTrigger.getAsBoolean());
|
|
|
|
|
|
|
|
|
|
signalA.set(false);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(orTrigger.getAsBoolean());
|
|
|
|
|
|
|
|
|
|
signalB.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(orTrigger.getAsBoolean());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void composedOrWithSupplier() {
|
|
|
|
|
var signalA = new AtomicBoolean(false);
|
|
|
|
|
var signalB = new AtomicBoolean(false);
|
|
|
|
|
var triggerA = new Trigger(m_scheduler, flickering(signalA));
|
|
|
|
|
|
|
|
|
|
var orTrigger = triggerA.or(flickering(signalB));
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(orTrigger.getAsBoolean());
|
|
|
|
|
|
|
|
|
|
signalB.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(orTrigger.getAsBoolean());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void composedNegate() {
|
|
|
|
|
var signal = new AtomicBoolean(false);
|
|
|
|
|
var trigger = new Trigger(m_scheduler, flickering(signal));
|
|
|
|
|
|
|
|
|
|
var negated = trigger.negate();
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(negated.getAsBoolean());
|
|
|
|
|
|
|
|
|
|
signal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(negated.getAsBoolean());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void selfComposition() {
|
|
|
|
|
var signal = new AtomicBoolean(false);
|
|
|
|
|
var trigger = new Trigger(m_scheduler, flickering(signal));
|
|
|
|
|
|
|
|
|
|
var selfAnd = trigger.and(trigger);
|
|
|
|
|
var selfOr = trigger.or(trigger);
|
|
|
|
|
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(selfAnd.getAsBoolean());
|
|
|
|
|
assertFalse(selfOr.getAsBoolean());
|
|
|
|
|
|
|
|
|
|
signal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(selfAnd.getAsBoolean());
|
|
|
|
|
assertTrue(selfOr.getAsBoolean());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void complexComposition() {
|
|
|
|
|
var signalA = new AtomicBoolean(false);
|
|
|
|
|
var signalB = new AtomicBoolean(false);
|
|
|
|
|
var signalC = new AtomicBoolean(false);
|
|
|
|
|
var triggerA = new Trigger(m_scheduler, flickering(signalA));
|
|
|
|
|
var triggerB = new Trigger(m_scheduler, flickering(signalB));
|
|
|
|
|
var triggerC = new Trigger(m_scheduler, flickering(signalC));
|
|
|
|
|
|
|
|
|
|
// (A and B) or (not C)
|
|
|
|
|
var composed = triggerA.and(triggerB).or(triggerC.negate());
|
|
|
|
|
|
|
|
|
|
// Initially A=F, B=F, C=F. (F and F) or (not F) -> F or T -> T
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(composed.getAsBoolean());
|
|
|
|
|
|
|
|
|
|
// A=T, B=T, C=T. (T and T) or (not T) -> T or F -> T
|
|
|
|
|
signalA.set(true);
|
|
|
|
|
signalB.set(true);
|
|
|
|
|
signalC.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(composed.getAsBoolean());
|
|
|
|
|
|
|
|
|
|
// A=F, B=T, C=T. (F and T) or (not T) -> F or F -> F
|
|
|
|
|
signalA.set(false);
|
|
|
|
|
signalC.set(true); // Ensure C is high for next run if it flickered
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(composed.getAsBoolean());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void triggerUnbindsWhenCommandScopeInactive() {
|
|
|
|
|
var triggerSignal = new AtomicBoolean(false);
|
|
|
|
|
var commandRan = new AtomicBoolean(false);
|
|
|
|
|
var innerCommand = Command.noRequirements(_ -> commandRan.set(true)).named("Inner");
|
|
|
|
|
|
|
|
|
|
var outerCommand =
|
|
|
|
|
Command.noRequirements(
|
|
|
|
|
co -> {
|
|
|
|
|
var trigger = new Trigger(m_scheduler, triggerSignal::get);
|
|
|
|
|
trigger.onTrue(innerCommand);
|
|
|
|
|
co.park();
|
|
|
|
|
})
|
|
|
|
|
.named("Outer");
|
|
|
|
|
|
|
|
|
|
m_scheduler.schedule(outerCommand);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(m_scheduler.isRunning(outerCommand));
|
|
|
|
|
|
|
|
|
|
triggerSignal.set(true);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertTrue(commandRan.get());
|
|
|
|
|
|
|
|
|
|
// Cancel outer command, trigger should now be out of scope
|
|
|
|
|
m_scheduler.cancel(outerCommand);
|
|
|
|
|
m_scheduler.run();
|
|
|
|
|
assertFalse(m_scheduler.isRunning(outerCommand));
|
|
|
|
|
|
|
|
|
|
// The trigger should have unbound itself during the last run() call.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private BooleanSupplier flickering(AtomicBoolean signal) {
|
|
|
|
|
return () -> {
|
|
|
|
|
boolean val = signal.get();
|
|
|
|
|
if (val) {
|
|
|
|
|
signal.set(false);
|
|
|
|
|
}
|
|
|
|
|
return val;
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-10-10 16:47:22 -04:00
|
|
|
}
|