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.
555 lines
14 KiB
Markdown
555 lines
14 KiB
Markdown
# 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:
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```bash
|
|
# 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)
|
|
```java
|
|
public interface MotorController {
|
|
void setPower(double power);
|
|
double getPower();
|
|
}
|
|
```
|
|
|
|
### 2. Real Implementation (FTC-dependent)
|
|
```java
|
|
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)
|
|
```java
|
|
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)
|
|
```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
|
|
```java
|
|
// 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
|
|
```java
|
|
// 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
|
|
```java
|
|
// 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
|
|
```java
|
|
// 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
|
|
```java
|
|
// 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**
|
|
```java
|
|
@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**
|
|
```java
|
|
@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**
|
|
```java
|
|
@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**
|
|
```java
|
|
@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`:
|
|
|
|
```kotlin
|
|
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)
|
|
```java
|
|
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
|
|
```java
|
|
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
|
|
```java
|
|
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.
|