Desktop Simulation & Testing¶
Because valleyLib-core has zero Android or FTC SDK dependencies, your command logic can run on a laptop: JUnit tests, desktop simulations, and CI — no robot, no phone, no Android Studio deploy cycle.
What runs where¶
| Desktop JVM | Robot only | |
|---|---|---|
Command, groups, decorators |
✅ | |
CommandScheduler, listeners |
✅ | |
Subsystem (+ simulationPeriodic) |
✅ | |
AutoDsl, StateMachine |
✅ | |
RobotClock / ManualClock (controllable time) |
✅ | |
CommandXboxLike (supplier-based input), Trigger |
✅ | |
Motor, CommandOpMode, Pedro, telemetry |
✅ (needs FTC SDK) |
The trick is keeping your logic in core-only terms and injecting hardware behind interfaces or supplier functions.
Unit-testing commands¶
Add valleyLib-core to a plain JVM test module and drive the scheduler by hand:
class AutoRoutineTest {
private final CommandScheduler scheduler = CommandScheduler.getInstance();
@BeforeEach
void setUp() {
scheduler.reset(); // full isolation between tests
}
@Test
void sequenceRunsInOrder() {
List<String> log = new ArrayList<>();
Command auto = Commands.sequence(
Commands.runOnce(() -> log.add("first")),
Commands.runOnce(() -> log.add("second"))
);
scheduler.schedule(auto);
scheduler.run(); // one tick: first step
scheduler.run(); // second step
assertEquals(List.of("first", "second"), log);
}
}
Each scheduler.run() call is one loop tick — deterministic and fast. The library's own test suite (CommandSchedulerTest, CommandGroupTest, DecoratorSemanticsTest, StateMachineTest, ClockTest, TriggerTest, ...) uses exactly this pattern; read them for more examples.
Controlling time¶
Every time-based behavior in the library — WaitCommand, withTimeout, StateMachine.transitionAfter/getTimeInState, Trigger.debounce, TimedCommand, the PIDFController period, and Motor.Encoder velocity estimation — reads the clock through RobotClock (in com.vcs.valleylib.core.time) rather than System.nanoTime(). On the robot that is the system clock. In tests, install a ManualClock and move time yourself:
class TimingTest {
private final ManualClock clock = new ManualClock();
@BeforeEach void setUp() { RobotClock.setClock(clock); }
@AfterEach void tearDown() { RobotClock.useSystemClock(); }
@Test
void shooterSpinsUpForHalfASecond() {
Command spinUp = shooter.runEnd(shooter::spin, shooter::hold).withTimeout(0.5);
spinUp.initialize();
clock.advance(0.49);
spinUp.execute();
assertFalse(spinUp.isFinished());
clock.advance(0.01);
spinUp.execute();
assertTrue(spinUp.isFinished()); // exactly on the 500 ms boundary
}
}
RobotClock |
Purpose |
|---|---|
nanos() / seconds() / millis() |
Read the installed clock |
setClock(Clock) |
Install any Clock (a long nanos() functional interface) |
useSystemClock() |
Restore the default |
isSystemClock() |
Whether time is real or simulated |
ManualClock |
Purpose |
|---|---|
advance(seconds) / advanceNanos(n) |
Move forward (never backwards) |
set(seconds) |
Jump to an absolute time |
Simulation loops
In a desktop simulation, advance the manual clock by your loop period (e.g. clock.advance(0.02)) before each scheduler.run() — timeouts, dwell transitions, and debounces then behave exactly as they will at 50 Hz on the robot, but you can run thousands of cycles per second.
No test should ever need Thread.sleep for library timing. Always restore the system clock in @AfterEach; RobotClock is process-wide.
Simulation hooks¶
Subsystems have a second periodic method that only runs in simulation:
public class SimulatedLift extends Subsystem {
private double positionTicks = 0;
private double commandedPower = 0;
public void setPower(double power) { commandedPower = power; }
public double getPosition() { return positionTicks; }
@Override
public void simulationPeriodic() {
// Toy physics: 500 ticks/s at full power, 20 ms ticks
positionTicks += commandedPower * 500 * 0.020;
}
}
Two ways to run the hooks:
Simulated input¶
CommandXboxLike takes plain suppliers, so a fake gamepad for tests is trivial:
AtomicBoolean aPressed = new AtomicBoolean(false);
CommandXboxLike pad = new CommandXboxLike(
aPressed::get, () -> false, () -> false, () -> false,
() -> false, () -> false,
() -> false, () -> false, () -> false, () -> false,
() -> 0, () -> 0, () -> 0, () -> 0, () -> 0, () -> 0);
TriggerManager triggers = new TriggerManager();
triggers.bind(pad.a().onTrue(Commands.runOnce(() -> log.add("fired"))));
aPressed.set(true);
triggers.poll(); // rising edge → command scheduled
scheduler.run(); // command executes
A recommended workflow¶
- Model mechanisms as core
Subsystems withsimulationPeriodic()physics. - Write your autonomous with
AutoDsl/command composition against those subsystems. - Test the routine on desktop until the sequencing is right.
- Swap the simulated subsystems for
FtcSubsystemimplementations that drive real hardware — the commands don't change.
This catches sequencing bugs, missing requirements, and logic errors in seconds instead of practice-field minutes.