feat: Add template system to weevil new command
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.
This commit is contained in:
554
templates/testing/TESTING_GUIDE.md
Normal file
554
templates/testing/TESTING_GUIDE.md
Normal file
@@ -0,0 +1,554 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user