Implements template-based project creation allowing teams to start with
professional example code instead of empty projects.
Features:
- Two templates: 'basic' (minimal) and 'testing' (45-test showcase)
- Template variable substitution ({{PROJECT_NAME}}, etc.)
- Template validation with helpful error messages
- `weevil new --list-templates` command
- Template files embedded in binary at compile time
Technical details:
- Templates stored in templates/basic/ and templates/testing/
- Files ending in .template have variables replaced
- Uses include_dir! macro to embed templates in binary
- Returns file count for user feedback
Testing template includes:
- 3 complete subsystems (MotorCycler, WallApproach, TurnController)
- Hardware abstraction layer with mock implementations
- 45 comprehensive tests (unit, integration, system)
- Professional documentation (DESIGN_AND_TEST_PLAN.md, etc.)
Usage:
weevil new my-robot # basic template
weevil new my-robot --template testing # testing showcase
weevil new --list-templates # show available templates
This enables FTC teams to learn from working code and best practices
rather than starting from scratch.
14 KiB
Testing Guide: Mocking Hardware Without the Robot
The Problem
When you run gradlew test, Gradle tries to compile all your main source code, including files that depend on the FTC SDK (like FtcMotorController and MotorCycleOpMode). Since the FTC SDK isn't available on your Windows machine, compilation fails.
The Solution: Source Set Exclusion
Your build.gradle.kts now excludes FTC-dependent files from test compilation:
sourceSets {
main {
java {
exclude(
"robot/hardware/FtcMotorController.java",
"robot/opmodes/**/*.java"
)
}
}
}
This means:
- ✅
MotorController.java(interface) - Compiles for tests - ✅
MotorCycler.java(pure logic) - Compiles for tests - ✅
MockMotorController.java(test mock) - Compiles for tests - ❌
FtcMotorController.java(FTC SDK) - Skipped for tests - ❌
MotorCycleOpMode.java(FTC SDK) - Skipped for tests
Running Tests
# On Windows
gradlew test
# On Linux/Mac
./gradlew test
The tests run entirely on your Windows JRE - no robot, no Android, no FTC SDK needed!
The Architecture Pattern
1. Interface (Hardware Abstraction)
public interface MotorController {
void setPower(double power);
double getPower();
}
2. Real Implementation (FTC-dependent)
public class FtcMotorController implements MotorController {
private final DcMotor motor; // FTC SDK class
public FtcMotorController(DcMotor motor) {
this.motor = motor;
}
@Override
public void setPower(double power) {
motor.setPower(power);
}
@Override
public double getPower() {
return motor.getPower();
}
}
3. Mock Implementation (Testing)
public class MockMotorController implements MotorController {
private double power = 0.0;
@Override
public void setPower(double power) {
this.power = power;
}
@Override
public double getPower() {
return power;
}
}
4. Business Logic (Pure Java)
public class MotorCycler {
private final MotorController motor; // Interface, not FTC class!
public MotorCycler(MotorController motor, long onMs, long offMs) {
this.motor = motor;
// ...
}
public void update(long currentTimeMs) {
// Time-based state machine
// No FTC SDK dependencies!
}
}
Example: I2C Bump Sensor
Here's how you'd implement the pattern for an I2C bump sensor:
Interface
// src/main/java/robot/hardware/BumpSensor.java
package robot.hardware;
public interface BumpSensor {
/**
* Check if the sensor detects contact.
* @return true if bumped, false otherwise
*/
boolean isBumped();
/**
* Get the force reading (0.0 to 1.0).
* @return force value
*/
double getForce();
}
FTC Implementation
// src/main/java/robot/hardware/FtcBumpSensor.java
package robot.hardware;
import com.qualcomm.robotcore.hardware.I2cDevice;
import com.qualcomm.robotcore.hardware.I2cDeviceReader;
public class FtcBumpSensor implements BumpSensor {
private final I2cDevice sensor;
private final I2cDeviceReader reader;
private static final double BUMP_THRESHOLD = 0.5;
public FtcBumpSensor(I2cDevice sensor) {
this.sensor = sensor;
this.reader = new I2cDeviceReader(sensor, 0x00, 2);
}
@Override
public boolean isBumped() {
return getForce() > BUMP_THRESHOLD;
}
@Override
public double getForce() {
byte[] data = reader.read();
// Convert I2C bytes to force value
int raw = ((data[0] & 0xFF) << 8) | (data[1] & 0xFF);
return raw / 65535.0;
}
}
Mock Implementation
// src/test/java/robot/hardware/MockBumpSensor.java
package robot.hardware;
public class MockBumpSensor implements BumpSensor {
private double force = 0.0;
/**
* Simulate hitting a wall with given force.
*/
public void simulateImpact(double force) {
this.force = Math.max(0.0, Math.min(1.0, force));
}
/**
* Simulate sensor returning to neutral.
*/
public void reset() {
this.force = 0.0;
}
@Override
public boolean isBumped() {
return force > 0.5;
}
@Override
public double getForce() {
return force;
}
}
Business Logic Using Sensor
// src/main/java/robot/subsystems/CollisionDetector.java
package robot.subsystems;
import robot.hardware.BumpSensor;
import robot.hardware.MotorController;
public class CollisionDetector {
private final BumpSensor sensor;
private final MotorController motor;
private boolean collisionDetected = false;
public CollisionDetector(BumpSensor sensor, MotorController motor) {
this.sensor = sensor;
this.motor = motor;
}
public void update() {
if (sensor.isBumped() && !collisionDetected) {
// First detection - stop the motor
motor.setPower(0.0);
collisionDetected = true;
} else if (!sensor.isBumped() && collisionDetected) {
// Sensor cleared - reset flag
collisionDetected = false;
}
}
public boolean hasCollision() {
return collisionDetected;
}
}
Test with Simulated Wall Hit
// src/test/java/robot/subsystems/CollisionDetectorTest.java
package robot.subsystems;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import robot.hardware.MockBumpSensor;
import robot.hardware.MockMotorController;
import static org.junit.jupiter.api.Assertions.*;
class CollisionDetectorTest {
private MockBumpSensor sensor;
private MockMotorController motor;
private CollisionDetector detector;
@BeforeEach
void setUp() {
sensor = new MockBumpSensor();
motor = new MockMotorController();
detector = new CollisionDetector(sensor, motor);
}
@Test
void testWallImpactStopsMotor() {
// Robot is driving
motor.setPower(0.8);
assertEquals(0.8, motor.getPower(), 0.001);
// Simulate hitting a wall with high force
sensor.simulateImpact(0.9);
detector.update();
// Motor should stop
assertEquals(0.0, motor.getPower(), 0.001);
assertTrue(detector.hasCollision());
}
@Test
void testGentleContactDetected() {
// Simulate gentle touch (above threshold)
sensor.simulateImpact(0.6);
detector.update();
assertTrue(detector.hasCollision());
}
@Test
void testBelowThresholdIgnored() {
motor.setPower(0.5);
// Simulate light vibration (below threshold)
sensor.simulateImpact(0.3);
detector.update();
// Should not register as collision
assertFalse(detector.hasCollision());
assertEquals(0.5, motor.getPower(), 0.001);
}
@Test
void testCollisionClearsWhenSensorReleased() {
// Hit wall
sensor.simulateImpact(0.8);
detector.update();
assertTrue(detector.hasCollision());
// Back away from wall
sensor.reset();
detector.update();
// Collision flag should clear
assertFalse(detector.hasCollision());
}
@Test
void testMultipleImpacts() {
// First impact
sensor.simulateImpact(0.7);
detector.update();
assertTrue(detector.hasCollision());
// Clear
sensor.reset();
detector.update();
assertFalse(detector.hasCollision());
// Second impact
sensor.simulateImpact(0.8);
detector.update();
assertTrue(detector.hasCollision());
}
}
Key Benefits
1. Test Real Scenarios Without Hardware
@Test
void testRobotBouncesOffWall() {
// Simulate approach
motor.setPower(0.8);
// Hit wall
sensor.simulateImpact(0.9);
detector.update();
// Verify emergency stop
assertEquals(0.0, motor.getPower());
}
2. Test Edge Cases
@Test
void testSensorNoise() {
// Simulate sensor flutter at threshold
sensor.simulateImpact(0.49);
detector.update();
assertFalse(detector.hasCollision());
sensor.simulateImpact(0.51);
detector.update();
assertTrue(detector.hasCollision());
}
3. Test Timing Issues
@Test
void testRapidImpacts() {
for (int i = 0; i < 10; i++) {
sensor.simulateImpact(0.8);
detector.update();
assertTrue(detector.hasCollision());
sensor.reset();
detector.update();
}
}
4. Integration Tests
@Test
void testFullDriveSequence() {
// Drive forward
motor.setPower(0.5);
for (int i = 0; i < 10; i++) {
detector.update();
assertFalse(detector.hasCollision());
}
// Hit obstacle
sensor.simulateImpact(0.8);
detector.update();
assertEquals(0.0, motor.getPower());
// Back up
motor.setPower(-0.3);
sensor.reset();
detector.update();
// Continue backing
for (int i = 0; i < 5; i++) {
detector.update();
assertEquals(-0.3, motor.getPower());
}
}
File Organization
my-robot/
├── src/main/java/robot/
│ ├── hardware/ # Abstractions
│ │ ├── MotorController.java ✅ Tests compile
│ │ ├── BumpSensor.java ✅ Tests compile
│ │ ├── FtcMotorController.java ❌ Excluded from tests
│ │ └── FtcBumpSensor.java ❌ Excluded from tests
│ │
│ ├── subsystems/ # Business Logic
│ │ ├── MotorCycler.java ✅ Tests compile
│ │ └── CollisionDetector.java ✅ Tests compile
│ │
│ └── opmodes/ # FTC Integration
│ └── MotorCycleOpMode.java ❌ Excluded from tests
│
└── src/test/java/robot/
├── hardware/ # Mocks
│ ├── MockMotorController.java
│ └── MockBumpSensor.java
│
└── subsystems/ # Tests
├── MotorCyclerTest.java
└── CollisionDetectorTest.java
Build Configuration Rules
In build.gradle.kts:
sourceSets {
main {
java {
// Exclude all FTC-dependent code from test compilation
exclude(
"robot/hardware/Ftc*.java", // All FTC implementations
"robot/opmodes/**/*.java" // All OpModes
)
}
}
}
This pattern ensures:
- ✅ Interfaces and pure logic compile for tests
- ✅ Mocks are available in test classpath
- ✅ Tests run on Windows JRE instantly
- ✅ FTC-dependent code is deployed and compiled on robot
- ✅ Same logic runs in tests and on robot
Advanced Mocking Patterns
Stateful Mock (Servo with Position Memory)
public class MockServo implements ServoController {
private double position = 0.5;
private double speed = 1.0; // Instant by default
public void setSpeed(double speed) {
this.speed = speed;
}
@Override
public void setPosition(double target) {
// Simulate gradual movement
double delta = target - position;
position += delta * speed;
position = Math.max(0.0, Math.min(1.0, position));
}
@Override
public double getPosition() {
return position;
}
}
Mock with Latency
public class MockGyro implements GyroSensor {
private double heading = 0.0;
private long lastUpdateTime = 0;
private double drift = 0.1; // Degrees per second drift
public void simulateRotation(double degrees, long timeMs) {
heading += degrees;
heading = (heading + 360) % 360;
lastUpdateTime = timeMs;
}
@Override
public double getHeading(long currentTime) {
// Simulate sensor drift over time
long elapsed = currentTime - lastUpdateTime;
double driftError = (elapsed / 1000.0) * drift;
return (heading + driftError) % 360;
}
}
Mock with Failure Modes
public class MockDistanceSensor implements DistanceSensor {
private double distance = 100.0;
private boolean connected = true;
private double noise = 0.0;
public void simulateDisconnect() {
connected = false;
}
public void setNoise(double stdDev) {
this.noise = stdDev;
}
@Override
public double getDistance() throws SensorException {
if (!connected) {
throw new SensorException("Sensor disconnected");
}
// Add Gaussian noise
double noisyDistance = distance + (Math.random() - 0.5) * noise;
return Math.max(0, noisyDistance);
}
}
Why This Matters
Traditional FTC development:
- ❌ Can't test without robot
- ❌ Long iteration cycles (code → deploy → test → repeat)
- ❌ Hard to test edge cases
- ❌ Integration issues found late
Weevil + proper mocking:
- ✅ Test instantly on PC
- ✅ Rapid iteration (code → test → fix)
- ✅ Comprehensive edge case coverage
- ✅ Catch bugs before robot practice
You're not just testing motor values - you're simulating complete scenarios: wall collisions, sensor failures, timing issues, state machines, everything!
This is professional robotics software engineering.