mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-22 01:11:42 +00:00
[commands] Add Commands v3 framework (#6518)
The framework fundamentally relies on the continuation API added in Java 21 (which is currently internal to the JDK). Continuations allow for call stacks to be saved to the heap and resumed later. The async framework allows command bodies to be written in an imperative style. However, an async command will need to be actively cooperative and periodically call coroutine.yield() in loops to yield control back to the command scheduler to let it process other commands. There are also some other additions like priority levels (as opposed to a blanket yes/no for ignoring incoming commands), factories requiring names be provided for commands, and the scheduler tracking all running commands and not just the highest-level groups. However, those changes aren't unique to an async framework, and could just as easily be used in a traditional command framework.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import edu.wpi.first.wpilibj.RobotController;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
class CommandTestBase {
|
||||
protected Scheduler m_scheduler;
|
||||
protected List<SchedulerEvent> m_events;
|
||||
|
||||
@BeforeEach
|
||||
void initScheduler() {
|
||||
RobotController.setTimeSource(() -> System.nanoTime() / 1000L);
|
||||
m_scheduler = Scheduler.createIndependentScheduler();
|
||||
m_events = new ArrayList<>();
|
||||
m_scheduler.addEventListener(m_events::add);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.wpilib.commands3.ConflictDetector.findAllConflicts;
|
||||
import static org.wpilib.commands3.ConflictDetector.throwIfConflicts;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.wpilib.commands3.ConflictDetector.Conflict;
|
||||
|
||||
class ConflictDetectorTest extends CommandTestBase {
|
||||
@Test
|
||||
void emptyInputHasNoConflicts() {
|
||||
var conflicts = findAllConflicts(List.of());
|
||||
assertEquals(0, conflicts.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleInputHasNoConflicts() {
|
||||
var mech = new Mechanism("Mech", m_scheduler);
|
||||
var command = Command.requiring(mech).executing(Coroutine::park).named("Command");
|
||||
var conflicts = findAllConflicts(List.of(command));
|
||||
assertEquals(0, conflicts.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandDoesNotConflictWithSelf() {
|
||||
var mech = new Mechanism("Mech", m_scheduler);
|
||||
var command = Command.requiring(mech).executing(Coroutine::park).named("Command");
|
||||
var conflicts = findAllConflicts(List.of(command, command));
|
||||
assertEquals(0, conflicts.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectManyConflicts() {
|
||||
var mech1 = new Mechanism("Mech 1", m_scheduler);
|
||||
var mech2 = new Mechanism("Mech 2", m_scheduler);
|
||||
|
||||
var command1 = Command.requiring(mech1, mech2).executing(Coroutine::park).named("Command1");
|
||||
var command2 = Command.requiring(mech1).executing(Coroutine::park).named("Command2");
|
||||
var command3 = Command.requiring(mech2).executing(Coroutine::park).named("Command3");
|
||||
var command4 = Command.requiring(mech2, mech1).executing(Coroutine::park).named("Command4");
|
||||
var allCommands = List.of(command1, command2, command3, command4);
|
||||
|
||||
var conflicts = findAllConflicts(allCommands);
|
||||
assertEquals(5, conflicts.size(), "Five conflicting pairs should have been found");
|
||||
assertEquals(new Conflict(command1, command2, Set.of(mech1)), conflicts.get(0));
|
||||
assertEquals(new Conflict(command1, command3, Set.of(mech2)), conflicts.get(1));
|
||||
assertEquals(new Conflict(command1, command4, Set.of(mech1, mech2)), conflicts.get(2));
|
||||
assertEquals(new Conflict(command2, command4, Set.of(mech1)), conflicts.get(3));
|
||||
assertEquals(new Conflict(command3, command4, Set.of(mech2)), conflicts.get(4));
|
||||
|
||||
// error messaging
|
||||
var error = assertThrows(IllegalArgumentException.class, () -> throwIfConflicts(allCommands));
|
||||
assertEquals(
|
||||
"Commands running in parallel cannot share requirements: "
|
||||
+ "Command1 and Command2 both require Mech 1; "
|
||||
+ "Command1 and Command3 both require Mech 2; "
|
||||
+ "Command1 and Command4 both require Mech 1, Mech 2; "
|
||||
+ "Command2 and Command4 both require Mech 1; "
|
||||
+ "Command3 and Command4 both require Mech 2",
|
||||
error.getMessage());
|
||||
}
|
||||
}
|
||||
174
commandsv3/src/test/java/org/wpilib/commands3/CoroutineTest.java
Normal file
174
commandsv3/src/test/java/org/wpilib/commands3/CoroutineTest.java
Normal file
@@ -0,0 +1,174 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class CoroutineTest extends CommandTestBase {
|
||||
@Test
|
||||
void forkMany() {
|
||||
var a = new NullCommand();
|
||||
var b = new NullCommand();
|
||||
var c = new NullCommand();
|
||||
|
||||
var all =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.fork(a, b, c);
|
||||
co.park();
|
||||
})
|
||||
.named("Fork Many");
|
||||
|
||||
m_scheduler.schedule(all);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(a));
|
||||
assertTrue(m_scheduler.isRunning(b));
|
||||
assertTrue(m_scheduler.isRunning(c));
|
||||
}
|
||||
|
||||
@Test
|
||||
void yieldInSynchronizedBlock() {
|
||||
Object mutex = new Object();
|
||||
AtomicInteger i = new AtomicInteger(0);
|
||||
|
||||
var yieldInSynchronized =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
while (true) {
|
||||
synchronized (mutex) {
|
||||
i.incrementAndGet();
|
||||
co.yield();
|
||||
}
|
||||
}
|
||||
})
|
||||
.named("Yield In Synchronized Block");
|
||||
|
||||
m_scheduler.schedule(yieldInSynchronized);
|
||||
|
||||
var error = assertThrows(IllegalStateException.class, m_scheduler::run);
|
||||
assertEquals(
|
||||
"Coroutine.yield() cannot be called inside a synchronized block or method. "
|
||||
+ "Consider using a Lock instead of synchronized, "
|
||||
+ "or rewrite your code to avoid locks and mutexes altogether.",
|
||||
error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void yieldInLockBody() {
|
||||
Lock lock = new ReentrantLock();
|
||||
AtomicInteger i = new AtomicInteger(0);
|
||||
|
||||
var yieldInLock =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
while (true) {
|
||||
lock.lock();
|
||||
try {
|
||||
i.incrementAndGet();
|
||||
co.yield();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
})
|
||||
.named("Increment In Lock Block");
|
||||
|
||||
m_scheduler.schedule(yieldInLock);
|
||||
m_scheduler.run();
|
||||
assertEquals(1, i.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void coroutineEscapingCommand() {
|
||||
AtomicReference<Runnable> escapeeCallback = new AtomicReference<>();
|
||||
|
||||
var badCommand =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
escapeeCallback.set(co::yield);
|
||||
})
|
||||
.named("Bad Command");
|
||||
|
||||
m_scheduler.schedule(badCommand);
|
||||
m_scheduler.run();
|
||||
|
||||
var error = assertThrows(IllegalStateException.class, escapeeCallback.get()::run);
|
||||
assertEquals("Coroutines can only be used by the command bound to them", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void usingParentCoroutineInChildThrows() {
|
||||
var parent =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
parentCoroutine -> {
|
||||
parentCoroutine.await(
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
childCoroutine -> {
|
||||
parentCoroutine.yield();
|
||||
})
|
||||
.named("Child"));
|
||||
})
|
||||
.named("Parent");
|
||||
|
||||
m_scheduler.schedule(parent);
|
||||
var error = assertThrows(IllegalStateException.class, m_scheduler::run);
|
||||
assertEquals("Coroutines can only be used by the command bound to them", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void awaitAnyCleansUp() {
|
||||
AtomicBoolean firstRan = new AtomicBoolean(false);
|
||||
AtomicBoolean secondRan = new AtomicBoolean(false);
|
||||
AtomicBoolean ranAfterAwait = new AtomicBoolean(false);
|
||||
|
||||
var firstInner = Command.noRequirements().executing(c2 -> firstRan.set(true)).named("First");
|
||||
var secondInner =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
c2 -> {
|
||||
secondRan.set(true);
|
||||
c2.park();
|
||||
})
|
||||
.named("Second");
|
||||
|
||||
var outer =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.awaitAny(firstInner, secondInner);
|
||||
|
||||
ranAfterAwait.set(true);
|
||||
co.park(); // prevent exiting
|
||||
})
|
||||
.named("Command");
|
||||
|
||||
m_scheduler.schedule(outer);
|
||||
m_scheduler.run();
|
||||
|
||||
// Everything should have run...
|
||||
assertTrue(firstRan.get());
|
||||
assertTrue(secondRan.get());
|
||||
assertTrue(ranAfterAwait.get());
|
||||
|
||||
// But only the outer command should still be running; secondInner should have been canceled
|
||||
assertEquals(Set.of(outer), m_scheduler.getRunningCommands());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
class NullCommand implements Command {
|
||||
@Override
|
||||
public void run(Coroutine coroutine) {
|
||||
coroutine.park();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "Null Command";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Mechanism> requirements() {
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
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.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ParallelGroupTest extends CommandTestBase {
|
||||
@Test
|
||||
void parallelAll() {
|
||||
var r1 = new Mechanism("R1", m_scheduler);
|
||||
var r2 = new Mechanism("R2", m_scheduler);
|
||||
|
||||
var c1Count = new AtomicInteger(0);
|
||||
var c2Count = new AtomicInteger(0);
|
||||
|
||||
var c1 =
|
||||
r1.run(
|
||||
coroutine -> {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
coroutine.yield();
|
||||
c1Count.incrementAndGet();
|
||||
}
|
||||
})
|
||||
.named("C1");
|
||||
var c2 =
|
||||
r2.run(
|
||||
coroutine -> {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
coroutine.yield();
|
||||
c2Count.incrementAndGet();
|
||||
}
|
||||
})
|
||||
.named("C2");
|
||||
|
||||
var parallel = new ParallelGroup("Parallel", List.of(c1, c2), List.of());
|
||||
m_scheduler.schedule(parallel);
|
||||
|
||||
// First call to run() should schedule and start the commands
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(parallel));
|
||||
assertTrue(m_scheduler.isRunning(c1));
|
||||
assertTrue(m_scheduler.isRunning(c2));
|
||||
|
||||
// Next call to run() should start them
|
||||
for (int i = 1; i < 5; i++) {
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(c1));
|
||||
assertTrue(m_scheduler.isRunning(c2));
|
||||
assertEquals(i, c1Count.get());
|
||||
assertEquals(i, c2Count.get());
|
||||
}
|
||||
// c1 should finish after 5 iterations; c2 should continue for another 5
|
||||
for (int i = 5; i < 10; i++) {
|
||||
m_scheduler.run();
|
||||
assertFalse(m_scheduler.isRunning(c1));
|
||||
assertTrue(m_scheduler.isRunning(c2));
|
||||
assertEquals(5, c1Count.get());
|
||||
assertEquals(i, c2Count.get());
|
||||
}
|
||||
|
||||
// one final run() should unschedule the c2 command and end the group
|
||||
assertTrue(m_scheduler.isRunning(parallel));
|
||||
m_scheduler.run();
|
||||
assertFalse(m_scheduler.isRunning(c1));
|
||||
assertFalse(m_scheduler.isRunning(c2));
|
||||
assertFalse(m_scheduler.isRunning(parallel));
|
||||
|
||||
// and final counts should be 5 and 10
|
||||
assertEquals(5, c1Count.get());
|
||||
assertEquals(10, c2Count.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void race() {
|
||||
var r1 = new Mechanism("R1", m_scheduler);
|
||||
var r2 = new Mechanism("R2", m_scheduler);
|
||||
|
||||
var c1Count = new AtomicInteger(0);
|
||||
var c2Count = new AtomicInteger(0);
|
||||
|
||||
var c1 =
|
||||
r1.run(
|
||||
coroutine -> {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
coroutine.yield();
|
||||
c1Count.incrementAndGet();
|
||||
}
|
||||
})
|
||||
.named("C1");
|
||||
var c2 =
|
||||
r2.run(
|
||||
coroutine -> {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
coroutine.yield();
|
||||
c2Count.incrementAndGet();
|
||||
}
|
||||
})
|
||||
.named("C2");
|
||||
|
||||
var race = new ParallelGroup("Race", List.of(), List.of(c1, c2));
|
||||
m_scheduler.schedule(race);
|
||||
|
||||
// First call to run() should schedule the commands
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(race));
|
||||
assertTrue(m_scheduler.isRunning(c1));
|
||||
assertTrue(m_scheduler.isRunning(c2));
|
||||
|
||||
for (int i = 1; i < 5; i++) {
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(c1));
|
||||
assertTrue(m_scheduler.isRunning(c2));
|
||||
assertEquals(i, c1Count.get());
|
||||
assertEquals(i, c2Count.get());
|
||||
}
|
||||
m_scheduler.run(); // complete c1
|
||||
assertFalse(m_scheduler.isRunning(race));
|
||||
assertFalse(m_scheduler.isRunning(c1));
|
||||
assertFalse(m_scheduler.isRunning(c2));
|
||||
|
||||
// and final counts should be 5 and 5
|
||||
assertEquals(5, c1Count.get());
|
||||
assertEquals(5, c2Count.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nested() {
|
||||
var mechanism = new Mechanism("mechanism", m_scheduler);
|
||||
|
||||
var count = new AtomicInteger(0);
|
||||
|
||||
var command =
|
||||
mechanism
|
||||
.run(
|
||||
coroutine -> {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
coroutine.yield();
|
||||
count.incrementAndGet();
|
||||
}
|
||||
})
|
||||
.named("Command");
|
||||
|
||||
var inner = new ParallelGroup("Inner", Set.of(command), Set.of());
|
||||
var outer = new ParallelGroup("Outer", Set.of(), Set.of(inner));
|
||||
|
||||
// Scheduling: Outer group should be on deck
|
||||
m_scheduler.schedule(outer);
|
||||
assertTrue(m_scheduler.isScheduled(outer));
|
||||
assertFalse(m_scheduler.isScheduledOrRunning(inner));
|
||||
assertFalse(m_scheduler.isScheduledOrRunning(command));
|
||||
|
||||
// First run: Inner group and command should both be scheduled and running
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(outer), "Outer group should be running");
|
||||
assertTrue(m_scheduler.isRunning(inner), "Inner group should be running");
|
||||
assertTrue(m_scheduler.isRunning(command), "Command should be running");
|
||||
assertEquals(0, count.get());
|
||||
|
||||
// Runs 2 through 5: Outer and inner should both be running while the command runs
|
||||
for (int i = 1; i < 5; i++) {
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(outer), "Outer group should be running");
|
||||
assertTrue(m_scheduler.isRunning(inner), "Inner group should be running");
|
||||
assertTrue(m_scheduler.isRunning(command), "Command should be running (" + i + ")");
|
||||
assertEquals(i, count.get());
|
||||
}
|
||||
|
||||
// Run 6: Command should have completed naturally
|
||||
m_scheduler.run();
|
||||
assertFalse(m_scheduler.isRunning(outer), "Outer group should be running");
|
||||
assertFalse(m_scheduler.isRunning(inner), "Inner group should be running");
|
||||
assertFalse(m_scheduler.isRunning(command), "Command should have completed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void automaticNameRace() {
|
||||
var a = Command.noRequirements().executing(coroutine -> {}).named("A");
|
||||
var b = Command.noRequirements().executing(coroutine -> {}).named("B");
|
||||
var c = Command.noRequirements().executing(coroutine -> {}).named("C");
|
||||
|
||||
var group = new ParallelGroupBuilder().optional(a, b, c).withAutomaticName();
|
||||
assertEquals("(A | B | C)", group.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void automaticNameAll() {
|
||||
var a = Command.noRequirements().executing(coroutine -> {}).named("A");
|
||||
var b = Command.noRequirements().executing(coroutine -> {}).named("B");
|
||||
var c = Command.noRequirements().executing(coroutine -> {}).named("C");
|
||||
|
||||
var group = new ParallelGroupBuilder().requiring(a, b, c).withAutomaticName();
|
||||
assertEquals("(A & B & C)", group.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void automaticNameDeadline() {
|
||||
var a = Command.noRequirements().executing(coroutine -> {}).named("A");
|
||||
var b = Command.noRequirements().executing(coroutine -> {}).named("B");
|
||||
var c = Command.noRequirements().executing(coroutine -> {}).named("C");
|
||||
|
||||
var group = new ParallelGroupBuilder().requiring(a).optional(b, c).withAutomaticName();
|
||||
assertEquals("[(A) * (B | C)]", group.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritsRequirements() {
|
||||
var mech1 = new Mechanism("Mech 1", m_scheduler);
|
||||
var mech2 = new Mechanism("Mech 2", m_scheduler);
|
||||
var command1 = mech1.run(Coroutine::park).named("Command 1");
|
||||
var command2 = mech2.run(Coroutine::park).named("Command 2");
|
||||
var group = new ParallelGroup("Group", Set.of(command1, command2), Set.of());
|
||||
assertEquals(Set.of(mech1, mech2), group.requirements(), "Requirements were not inherited");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritsPriority() {
|
||||
var mech1 = new Mechanism("Mech 1", m_scheduler);
|
||||
var mech2 = new Mechanism("Mech 2", m_scheduler);
|
||||
var command1 = mech1.run(Coroutine::park).withPriority(100).named("Command 1");
|
||||
var command2 = mech2.run(Coroutine::park).withPriority(200).named("Command 2");
|
||||
var group = new ParallelGroup("Group", Set.of(command1, command2), Set.of());
|
||||
assertEquals(200, group.priority(), "Priority was not inherited");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
record PriorityCommand(int priority, Mechanism... subsystems) implements Command {
|
||||
@Override
|
||||
public void run(Coroutine coroutine) {
|
||||
coroutine.park();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Mechanism> requirements() {
|
||||
return Set.of(subsystems);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PriorityCommand[priority=" + priority + ", subsystems=" + Set.of(subsystems) + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SchedulerCancellationTests extends CommandTestBase {
|
||||
@Test
|
||||
void cancelOnInterruptDoesNotResume() {
|
||||
var count = new AtomicInteger(0);
|
||||
|
||||
var mechanism = new Mechanism("mechanism", m_scheduler);
|
||||
|
||||
var interrupter =
|
||||
Command.requiring(mechanism)
|
||||
.executing(coroutine -> {})
|
||||
.withPriority(2)
|
||||
.named("Interrupter");
|
||||
|
||||
var canceledCommand =
|
||||
Command.requiring(mechanism)
|
||||
.executing(
|
||||
coroutine -> {
|
||||
count.set(1);
|
||||
coroutine.yield();
|
||||
count.set(2);
|
||||
})
|
||||
.withPriority(1)
|
||||
.named("Cancel By Default");
|
||||
|
||||
m_scheduler.schedule(canceledCommand);
|
||||
m_scheduler.run();
|
||||
|
||||
m_scheduler.schedule(interrupter);
|
||||
m_scheduler.run();
|
||||
assertEquals(1, count.get()); // the second "set" call should not have run
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultCommandResumesAfterInterruption() {
|
||||
var count = new AtomicInteger(0);
|
||||
|
||||
var mechanism = new Mechanism("mechanism", m_scheduler);
|
||||
var defaultCmd =
|
||||
mechanism
|
||||
.run(
|
||||
coroutine -> {
|
||||
while (true) {
|
||||
count.incrementAndGet();
|
||||
coroutine.yield();
|
||||
}
|
||||
})
|
||||
.withPriority(-1)
|
||||
.named("Default Command");
|
||||
|
||||
final var newerCmd = mechanism.run(coroutine -> {}).named("Newer Command");
|
||||
mechanism.setDefaultCommand(defaultCmd);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(defaultCmd), "Default command should be running");
|
||||
|
||||
m_scheduler.schedule(newerCmd);
|
||||
m_scheduler.run();
|
||||
assertFalse(m_scheduler.isRunning(defaultCmd), "Default command should have been interrupted");
|
||||
assertEquals(1, count.get(), "Default command should have run once");
|
||||
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(defaultCmd), "Default command should have resumed");
|
||||
assertEquals(2, count.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelsEvictsOnDeck() {
|
||||
var command = Command.noRequirements().executing(Coroutine::park).named("Command");
|
||||
m_scheduler.schedule(command);
|
||||
m_scheduler.cancel(command);
|
||||
assertFalse(m_scheduler.isScheduledOrRunning(command));
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandCancelingSelf() {
|
||||
var ranAfterCancel = new AtomicBoolean(false);
|
||||
var commandRef = new AtomicReference<Command>(null);
|
||||
var command =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.scheduler().cancel(commandRef.get());
|
||||
ranAfterCancel.set(true);
|
||||
})
|
||||
.named("Command");
|
||||
commandRef.set(command);
|
||||
m_scheduler.schedule(command);
|
||||
|
||||
var error = assertThrows(IllegalArgumentException.class, () -> m_scheduler.run());
|
||||
assertEquals("Command `Command` is mounted and cannot be canceled", error.getMessage());
|
||||
assertFalse(ranAfterCancel.get(), "Command should have stopped after encountering an error");
|
||||
assertFalse(
|
||||
m_scheduler.isScheduledOrRunning(command),
|
||||
"Command should have been removed from the scheduler");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelAllEvictsOnDeck() {
|
||||
var command = Command.noRequirements().executing(Coroutine::park).named("Command");
|
||||
m_scheduler.schedule(command);
|
||||
m_scheduler.cancelAll();
|
||||
assertFalse(m_scheduler.isScheduledOrRunning(command));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelAllCancelsAll() {
|
||||
var commands = new ArrayList<Command>(10);
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
commands.add(Command.noRequirements().executing(Coroutine::yield).named("Command " + i));
|
||||
}
|
||||
commands.forEach(m_scheduler::schedule);
|
||||
m_scheduler.run();
|
||||
m_scheduler.cancelAll();
|
||||
for (Command command : commands) {
|
||||
if (m_scheduler.isRunning(command)) {
|
||||
fail(command.name() + " was not canceled by cancelAll()");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelAllCallsOnCancelHookForRunningCommands() {
|
||||
AtomicBoolean ranHook = new AtomicBoolean(false);
|
||||
var command =
|
||||
Command.noRequirements()
|
||||
.executing(Coroutine::park)
|
||||
.whenCanceled(() -> ranHook.set(true))
|
||||
.named("Command");
|
||||
m_scheduler.schedule(command);
|
||||
m_scheduler.run();
|
||||
m_scheduler.cancelAll();
|
||||
assertTrue(ranHook.get(), "onCancel hook was not called");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelAllDoesNotCallOnCancelHookForQueuedCommands() {
|
||||
AtomicBoolean ranHook = new AtomicBoolean(false);
|
||||
var command =
|
||||
Command.noRequirements()
|
||||
.executing(Coroutine::park)
|
||||
.whenCanceled(() -> ranHook.set(true))
|
||||
.named("Command");
|
||||
m_scheduler.schedule(command);
|
||||
// no call to run before cancelAll()
|
||||
m_scheduler.cancelAll();
|
||||
assertFalse(ranHook.get(), "onCancel hook was not called");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelAllStartsDefaults() {
|
||||
var mechanisms = new ArrayList<Mechanism>(10);
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
mechanisms.add(new Mechanism("System " + i, m_scheduler));
|
||||
}
|
||||
|
||||
var command = Command.requiring(mechanisms).executing(Coroutine::yield).named("Big Command");
|
||||
|
||||
// Scheduling the command should evict the on-deck default commands
|
||||
m_scheduler.schedule(command);
|
||||
|
||||
// Then running should get it into the set of running commands
|
||||
m_scheduler.run();
|
||||
|
||||
// Canceling should clear out the set of running commands
|
||||
m_scheduler.cancelAll();
|
||||
|
||||
// Then ticking the scheduler once to fully remove the command and schedule the defaults
|
||||
m_scheduler.run();
|
||||
|
||||
assertFalse(m_scheduler.isRunning(command), "Command was not canceled by cancelAll()");
|
||||
|
||||
for (var mechanism : mechanisms) {
|
||||
var runningCommands = m_scheduler.getRunningCommandsFor(mechanism);
|
||||
assertEquals(
|
||||
1,
|
||||
runningCommands.size(),
|
||||
"mechanism " + mechanism + " should have exactly one running command");
|
||||
assertEquals(
|
||||
mechanism.getDefaultCommand(),
|
||||
runningCommands.getFirst(),
|
||||
"mechanism " + mechanism + " is not running the default command");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelDeeplyNestedCompositions() {
|
||||
Command root =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.await(
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co2 -> {
|
||||
co2.await(
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co3 -> {
|
||||
co3.await(
|
||||
Command.noRequirements()
|
||||
.executing(Coroutine::park)
|
||||
.named("Park"));
|
||||
})
|
||||
.named("C3"));
|
||||
})
|
||||
.named("C2"));
|
||||
})
|
||||
.named("Root");
|
||||
|
||||
m_scheduler.schedule(root);
|
||||
|
||||
m_scheduler.run();
|
||||
assertEquals(4, m_scheduler.getRunningCommands().size());
|
||||
|
||||
m_scheduler.cancel(root);
|
||||
assertEquals(0, m_scheduler.getRunningCommands().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compositionsDoNotSelfCancel() {
|
||||
var mech = new Mechanism("The mechanism", m_scheduler);
|
||||
var group =
|
||||
mech.run(
|
||||
co -> {
|
||||
co.await(
|
||||
mech.run(
|
||||
co2 -> {
|
||||
co2.await(
|
||||
mech.run(
|
||||
co3 -> {
|
||||
co3.await(mech.run(Coroutine::park).named("Park"));
|
||||
})
|
||||
.named("C3"));
|
||||
})
|
||||
.named("C2"));
|
||||
})
|
||||
.named("Group");
|
||||
|
||||
m_scheduler.schedule(group);
|
||||
m_scheduler.run();
|
||||
assertEquals(4, m_scheduler.getRunningCommands().size());
|
||||
assertTrue(m_scheduler.isRunning(group));
|
||||
}
|
||||
|
||||
@Test
|
||||
void compositionsDoNotCancelParent() {
|
||||
var mech = new Mechanism("The mechanism", m_scheduler);
|
||||
var group =
|
||||
mech.run(
|
||||
co -> {
|
||||
co.fork(mech.run(Coroutine::park).named("First Child"));
|
||||
co.fork(mech.run(Coroutine::park).named("Second Child"));
|
||||
co.park();
|
||||
})
|
||||
.named("Group");
|
||||
|
||||
m_scheduler.schedule(group);
|
||||
m_scheduler.run();
|
||||
|
||||
// second child interrupts first child
|
||||
assertEquals(
|
||||
List.of("Group", "Second Child"),
|
||||
m_scheduler.getRunningCommands().stream().map(Command::name).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotRunOnCancelWhenInterruptingOnDeck() {
|
||||
var ran = new AtomicBoolean(false);
|
||||
|
||||
var mechanism = new Mechanism("The mechanism", m_scheduler);
|
||||
var cmd = mechanism.run(Coroutine::yield).whenCanceled(() -> ran.set(true)).named("cmd");
|
||||
var interrupter = mechanism.run(Coroutine::yield).named("Interrupter");
|
||||
m_scheduler.schedule(cmd);
|
||||
m_scheduler.schedule(interrupter);
|
||||
m_scheduler.run();
|
||||
|
||||
assertFalse(ran.get(), "onCancel ran when it shouldn't have!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotRunOnCancelWhenCancelingOnDeck() {
|
||||
var ran = new AtomicBoolean(false);
|
||||
|
||||
var mechanism = new Mechanism("The mechanism", m_scheduler);
|
||||
var cmd = mechanism.run(Coroutine::yield).whenCanceled(() -> ran.set(true)).named("cmd");
|
||||
m_scheduler.schedule(cmd);
|
||||
// canceling before calling .run()
|
||||
m_scheduler.cancel(cmd);
|
||||
m_scheduler.run();
|
||||
|
||||
assertFalse(ran.get(), "onCancel ran when it shouldn't have!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void runsOnCancelWhenInterruptingCommand() {
|
||||
var ran = new AtomicBoolean(false);
|
||||
|
||||
var mechanism = new Mechanism("The mechanism", m_scheduler);
|
||||
var cmd = mechanism.run(Coroutine::park).whenCanceled(() -> ran.set(true)).named("cmd");
|
||||
var interrupter = mechanism.run(Coroutine::park).named("Interrupter");
|
||||
m_scheduler.schedule(cmd);
|
||||
m_scheduler.run();
|
||||
m_scheduler.schedule(interrupter);
|
||||
m_scheduler.run();
|
||||
|
||||
assertTrue(ran.get(), "onCancel should have run!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotRunOnCancelWhenCompleting() {
|
||||
var ran = new AtomicBoolean(false);
|
||||
|
||||
var mechanism = new Mechanism("The mechanism", m_scheduler);
|
||||
var cmd = mechanism.run(Coroutine::yield).whenCanceled(() -> ran.set(true)).named("cmd");
|
||||
m_scheduler.schedule(cmd);
|
||||
m_scheduler.run();
|
||||
m_scheduler.run();
|
||||
|
||||
assertFalse(m_scheduler.isScheduledOrRunning(cmd));
|
||||
assertFalse(ran.get(), "onCancel ran when it shouldn't have!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void runsOnCancelWhenCanceling() {
|
||||
var ran = new AtomicBoolean(false);
|
||||
|
||||
var mechanism = new Mechanism("The mechanism", m_scheduler);
|
||||
var cmd = mechanism.run(Coroutine::yield).whenCanceled(() -> ran.set(true)).named("cmd");
|
||||
m_scheduler.schedule(cmd);
|
||||
m_scheduler.run();
|
||||
m_scheduler.cancel(cmd);
|
||||
|
||||
assertTrue(ran.get(), "onCancel should have run!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void runsOnCancelWhenCancelingParent() {
|
||||
var ran = new AtomicBoolean(false);
|
||||
|
||||
var mechanism = new Mechanism("The mechanism", m_scheduler);
|
||||
var cmd = mechanism.run(Coroutine::yield).whenCanceled(() -> ran.set(true)).named("cmd");
|
||||
|
||||
var group = new SequentialGroup("Seq", Collections.singletonList(cmd));
|
||||
m_scheduler.schedule(group);
|
||||
m_scheduler.run();
|
||||
m_scheduler.cancel(group);
|
||||
|
||||
assertTrue(ran.get(), "onCancel should have run!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SchedulerConflictTests extends CommandTestBase {
|
||||
@Test
|
||||
void compositionsCannotAwaitConflictingCommands() {
|
||||
var mech = new Mechanism("The Mechanism", m_scheduler);
|
||||
|
||||
var group =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.awaitAll(
|
||||
mech.run(Coroutine::park).named("First"),
|
||||
mech.run(Coroutine::park).named("Second"));
|
||||
})
|
||||
.named("Group");
|
||||
|
||||
m_scheduler.schedule(group);
|
||||
|
||||
// Running should attempt to schedule multiple conflicting commands
|
||||
var exception = assertThrows(IllegalArgumentException.class, m_scheduler::run);
|
||||
assertEquals(
|
||||
"Commands running in parallel cannot share requirements: "
|
||||
+ "First and Second both require The Mechanism",
|
||||
exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void innerCommandMayInterruptOtherInnerCommand() {
|
||||
var mechanism = new Mechanism("The mechanism", m_scheduler);
|
||||
var firstRan = new AtomicBoolean(false);
|
||||
var secondRan = new AtomicBoolean(false);
|
||||
|
||||
var first =
|
||||
mechanism
|
||||
.run(
|
||||
c -> {
|
||||
firstRan.set(true);
|
||||
c.park();
|
||||
})
|
||||
.named("First");
|
||||
|
||||
var second =
|
||||
mechanism
|
||||
.run(
|
||||
c -> {
|
||||
secondRan.set(true);
|
||||
c.park();
|
||||
})
|
||||
.named("Second");
|
||||
|
||||
var group =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.fork(first);
|
||||
co.fork(second);
|
||||
co.park();
|
||||
})
|
||||
.named("Group");
|
||||
|
||||
m_scheduler.schedule(group);
|
||||
m_scheduler.run();
|
||||
|
||||
assertTrue(firstRan.get(), "First child should have run to a yield point");
|
||||
assertTrue(secondRan.get(), "Second child should have run to a yield point");
|
||||
assertFalse(
|
||||
m_scheduler.isScheduledOrRunning(first), "First child should have been interrupted");
|
||||
assertTrue(m_scheduler.isRunning(second), "Second child should still be running");
|
||||
assertTrue(m_scheduler.isRunning(group), "Group should still be running");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedOneShotCompositionsAllRunInOneCycle() {
|
||||
var runs = new AtomicInteger(0);
|
||||
Supplier<Command> makeOneShot =
|
||||
() -> Command.noRequirements().executing(_c -> runs.incrementAndGet()).named("One Shot");
|
||||
var command =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.fork(makeOneShot.get());
|
||||
co.fork(makeOneShot.get());
|
||||
co.fork(
|
||||
Command.noRequirements()
|
||||
.executing(inner -> inner.fork(makeOneShot.get()))
|
||||
.named("Inner"));
|
||||
co.fork(
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co2 -> {
|
||||
co2.fork(makeOneShot.get());
|
||||
co2.fork(
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co3 -> {
|
||||
co3.fork(makeOneShot.get());
|
||||
})
|
||||
.named("3"));
|
||||
})
|
||||
.named("2"));
|
||||
})
|
||||
.named("Command");
|
||||
|
||||
m_scheduler.schedule(command);
|
||||
m_scheduler.run();
|
||||
assertEquals(5, runs.get(), "All oneshot commands should have run");
|
||||
assertFalse(m_scheduler.isRunning(command), "Command should have exited after one cycle");
|
||||
}
|
||||
|
||||
@Test
|
||||
void childConflictsWithHigherPriorityTopLevel() {
|
||||
var mechanism = new Mechanism("mechanism", m_scheduler);
|
||||
var top = mechanism.run(Coroutine::park).withPriority(10).named("Top");
|
||||
|
||||
// Child conflicts with and is lower priority than the Top command
|
||||
// It should not be scheduled, and the parent command should exit immediately
|
||||
var child = mechanism.run(Coroutine::park).named("Child");
|
||||
var parent = Command.noRequirements().executing(co -> co.await(child)).named("Parent");
|
||||
|
||||
m_scheduler.schedule(top);
|
||||
m_scheduler.schedule(parent);
|
||||
m_scheduler.run();
|
||||
|
||||
assertTrue(m_scheduler.isRunning(top), "Top command should not have been interrupted");
|
||||
assertFalse(m_scheduler.isRunning(child), "Conflicting child should not have run");
|
||||
assertFalse(m_scheduler.isRunning(parent), "Parent of conflicting child should have exited");
|
||||
}
|
||||
|
||||
@Test
|
||||
void childConflictsWithLowerPriorityTopLevel() {
|
||||
var mechanism = new Mechanism("mechanism", m_scheduler);
|
||||
var top = mechanism.run(Coroutine::park).withPriority(-10).named("Top");
|
||||
|
||||
// Child conflicts with and is higher priority than the Top command
|
||||
// It should be scheduled, and the top command should be interrupted
|
||||
var child = mechanism.run(Coroutine::park).named("Child");
|
||||
var parent = Command.noRequirements().executing(co -> co.await(child)).named("Parent");
|
||||
|
||||
m_scheduler.schedule(top);
|
||||
m_scheduler.schedule(parent);
|
||||
m_scheduler.run();
|
||||
|
||||
assertFalse(m_scheduler.isRunning(top), "Top command should have been interrupted");
|
||||
assertTrue(m_scheduler.isRunning(child), "Conflicting child should be running");
|
||||
assertTrue(m_scheduler.isRunning(parent), "Parent of conflicting child should be running");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SchedulerErrorHandlingTests extends CommandTestBase {
|
||||
@Test
|
||||
void errorDetection() {
|
||||
var mechanism = new Mechanism("X", m_scheduler);
|
||||
|
||||
var command =
|
||||
mechanism
|
||||
.run(
|
||||
coroutine -> {
|
||||
throw new RuntimeException("The exception");
|
||||
})
|
||||
.named("Bad Behavior");
|
||||
|
||||
new Trigger(m_scheduler, () -> true).onTrue(command);
|
||||
|
||||
var e = assertThrows(RuntimeException.class, m_scheduler::run);
|
||||
assertEquals("The exception", e.getMessage());
|
||||
|
||||
assertEquals(
|
||||
"org.wpilib.commands3.SchedulerErrorHandlingTests", e.getStackTrace()[0].getClassName());
|
||||
assertEquals("lambda$errorDetection$0", e.getStackTrace()[0].getMethodName());
|
||||
|
||||
assertEquals("=== Command Binding Trace ===", e.getStackTrace()[2].getClassName());
|
||||
|
||||
assertEquals(getClass().getName(), e.getStackTrace()[3].getClassName());
|
||||
assertEquals("errorDetection", e.getStackTrace()[3].getMethodName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedErrorDetection() {
|
||||
var command =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.await(
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
c2 -> {
|
||||
new Trigger(m_scheduler, () -> true)
|
||||
.onTrue(
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
c3 -> {
|
||||
// Throws IndexOutOfBoundsException
|
||||
var unused = new ArrayList<>(0).get(-1);
|
||||
})
|
||||
.named("Throws IndexOutOfBounds"));
|
||||
c2.park();
|
||||
})
|
||||
.named("Schedules With Trigger"));
|
||||
})
|
||||
.named("Schedules Directly");
|
||||
|
||||
m_scheduler.schedule(command);
|
||||
|
||||
// The first run sets up the trigger, but does not fire
|
||||
// The second run will fire the trigger and cause the inner command to run and throw
|
||||
m_scheduler.run();
|
||||
|
||||
var e = assertThrows(IndexOutOfBoundsException.class, m_scheduler::run);
|
||||
StackTraceElement[] stackTrace = e.getStackTrace();
|
||||
|
||||
assertEquals("Index -1 out of bounds for length 0", e.getMessage());
|
||||
int nestedIndex = 0;
|
||||
for (; nestedIndex < stackTrace.length; nestedIndex++) {
|
||||
if (stackTrace[nestedIndex].getClassName().equals(getClass().getName())) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// user code trace for the scheduler run invocation (to `scheduler.run()` in the try block)
|
||||
assertEquals("lambda$nestedErrorDetection$3", stackTrace[nestedIndex].getMethodName());
|
||||
assertEquals("assertThrows", stackTrace[nestedIndex + 1].getMethodName());
|
||||
|
||||
// user code trace for where the command was scheduled (the `.onTrue()` line)
|
||||
assertEquals("=== Command Binding Trace ===", stackTrace[nestedIndex + 2].getClassName());
|
||||
assertEquals("lambda$nestedErrorDetection$4", stackTrace[nestedIndex + 3].getMethodName());
|
||||
assertEquals("lambda$nestedErrorDetection$5", stackTrace[nestedIndex + 4].getMethodName());
|
||||
assertEquals("nestedErrorDetection", stackTrace[nestedIndex + 5].getMethodName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandEncounteringErrorCancelsChildren() {
|
||||
var child = Command.noRequirements().executing(Coroutine::park).named("Child 1");
|
||||
var command =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.fork(child);
|
||||
throw new RuntimeException("The exception");
|
||||
})
|
||||
.named("Bad Behavior");
|
||||
|
||||
m_scheduler.schedule(command);
|
||||
assertThrows(RuntimeException.class, m_scheduler::run);
|
||||
assertFalse(
|
||||
m_scheduler.isScheduledOrRunning(command),
|
||||
"Command should have been removed from the scheduler");
|
||||
assertFalse(
|
||||
m_scheduler.isScheduledOrRunning(child),
|
||||
"Child should have been removed from the scheduler");
|
||||
}
|
||||
|
||||
@Test
|
||||
void childCommandEncounteringErrorCancelsParent() {
|
||||
var child =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
throw new RuntimeException("The exception"); // note: bubbles up to the parent
|
||||
})
|
||||
.named("Child 1");
|
||||
var command =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.await(child);
|
||||
co.park(); // pretend other things would happen after the child
|
||||
})
|
||||
.named("Parent");
|
||||
|
||||
m_scheduler.schedule(command);
|
||||
assertThrows(RuntimeException.class, m_scheduler::run);
|
||||
assertFalse(
|
||||
m_scheduler.isRunning(command),
|
||||
"Parent command should have been removed from the scheduler");
|
||||
assertFalse(m_scheduler.isRunning(child), "Child should have been removed from the scheduler");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("PMD.CompareObjectsWithEquals")
|
||||
void childCommandEncounteringErrorAfterRemountCancelsParent() {
|
||||
var child =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.yield();
|
||||
throw new RuntimeException("The exception"); // does not bubble up to the parent
|
||||
})
|
||||
.named("Child 1");
|
||||
var command =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.await(child);
|
||||
co.park(); // pretend other things would happen after the child
|
||||
})
|
||||
.named("Parent");
|
||||
|
||||
m_scheduler.schedule(command);
|
||||
|
||||
// first run schedules the child and adds it to the running set
|
||||
m_scheduler.run();
|
||||
|
||||
// second run encounters the error in the child
|
||||
final var error = assertThrows(RuntimeException.class, m_scheduler::run);
|
||||
assertFalse(
|
||||
m_scheduler.isRunning(command),
|
||||
"Parent command should have been removed from the scheduler");
|
||||
assertFalse(m_scheduler.isRunning(child), "Child should have been removed from the scheduler");
|
||||
|
||||
// Full event history
|
||||
assertEquals(9, m_events.size());
|
||||
assertTrue(
|
||||
m_events.get(0) instanceof SchedulerEvent.Scheduled s && s.command() == command,
|
||||
"First event should be parent scheduled");
|
||||
assertTrue(
|
||||
m_events.get(1) instanceof SchedulerEvent.Mounted m && m.command() == command,
|
||||
"Second event should be parent mounted");
|
||||
assertTrue(
|
||||
m_events.get(2) instanceof SchedulerEvent.Scheduled s && s.command() == child,
|
||||
"Third event should be child scheduled");
|
||||
assertTrue(
|
||||
m_events.get(3) instanceof SchedulerEvent.Mounted m && m.command() == child,
|
||||
"Fourth event should be child mounted");
|
||||
assertTrue(
|
||||
m_events.get(4) instanceof SchedulerEvent.Yielded y && y.command() == child,
|
||||
"Fifth event should be child yielded");
|
||||
assertTrue(
|
||||
m_events.get(5) instanceof SchedulerEvent.Yielded y && y.command() == command,
|
||||
"Sixth event should be parent yielded");
|
||||
assertTrue(
|
||||
m_events.get(6) instanceof SchedulerEvent.Mounted m && m.command() == child,
|
||||
"Seventh event should be child remounted");
|
||||
assertTrue(
|
||||
m_events.get(7) instanceof SchedulerEvent.CompletedWithError c
|
||||
&& c.command() == child
|
||||
&& c.error() == error,
|
||||
"Eighth event should be child completed with error");
|
||||
assertTrue(
|
||||
m_events.get(8) instanceof SchedulerEvent.Canceled c && c.command() == command,
|
||||
"Ninth event should be parent canceled");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SchedulerPriorityLevelTests extends CommandTestBase {
|
||||
@Test
|
||||
void higherPriorityCancels() {
|
||||
final var subsystem = new Mechanism("Subsystem", m_scheduler);
|
||||
|
||||
final var lower = new PriorityCommand(-1000, subsystem);
|
||||
final var higher = new PriorityCommand(+1000, subsystem);
|
||||
|
||||
m_scheduler.schedule(lower);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(lower));
|
||||
|
||||
m_scheduler.schedule(higher);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(higher));
|
||||
assertFalse(m_scheduler.isRunning(lower));
|
||||
}
|
||||
|
||||
@Test
|
||||
void lowerPriorityDoesNotCancel() {
|
||||
final var subsystem = new Mechanism("Subsystem", m_scheduler);
|
||||
|
||||
final var lower = new PriorityCommand(-1000, subsystem);
|
||||
final var higher = new PriorityCommand(+1000, subsystem);
|
||||
|
||||
m_scheduler.schedule(higher);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(higher));
|
||||
|
||||
m_scheduler.schedule(lower);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(higher), "Higher priority command should still be running");
|
||||
assertFalse(
|
||||
m_scheduler.isScheduledOrRunning(lower), "Lower priority command should not be running");
|
||||
}
|
||||
|
||||
@Test
|
||||
void samePriorityCancels() {
|
||||
final var subsystem = new Mechanism("Subsystem", m_scheduler);
|
||||
|
||||
final var first = new PriorityCommand(512, subsystem);
|
||||
final var second = new PriorityCommand(512, subsystem);
|
||||
|
||||
m_scheduler.schedule(first);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(first));
|
||||
|
||||
m_scheduler.schedule(second);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(second), "New command should be running");
|
||||
assertFalse(m_scheduler.isRunning(first), "Old command should be canceled");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SchedulerSideloadFunctionTests extends CommandTestBase {
|
||||
@Test
|
||||
void sideloadThrowingException() {
|
||||
m_scheduler.sideload(
|
||||
co -> {
|
||||
throw new RuntimeException("Bang!");
|
||||
});
|
||||
|
||||
// An exception raised in a sideload function should bubble up
|
||||
assertEquals(
|
||||
"Bang!", assertThrowsExactly(RuntimeException.class, m_scheduler::run).getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void periodicSideload() {
|
||||
AtomicInteger count = new AtomicInteger(0);
|
||||
m_scheduler.addPeriodic(count::incrementAndGet);
|
||||
assertEquals(0, count.get());
|
||||
|
||||
m_scheduler.run();
|
||||
assertEquals(1, count.get());
|
||||
|
||||
m_scheduler.run();
|
||||
assertEquals(2, count.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sideloadSchedulingCommand() {
|
||||
var command = Command.noRequirements().executing(Coroutine::park).named("Command");
|
||||
// one-shot sideload forks a command and immediately exits
|
||||
m_scheduler.sideload(co -> co.fork(command));
|
||||
m_scheduler.run();
|
||||
assertTrue(
|
||||
m_scheduler.isRunning(command), "command should have started and outlasted the sideload");
|
||||
}
|
||||
|
||||
@Test
|
||||
void childCommandEscapesViaSideload() {
|
||||
var child = Command.noRequirements().executing(Coroutine::park).named("Child");
|
||||
var parent =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
parentCoroutine -> {
|
||||
m_scheduler.sideload(sidelodCoroutine -> sidelodCoroutine.fork(child));
|
||||
})
|
||||
.named("Parent");
|
||||
|
||||
m_scheduler.schedule(parent);
|
||||
m_scheduler.run();
|
||||
assertFalse(m_scheduler.isScheduledOrRunning(parent), "parent should have exited");
|
||||
assertFalse(
|
||||
m_scheduler.isScheduledOrRunning(child),
|
||||
"the sideload to schedule the child should not have run yet");
|
||||
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(child), "child should have started running");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sideloadCancelingCommand() {
|
||||
var command = Command.noRequirements().executing(Coroutine::park).named("Command");
|
||||
m_scheduler.schedule(command);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(command), "command should have started");
|
||||
|
||||
m_scheduler.sideload(co -> m_scheduler.cancel(command));
|
||||
assertTrue(m_scheduler.isRunning(command), "sideload should not have run yet");
|
||||
|
||||
m_scheduler.run();
|
||||
assertFalse(m_scheduler.isRunning(command), "sideload should have canceled the command");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sideloadAffectsStateForTriggerInSameCycle() {
|
||||
AtomicBoolean signal = new AtomicBoolean(false);
|
||||
var trigger = new Trigger(m_scheduler, signal::get);
|
||||
var command = Command.noRequirements().executing(Coroutine::park).named("Command");
|
||||
trigger.onTrue(command);
|
||||
m_scheduler.sideload(co -> signal.set(true));
|
||||
|
||||
m_scheduler.run();
|
||||
assertTrue(signal.get(), "Sideload should have run and set the signal");
|
||||
assertTrue(m_scheduler.isRunning(command), "Command should have started");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SchedulerTelemetryTests extends CommandTestBase {
|
||||
@Test
|
||||
void protobuf() {
|
||||
var mech = new Mechanism("The mechanism", m_scheduler);
|
||||
var parkCommand = mech.run(Coroutine::park).named("Park");
|
||||
var c3Command = mech.run(co -> co.await(parkCommand)).named("C3");
|
||||
var c2Command = mech.run(co -> co.await(c3Command)).named("C2");
|
||||
var group = mech.run(co -> co.await(c2Command)).named("Group");
|
||||
|
||||
m_scheduler.schedule(group);
|
||||
m_scheduler.run();
|
||||
|
||||
var scheduledCommand1 = Command.noRequirements().executing(Coroutine::park).named("Command 1");
|
||||
var scheduledCommand2 = Command.noRequirements().executing(Coroutine::park).named("Command 2");
|
||||
m_scheduler.schedule(scheduledCommand1);
|
||||
m_scheduler.schedule(scheduledCommand2);
|
||||
|
||||
var message = Scheduler.proto.createMessage();
|
||||
Scheduler.proto.pack(message, m_scheduler);
|
||||
var messageJson = message.toString();
|
||||
assertEquals(
|
||||
"""
|
||||
{
|
||||
"lastTimeMs": %s,
|
||||
"queuedCommands": [{
|
||||
"priority": 0,
|
||||
"id": %s,
|
||||
"name": "Command 1",
|
||||
"requirements": []
|
||||
}, {
|
||||
"priority": 0,
|
||||
"id": %s,
|
||||
"name": "Command 2",
|
||||
"requirements": []
|
||||
}],
|
||||
"runningCommands": [{
|
||||
"lastTimeMs": %s,
|
||||
"totalTimeMs": %s,
|
||||
"priority": 0,
|
||||
"id": %s,
|
||||
"name": "Group",
|
||||
"requirements": [{
|
||||
"name": "The mechanism"
|
||||
}]
|
||||
}, {
|
||||
"lastTimeMs": %s,
|
||||
"totalTimeMs": %s,
|
||||
"priority": 0,
|
||||
"id": %s,
|
||||
"parentId": %s,
|
||||
"name": "C2",
|
||||
"requirements": [{
|
||||
"name": "The mechanism"
|
||||
}]
|
||||
}, {
|
||||
"lastTimeMs": %s,
|
||||
"totalTimeMs": %s,
|
||||
"priority": 0,
|
||||
"id": %s,
|
||||
"parentId": %s,
|
||||
"name": "C3",
|
||||
"requirements": [{
|
||||
"name": "The mechanism"
|
||||
}]
|
||||
}, {
|
||||
"lastTimeMs": %s,
|
||||
"totalTimeMs": %s,
|
||||
"priority": 0,
|
||||
"id": %s,
|
||||
"parentId": %s,
|
||||
"name": "Park",
|
||||
"requirements": [{
|
||||
"name": "The mechanism"
|
||||
}]
|
||||
}]
|
||||
}"""
|
||||
.formatted(
|
||||
// Scheduler data
|
||||
m_scheduler.lastRuntimeMs(),
|
||||
|
||||
// On deck commands
|
||||
m_scheduler.runId(scheduledCommand1),
|
||||
m_scheduler.runId(scheduledCommand2),
|
||||
|
||||
// Running commands
|
||||
m_scheduler.lastCommandRuntimeMs(group),
|
||||
m_scheduler.totalRuntimeMs(group),
|
||||
m_scheduler.runId(group), // id
|
||||
// top-level command, no parent ID
|
||||
|
||||
m_scheduler.lastCommandRuntimeMs(c2Command),
|
||||
m_scheduler.totalRuntimeMs(c2Command),
|
||||
m_scheduler.runId(c2Command), // id
|
||||
m_scheduler.runId(group), // parent
|
||||
m_scheduler.lastCommandRuntimeMs(c3Command),
|
||||
m_scheduler.totalRuntimeMs(c3Command),
|
||||
m_scheduler.runId(c3Command), // id
|
||||
m_scheduler.runId(c2Command), // parent
|
||||
m_scheduler.lastCommandRuntimeMs(parkCommand),
|
||||
m_scheduler.totalRuntimeMs(parkCommand),
|
||||
m_scheduler.runId(parkCommand), // id
|
||||
m_scheduler.runId(c3Command) // parent
|
||||
),
|
||||
messageJson);
|
||||
}
|
||||
}
|
||||
161
commandsv3/src/test/java/org/wpilib/commands3/SchedulerTest.java
Normal file
161
commandsv3/src/test/java/org/wpilib/commands3/SchedulerTest.java
Normal file
@@ -0,0 +1,161 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SchedulerTest extends CommandTestBase {
|
||||
@Test
|
||||
void basic() {
|
||||
var enabled = new AtomicBoolean(false);
|
||||
var ran = new AtomicBoolean(false);
|
||||
var command =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
coroutine -> {
|
||||
do {
|
||||
coroutine.yield();
|
||||
} while (!enabled.get());
|
||||
ran.set(true);
|
||||
})
|
||||
.named("Basic Command");
|
||||
|
||||
m_scheduler.schedule(command);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(command), "Command should be running after being scheduled");
|
||||
|
||||
enabled.set(true);
|
||||
m_scheduler.run();
|
||||
if (m_scheduler.isRunning(command)) {
|
||||
fail("Command should no longer be running after awaiting its completion");
|
||||
}
|
||||
|
||||
assertTrue(ran.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("PMD.ImmutableField") // PMD bugs
|
||||
void atomicity() {
|
||||
var mechanism =
|
||||
new Mechanism("X", m_scheduler) {
|
||||
int m_x = 0;
|
||||
};
|
||||
|
||||
// Launch 100 commands that each call `x++` 500 times.
|
||||
// If commands run on different threads, the lack of atomic
|
||||
// operations or locks will mean the final number will be
|
||||
// less than the expected 50,000
|
||||
int numCommands = 100;
|
||||
int iterations = 500;
|
||||
|
||||
for (int cmdCount = 0; cmdCount < numCommands; cmdCount++) {
|
||||
var command =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
coroutine -> {
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
mechanism.m_x++;
|
||||
coroutine.yield();
|
||||
}
|
||||
})
|
||||
.named("CountCommand[" + cmdCount + "]");
|
||||
|
||||
m_scheduler.schedule(command);
|
||||
}
|
||||
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
m_scheduler.run();
|
||||
}
|
||||
|
||||
assertEquals(numCommands * iterations, mechanism.m_x);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("PMD.ImmutableField") // PMD bugs
|
||||
void runMechanism() {
|
||||
var example =
|
||||
new Mechanism("Counting", m_scheduler) {
|
||||
int m_x = 0;
|
||||
};
|
||||
|
||||
Command countToTen =
|
||||
example
|
||||
.run(
|
||||
coroutine -> {
|
||||
example.m_x = 0;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
coroutine.yield();
|
||||
example.m_x++;
|
||||
}
|
||||
})
|
||||
.named("Count To Ten");
|
||||
|
||||
m_scheduler.schedule(countToTen);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
m_scheduler.run();
|
||||
}
|
||||
m_scheduler.run();
|
||||
|
||||
assertEquals(10, example.m_x);
|
||||
}
|
||||
|
||||
@Test
|
||||
void compositionsDoNotNeedRequirements() {
|
||||
var m1 = new Mechanism("M1", m_scheduler);
|
||||
var m2 = new Mechanism("m2", m_scheduler);
|
||||
|
||||
// the group has no requirements, but can schedule child commands that do
|
||||
var group =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.awaitAll(
|
||||
m1.run(Coroutine::park).named("M1 Command"),
|
||||
m2.run(Coroutine::park).named("M2 Command"));
|
||||
})
|
||||
.named("Composition");
|
||||
|
||||
m_scheduler.schedule(group);
|
||||
m_scheduler.run(); // start m1 and m2 commands
|
||||
assertEquals(3, m_scheduler.getRunningCommands().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedMechanisms() {
|
||||
var superstructure =
|
||||
new Mechanism("Superstructure", m_scheduler) {
|
||||
private final Mechanism m_elevator = new Mechanism("Elevator", m_scheduler);
|
||||
private final Mechanism m_arm = new Mechanism("Arm", m_scheduler);
|
||||
|
||||
public Command superCommand() {
|
||||
return run(co -> {
|
||||
co.await(m_elevator.run(Coroutine::park).named("Elevator Subcommand"));
|
||||
co.await(m_arm.run(Coroutine::park).named("Arm Subcommand"));
|
||||
})
|
||||
.named("Super Command");
|
||||
}
|
||||
};
|
||||
|
||||
m_scheduler.schedule(superstructure.superCommand());
|
||||
m_scheduler.run();
|
||||
assertEquals(
|
||||
List.of(superstructure.m_arm.getDefaultCommand()),
|
||||
superstructure.m_arm.getRunningCommands(),
|
||||
"Arm should only be running its default command");
|
||||
|
||||
// Scheduling something that requires an in-use inner mechanism cancels the outer command
|
||||
m_scheduler.schedule(superstructure.m_elevator.run(Coroutine::park).named("Conflict"));
|
||||
|
||||
m_scheduler.run(); // schedules the default superstructure command
|
||||
m_scheduler.run(); // starts running the default superstructure command
|
||||
assertEquals(List.of(superstructure.getDefaultCommand()), superstructure.getRunningCommands());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import static edu.wpi.first.units.Units.Microseconds;
|
||||
import static edu.wpi.first.units.Units.Milliseconds;
|
||||
import static edu.wpi.first.units.Units.Seconds;
|
||||
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 edu.wpi.first.wpilibj.RobotController;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SchedulerTimingTests extends CommandTestBase {
|
||||
@Test
|
||||
void commandAwaitingItself() {
|
||||
// This command deadlocks on itself. It's calling yield() in an infinite loop, which is
|
||||
// equivalent to calling Coroutine.park(). No deleterious side effects other than stalling
|
||||
// the command
|
||||
AtomicReference<Command> commandRef = new AtomicReference<>();
|
||||
var command =
|
||||
Command.noRequirements().executing(co -> co.await(commandRef.get())).named("Self Await");
|
||||
commandRef.set(command);
|
||||
|
||||
m_scheduler.schedule(command);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(command));
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandDeadlock() {
|
||||
AtomicReference<Command> parentRef = new AtomicReference<>();
|
||||
AtomicReference<Command> childRef = new AtomicReference<>();
|
||||
|
||||
// Deadlock scenario:
|
||||
// parent starts, schedules child, then waits for child to exit
|
||||
// child starts, waits for parent to exit
|
||||
//
|
||||
// Each successive run sees parent mount, check for child, then yield.
|
||||
// Then sees child mount, check for parent, then also yield.
|
||||
// This is like two threads spinwaiting for the other to exit.
|
||||
//
|
||||
// Externally canceling child allows parent to continue
|
||||
// Externally canceling parent cancels both
|
||||
var parent = Command.noRequirements().executing(co -> co.await(childRef.get())).named("Parent");
|
||||
var child = Command.noRequirements().executing(co -> co.await(parentRef.get())).named("Child");
|
||||
parentRef.set(parent);
|
||||
childRef.set(child);
|
||||
|
||||
m_scheduler.schedule(parent);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(parent));
|
||||
assertTrue(m_scheduler.isRunning(child));
|
||||
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(parent));
|
||||
assertTrue(m_scheduler.isRunning(child));
|
||||
|
||||
m_scheduler.cancel(parent);
|
||||
m_scheduler.run();
|
||||
assertFalse(m_scheduler.isRunning(parent));
|
||||
assertFalse(m_scheduler.isRunning(child));
|
||||
}
|
||||
|
||||
@Test
|
||||
void delayedCommandDeadlock() {
|
||||
AtomicReference<Command> ref1 = new AtomicReference<>();
|
||||
AtomicReference<Command> ref2 = new AtomicReference<>();
|
||||
|
||||
AtomicBoolean command1CompletedNormally = new AtomicBoolean(false);
|
||||
AtomicBoolean command2CompletedNormally = new AtomicBoolean(false);
|
||||
|
||||
// Deadlock scenario:
|
||||
// command1 starts, waits for command2 to exit
|
||||
// command2 starts, waits for command1 to exit
|
||||
//
|
||||
// Each successive run sees command1 mount, check for command2, then yield.
|
||||
// Then sees command2 mount, check for command1, then also yield.
|
||||
// This is like two threads spinwaiting for the other to exit.
|
||||
//
|
||||
// Externally canceling either command allows the other to exit
|
||||
var command1 =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.yield();
|
||||
co.await(ref2.get());
|
||||
command1CompletedNormally.set(true);
|
||||
})
|
||||
.named("Command 1");
|
||||
var command2 =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.yield();
|
||||
co.await(ref1.get());
|
||||
command2CompletedNormally.set(true);
|
||||
})
|
||||
.named("Command 2");
|
||||
ref1.set(command1);
|
||||
ref2.set(command2);
|
||||
|
||||
m_scheduler.schedule(command1);
|
||||
m_scheduler.schedule(command2);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(command1));
|
||||
assertTrue(m_scheduler.isRunning(command2));
|
||||
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(command1));
|
||||
assertTrue(m_scheduler.isRunning(command2));
|
||||
|
||||
m_scheduler.cancel(command2);
|
||||
m_scheduler.run();
|
||||
assertFalse(m_scheduler.isRunning(command1));
|
||||
assertFalse(m_scheduler.isRunning(command2));
|
||||
assertTrue(
|
||||
command1CompletedNormally.get(),
|
||||
"Command 1 should have completed normally after command 2 stopped");
|
||||
assertFalse(
|
||||
command2CompletedNormally.get(),
|
||||
"Canceling command 2 should have stopped it before completing");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forkedChildRunsOnce() {
|
||||
AtomicInteger runCount = new AtomicInteger(0);
|
||||
|
||||
var inner =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
runCount.incrementAndGet();
|
||||
co.yield();
|
||||
|
||||
runCount.incrementAndGet();
|
||||
co.yield();
|
||||
})
|
||||
.named("Inner");
|
||||
|
||||
var outer = Command.noRequirements().executing(co -> co.await(inner)).named("Outer");
|
||||
m_scheduler.schedule(outer);
|
||||
m_scheduler.run();
|
||||
|
||||
assertEquals(1, runCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shortWaitWaitsOneLoop() {
|
||||
AtomicLong time = new AtomicLong(0);
|
||||
RobotController.setTimeSource(time::get);
|
||||
|
||||
AtomicBoolean completedWait = new AtomicBoolean(false);
|
||||
var command =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.wait(Milliseconds.of(1));
|
||||
completedWait.set(true);
|
||||
})
|
||||
.named("Short Wait");
|
||||
|
||||
m_scheduler.schedule(command);
|
||||
m_scheduler.run();
|
||||
|
||||
// wait 1 full second (much longer than the wait period)
|
||||
time.set((long) Seconds.of(1).in(Microseconds));
|
||||
m_scheduler.run();
|
||||
assertTrue(
|
||||
completedWait.get(),
|
||||
"Command with a short wait should have completed if its duration has elapsed between runs");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shortWaitWaitsOneLoopWithFastPeriod() {
|
||||
AtomicLong time = new AtomicLong(0);
|
||||
RobotController.setTimeSource(time::get);
|
||||
AtomicBoolean completedWait = new AtomicBoolean(false);
|
||||
var command =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.wait(Milliseconds.of(1));
|
||||
completedWait.set(true);
|
||||
})
|
||||
.named("Short Wait");
|
||||
|
||||
m_scheduler.schedule(command);
|
||||
m_scheduler.run();
|
||||
|
||||
// move forward by half the wait period
|
||||
time.set((long) Milliseconds.of(0.5).in(Microseconds));
|
||||
m_scheduler.run();
|
||||
assertFalse(completedWait.get(), "Command should still be waiting for 1 ms to elapse");
|
||||
|
||||
// move forward by the rest of the wait period
|
||||
time.set((long) Milliseconds.of(1).in(Microseconds));
|
||||
m_scheduler.run();
|
||||
assertTrue(
|
||||
completedWait.get(),
|
||||
"Command with a short wait should have completed if its duration has elapsed between runs");
|
||||
}
|
||||
|
||||
@Test
|
||||
void awaitingExitsImmediatelyWithoutAOneLoopDelay() {
|
||||
AtomicInteger innerRuns = new AtomicInteger(0);
|
||||
var inner =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
// executed immediately when forked
|
||||
innerRuns.incrementAndGet();
|
||||
co.yield();
|
||||
|
||||
// executed again on the next scheduler run, after the forking command runs
|
||||
innerRuns.incrementAndGet();
|
||||
})
|
||||
.named("Inner");
|
||||
|
||||
var outer = Command.noRequirements().executing(co -> co.await(inner)).named("Outer");
|
||||
m_scheduler.schedule(outer);
|
||||
|
||||
// First run: runs outer, forks inner, inner runs to its first yield, outer yields
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(inner));
|
||||
assertTrue(m_scheduler.isRunning(outer));
|
||||
assertEquals(1, innerRuns.get());
|
||||
|
||||
// Second run: runs inner to completion, runs outer, outer sees inner is complete and exits
|
||||
// NOTE: If child commands ran AFTER their parents, then outer would not have exited here and
|
||||
// would take another scheduler run to complete
|
||||
m_scheduler.run();
|
||||
assertFalse(m_scheduler.isRunning(inner));
|
||||
assertFalse(m_scheduler.isRunning(outer));
|
||||
assertEquals(2, innerRuns.get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
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.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SequentialGroupTest extends CommandTestBase {
|
||||
@Test
|
||||
void single() {
|
||||
var command = Command.noRequirements().executing(Coroutine::yield).named("The Command");
|
||||
|
||||
var sequence = new SequentialGroup("The Sequence", List.of(command));
|
||||
m_scheduler.schedule(sequence);
|
||||
|
||||
// First run - the composed command starts and yields; sequence yields
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(sequence));
|
||||
assertTrue(m_scheduler.isRunning(command));
|
||||
|
||||
// Second run - the composed command completes; sequence sees its completion and exits
|
||||
m_scheduler.run();
|
||||
assertFalse(m_scheduler.isRunning(sequence));
|
||||
assertFalse(m_scheduler.isRunning(command));
|
||||
}
|
||||
|
||||
@Test
|
||||
void twoCommands() {
|
||||
var c1 = Command.noRequirements().executing(Coroutine::yield).named("C1");
|
||||
var c2 = Command.noRequirements().executing(Coroutine::yield).named("C2");
|
||||
|
||||
var sequence = new SequentialGroup("C1 > C2", List.of(c1, c2));
|
||||
m_scheduler.schedule(sequence);
|
||||
|
||||
// First run - c1 is scheduled and starts
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(sequence), "Sequence should be running");
|
||||
assertTrue(m_scheduler.isRunning(c1), "Starting the sequence should start the first command");
|
||||
assertFalse(
|
||||
m_scheduler.isScheduledOrRunning(c2),
|
||||
"The second command should still be pending completion of the first command");
|
||||
|
||||
// Second run - c1 completes, sequence sees it finish, schedules c2
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(sequence));
|
||||
assertFalse(m_scheduler.isRunning(c1), "First command should have completed");
|
||||
assertTrue(
|
||||
m_scheduler.isScheduledOrRunning(c2), "Second command should not start in the same cycle");
|
||||
|
||||
// Third run - c2 completes, sequence sees it finish, exits
|
||||
m_scheduler.run();
|
||||
assertFalse(m_scheduler.isRunning(sequence));
|
||||
assertFalse(m_scheduler.isRunning(c2), "Second command should have started");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritsRequirements() {
|
||||
var mech1 = new Mechanism("Mech 1", m_scheduler);
|
||||
var mech2 = new Mechanism("Mech 2", m_scheduler);
|
||||
var command1 = mech1.run(Coroutine::park).named("Command 1");
|
||||
var command2 = mech2.run(Coroutine::park).named("Command 2");
|
||||
var sequence = new SequentialGroup("Sequence", List.of(command1, command2));
|
||||
assertEquals(Set.of(mech1, mech2), sequence.requirements(), "Requirements were not inherited");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritsPriority() {
|
||||
var mech1 = new Mechanism("Mech 1", m_scheduler);
|
||||
var mech2 = new Mechanism("Mech 2", m_scheduler);
|
||||
var command1 = mech1.run(Coroutine::park).withPriority(100).named("Command 1");
|
||||
var command2 = mech2.run(Coroutine::park).withPriority(200).named("Command 2");
|
||||
var sequence = new SequentialGroup("Sequence", List.of(command1, command2));
|
||||
assertEquals(200, sequence.priority(), "Priority was not inherited");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class StagedCommandBuilderTest {
|
||||
private static final Runnable no_op = () -> {};
|
||||
|
||||
private Mechanism m_mech1;
|
||||
private Mechanism m_mech2;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
Scheduler scheduler = Scheduler.createIndependentScheduler();
|
||||
m_mech1 = new Mechanism("Mech 1", scheduler);
|
||||
m_mech2 = new Mechanism("Mech 2", scheduler);
|
||||
}
|
||||
|
||||
// The next two tests are to check that various forms of builder usage are able to compile.
|
||||
|
||||
@Test
|
||||
void streamlined() {
|
||||
Command command =
|
||||
new StagedCommandBuilder()
|
||||
.noRequirements()
|
||||
.executing(Coroutine::park)
|
||||
.until(() -> false)
|
||||
.named("Name");
|
||||
|
||||
assertEquals("Name", command.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void allOptions() {
|
||||
var mech = new Mechanism("Mech", Scheduler.createIndependentScheduler());
|
||||
|
||||
Command command =
|
||||
new StagedCommandBuilder()
|
||||
.noRequirements()
|
||||
.requiring(mech)
|
||||
.requiring(mech, mech)
|
||||
.requiring(List.of(mech))
|
||||
.executing(Coroutine::park)
|
||||
.whenCanceled(no_op)
|
||||
.until(() -> false)
|
||||
.withPriority(10)
|
||||
.named("Name");
|
||||
|
||||
assertEquals("Name", command.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void starting_noRequirements_throwsAfterBuild() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
var ignored = builder.noRequirements().executing(c -> {}).named("cmd");
|
||||
|
||||
var err = assertThrows(IllegalStateException.class, builder::noRequirements);
|
||||
assertEquals("Command builders cannot be reused", err.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void starting_requiringVarargs_throwsAfterBuild() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
var ignored = builder.noRequirements().executing(c -> {}).named("cmd");
|
||||
|
||||
var err = assertThrows(IllegalStateException.class, () -> builder.requiring(m_mech1, m_mech2));
|
||||
assertEquals("Command builders cannot be reused", err.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void starting_requiringCollection_throwsAfterBuild() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
var ignored = builder.noRequirements().executing(c -> {}).named("cmd");
|
||||
|
||||
var err =
|
||||
assertThrows(
|
||||
IllegalStateException.class, () -> builder.requiring(List.of(m_mech1, m_mech2)));
|
||||
assertEquals("Command builders cannot be reused", err.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void requirements_requiringSingle_throwsAfterBuild() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
var reqStage = builder.noRequirements();
|
||||
var ignored = reqStage.executing(c -> {}).named("cmd");
|
||||
|
||||
var err = assertThrows(IllegalStateException.class, () -> reqStage.requiring(m_mech1));
|
||||
assertEquals("Command builders cannot be reused", err.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void requirements_requiringVarargs_throwsAfterBuild() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
var reqStage = builder.noRequirements();
|
||||
var ignored = reqStage.executing(Coroutine::park).named("cmd");
|
||||
|
||||
var err = assertThrows(IllegalStateException.class, () -> reqStage.requiring(m_mech1, m_mech2));
|
||||
assertEquals("Command builders cannot be reused", err.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void requirements_requiringCollection_throwsAfterBuild() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
var reqStage = builder.noRequirements();
|
||||
var ignored = reqStage.executing(Coroutine::park).named("cmd");
|
||||
|
||||
var err = assertThrows(IllegalStateException.class, () -> reqStage.requiring(List.of(m_mech1)));
|
||||
assertEquals("Command builders cannot be reused", err.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void requirements_executing_throwsAfterBuild() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
var reqStage = builder.noRequirements();
|
||||
var ignored = reqStage.executing(c -> {}).named("cmd");
|
||||
|
||||
Consumer<Coroutine> impl = Coroutine::park;
|
||||
var err = assertThrows(IllegalStateException.class, () -> reqStage.executing(impl));
|
||||
assertEquals("Command builders cannot be reused", err.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void execution_whenCanceled_throwsAfterBuild() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
var execStage = builder.noRequirements().executing(c -> {});
|
||||
var ignored = execStage.named("cmd");
|
||||
|
||||
var err = assertThrows(IllegalStateException.class, () -> execStage.whenCanceled(() -> {}));
|
||||
assertEquals("Command builders cannot be reused", err.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void execution_withPriority_throwsAfterBuild() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
var execStage = builder.noRequirements().executing(c -> {});
|
||||
var ignored = execStage.named("cmd");
|
||||
|
||||
var err = assertThrows(IllegalStateException.class, () -> execStage.withPriority(7));
|
||||
assertEquals("Command builders cannot be reused", err.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void execution_until_throwsAfterBuild() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
var execStage = builder.noRequirements().executing(c -> {});
|
||||
var ignored = execStage.named("cmd");
|
||||
|
||||
BooleanSupplier endCondition = () -> true;
|
||||
var err = assertThrows(IllegalStateException.class, () -> execStage.until(endCondition));
|
||||
assertEquals("Command builders cannot be reused", err.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void execution_named_throwsAfterBuild() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
var execStage = builder.noRequirements().executing(c -> {});
|
||||
var ignored = execStage.named("cmd");
|
||||
|
||||
var err = assertThrows(IllegalStateException.class, () -> execStage.named("other"));
|
||||
assertEquals("Command builders cannot be reused", err.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void starting_requiringVarargs_nullFirstRequirement_throwsNPE() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
assertThrows(NullPointerException.class, () -> builder.requiring(null, m_mech2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void starting_requiringVarargs_nullArray_throwsNPE() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
assertThrows(NullPointerException.class, () -> builder.requiring(m_mech1, (Mechanism[]) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void starting_requiringVarargs_nullInExtra_throwsNPE() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
assertThrows(NullPointerException.class, () -> builder.requiring(m_mech1, m_mech2, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void starting_requiringCollection_nullCollection_throwsNPE() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
assertThrows(NullPointerException.class, () -> builder.requiring((Collection<Mechanism>) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void starting_requiringCollection_nullElement_throwsNPE() {
|
||||
var builder = new StagedCommandBuilder();
|
||||
var listWithNull = Arrays.asList(m_mech1, null, m_mech2); // Arrays.asList allows nulls
|
||||
assertThrows(NullPointerException.class, () -> builder.requiring(listWithNull));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requirements_requiringSingle_null_throwsNPE() {
|
||||
var req = new StagedCommandBuilder().noRequirements();
|
||||
assertThrows(NullPointerException.class, () -> req.requiring((Mechanism) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requirements_requiringVarargs_nullFirstRequirement_throwsNPE() {
|
||||
var req = new StagedCommandBuilder().noRequirements();
|
||||
assertThrows(NullPointerException.class, () -> req.requiring(null, m_mech2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requirements_requiringVarargs_nullArray_throwsNPE() {
|
||||
var req = new StagedCommandBuilder().noRequirements();
|
||||
assertThrows(NullPointerException.class, () -> req.requiring(m_mech1, (Mechanism[]) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requirements_requiringVarargs_nullInExtra_throwsNPE() {
|
||||
var req = new StagedCommandBuilder().noRequirements();
|
||||
assertThrows(NullPointerException.class, () -> req.requiring(m_mech1, m_mech2, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requirements_requiringCollection_nullCollection_throwsNPE() {
|
||||
var req = new StagedCommandBuilder().noRequirements();
|
||||
assertThrows(NullPointerException.class, () -> req.requiring((Collection<Mechanism>) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requirements_requiringCollection_nullElement_throwsNPE() {
|
||||
var req = new StagedCommandBuilder().noRequirements();
|
||||
var listWithNull = Arrays.asList(m_mech1, null); // Arrays.asList allows nulls
|
||||
assertThrows(NullPointerException.class, () -> req.requiring(listWithNull));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requirements_executing_nullImpl_throwsNPE() {
|
||||
var req = new StagedCommandBuilder().noRequirements();
|
||||
assertThrows(NullPointerException.class, () -> req.executing(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void execution_named_nullName_throwsNPE() {
|
||||
var exec = new StagedCommandBuilder().noRequirements().executing(c -> {});
|
||||
assertThrows(NullPointerException.class, () -> exec.named(null));
|
||||
}
|
||||
}
|
||||
247
commandsv3/src/test/java/org/wpilib/commands3/TriggerTest.java
Normal file
247
commandsv3/src/test/java/org/wpilib/commands3/TriggerTest.java
Normal file
@@ -0,0 +1,247 @@
|
||||
// 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.
|
||||
|
||||
package org.wpilib.commands3;
|
||||
|
||||
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;
|
||||
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);
|
||||
var command = Command.noRequirements().executing(Coroutine::park).named("Command");
|
||||
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);
|
||||
var command = Command.noRequirements().executing(Coroutine::park).named("Command");
|
||||
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);
|
||||
var command = Command.noRequirements().executing(Coroutine::park).named("Command");
|
||||
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);
|
||||
var command = Command.noRequirements().executing(Coroutine::park).named("Command");
|
||||
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);
|
||||
var command = Command.noRequirements().executing(Coroutine::park).named("Command");
|
||||
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);
|
||||
var command = Command.noRequirements().executing(Coroutine::park).named("Command");
|
||||
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 =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
while (true) {
|
||||
innerRan.set(true);
|
||||
co.park();
|
||||
}
|
||||
})
|
||||
.named("Inner");
|
||||
|
||||
var outer =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
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);
|
||||
|
||||
var command = Command.noRequirements().executing(Coroutine::park).named("Command");
|
||||
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");
|
||||
}
|
||||
|
||||
// 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 =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
triggeredCommandRan.set(true);
|
||||
co.park();
|
||||
})
|
||||
.named("Inner");
|
||||
|
||||
var awaited =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
co -> {
|
||||
co.yield();
|
||||
condition.set(true);
|
||||
})
|
||||
.named("Awaited");
|
||||
|
||||
var outer =
|
||||
Command.noRequirements()
|
||||
.executing(
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user