Files
allwpilib/commandsv3/src/test/java/org/wpilib/commands3/SchedulerTest.java
Sam Carlberg b37e2d9343 [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.
2025-10-10 13:47:22 -07:00

162 lines
5.0 KiB
Java

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