mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-19 00:41:43 +00:00
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.
162 lines
5.0 KiB
Java
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());
|
|
}
|
|
}
|