mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-19 00:41:43 +00:00
[examples] Add unit testing infrastructure (#4646)
This commit is contained in:
32
wpilibcExamples/src/main/cpp/examples/UnitTest/cpp/Robot.cpp
Normal file
32
wpilibcExamples/src/main/cpp/examples/UnitTest/cpp/Robot.cpp
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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.
|
||||
|
||||
#include "Robot.h"
|
||||
|
||||
/**
|
||||
* This function is called periodically during operator control.
|
||||
*/
|
||||
void Robot::TeleopPeriodic() {
|
||||
// Activate the intake while the trigger is held
|
||||
if (m_joystick.GetTrigger()) {
|
||||
m_intake.Activate(IntakeConstants::kIntakeSpeed);
|
||||
} else {
|
||||
m_intake.Activate(0);
|
||||
}
|
||||
|
||||
// Toggle deploying the intake when the top button is pressed
|
||||
if (m_joystick.GetTop()) {
|
||||
if (m_intake.IsDeployed()) {
|
||||
m_intake.Retract();
|
||||
} else {
|
||||
m_intake.Deploy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef RUNNING_FRC_TESTS
|
||||
int main() {
|
||||
return frc::StartRobot<Robot>();
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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.
|
||||
|
||||
#include "subsystems/Intake.h"
|
||||
|
||||
void Intake::Deploy() {
|
||||
m_piston.Set(frc::DoubleSolenoid::Value::kForward);
|
||||
}
|
||||
|
||||
void Intake::Retract() {
|
||||
m_piston.Set(frc::DoubleSolenoid::Value::kReverse);
|
||||
m_motor.Set(0); // turn off the motor
|
||||
}
|
||||
|
||||
void Intake::Activate(double speed) {
|
||||
if (IsDeployed()) {
|
||||
m_motor.Set(speed);
|
||||
} else { // if piston isn't open, do nothing
|
||||
m_motor.Set(0);
|
||||
}
|
||||
}
|
||||
|
||||
bool Intake::IsDeployed() const {
|
||||
return m_piston.Get() == frc::DoubleSolenoid::Value::kForward;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* The Constants header provides a convenient place for teams to hold robot-wide
|
||||
* numerical or bool constants. This should not be used for any other purpose.
|
||||
*
|
||||
* It is generally a good idea to place constants into subsystem- or
|
||||
* command-specific namespaces within this header, which can then be used where
|
||||
* they are needed.
|
||||
*/
|
||||
|
||||
namespace IntakeConstants {
|
||||
constexpr int kMotorPort = 1;
|
||||
|
||||
constexpr int kPistonFwdChannel = 0;
|
||||
constexpr int kPistonRevChannel = 1;
|
||||
constexpr double kIntakeSpeed = 0.5;
|
||||
} // namespace IntakeConstants
|
||||
|
||||
namespace OperatorConstants {
|
||||
constexpr int kJoystickIndex = 0;
|
||||
} // namespace OperatorConstants
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <frc/Joystick.h>
|
||||
#include <frc/TimedRobot.h>
|
||||
|
||||
#include "Constants.h"
|
||||
#include "subsystems/Intake.h"
|
||||
|
||||
class Robot : public frc::TimedRobot {
|
||||
public:
|
||||
void TeleopPeriodic() override;
|
||||
|
||||
private:
|
||||
Intake m_intake;
|
||||
frc::Joystick m_joystick{OperatorConstants::kJoystickIndex};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <frc/DoubleSolenoid.h>
|
||||
#include <frc/motorcontrol/PWMSparkMax.h>
|
||||
|
||||
#include "Constants.h"
|
||||
|
||||
class Intake {
|
||||
public:
|
||||
void Deploy();
|
||||
void Retract();
|
||||
void Activate(double speed);
|
||||
bool IsDeployed() const;
|
||||
|
||||
private:
|
||||
frc::PWMSparkMax m_motor{IntakeConstants::kMotorPort};
|
||||
frc::DoubleSolenoid m_piston{frc::PneumaticsModuleType::CTREPCM,
|
||||
IntakeConstants::kPistonFwdChannel,
|
||||
IntakeConstants::kPistonRevChannel};
|
||||
};
|
||||
@@ -622,7 +622,6 @@
|
||||
],
|
||||
"foldername": "StateSpaceFlywheel",
|
||||
"gradlebase": "cpp",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
@@ -641,7 +640,6 @@
|
||||
],
|
||||
"foldername": "StateSpaceFlywheelSysId",
|
||||
"gradlebase": "cpp",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
@@ -657,7 +655,6 @@
|
||||
],
|
||||
"foldername": "StateSpaceElevator",
|
||||
"gradlebase": "cpp",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
@@ -673,7 +670,6 @@
|
||||
],
|
||||
"foldername": "StateSpaceArm",
|
||||
"gradlebase": "cpp",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
@@ -690,7 +686,6 @@
|
||||
],
|
||||
"foldername": "ElevatorSimulation",
|
||||
"gradlebase": "cpp",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
@@ -706,7 +701,6 @@
|
||||
],
|
||||
"foldername": "DifferentialDrivePoseEstimator",
|
||||
"gradlebase": "cpp",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
@@ -722,7 +716,6 @@
|
||||
],
|
||||
"foldername": "MecanumDrivePoseEstimator",
|
||||
"gradlebase": "cpp",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
@@ -740,7 +733,16 @@
|
||||
],
|
||||
"foldername": "ArmSimulation",
|
||||
"gradlebase": "cpp",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
"name": "UnitTesting",
|
||||
"description": "Demonstrates basic unit testing for a robot project.",
|
||||
"tags": [
|
||||
"Testing"
|
||||
],
|
||||
"foldername": "UnitTest",
|
||||
"gradlebase": "cpp",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
@@ -758,7 +760,6 @@
|
||||
],
|
||||
"foldername": "SimpleDifferentialDriveSimulation",
|
||||
"gradlebase": "cpp",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
@@ -776,7 +777,6 @@
|
||||
],
|
||||
"foldername": "StateSpaceDifferentialDriveSimulation",
|
||||
"gradlebase": "cpp",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
@@ -793,7 +793,6 @@
|
||||
],
|
||||
"foldername": "SwerveDrivePoseEstimator",
|
||||
"gradlebase": "cpp",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
}
|
||||
]
|
||||
|
||||
17
wpilibcExamples/src/test/cpp/examples/UnitTest/cpp/main.cpp
Normal file
17
wpilibcExamples/src/test/cpp/examples/UnitTest/cpp/main.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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.
|
||||
|
||||
#include <hal/HALBase.h>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
/**
|
||||
* Runs all unit tests.
|
||||
*/
|
||||
int main(int argc, char** argv) {
|
||||
HAL_Initialize(500, 0);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
int ret = RUN_ALL_TESTS();
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <frc/DoubleSolenoid.h>
|
||||
#include <frc/simulation/DoubleSolenoidSim.h>
|
||||
#include <frc/simulation/PWMSim.h>
|
||||
|
||||
#include "Constants.h"
|
||||
#include "subsystems/Intake.h"
|
||||
|
||||
class IntakeTest : public testing::Test {
|
||||
protected:
|
||||
Intake intake; // create our intake
|
||||
frc::sim::PWMSim simMotor{
|
||||
IntakeConstants::kMotorPort}; // create our simulation PWM
|
||||
frc::sim::DoubleSolenoidSim simPiston{
|
||||
frc::PneumaticsModuleType::CTREPCM, IntakeConstants::kPistonFwdChannel,
|
||||
IntakeConstants::kPistonRevChannel}; // create our simulation solenoid
|
||||
};
|
||||
|
||||
TEST_F(IntakeTest, DoesntWorkWhenClosed) {
|
||||
intake.Retract(); // close the intake
|
||||
intake.Activate(0.5); // try to activate the motor
|
||||
EXPECT_DOUBLE_EQ(
|
||||
0.0,
|
||||
simMotor.GetSpeed()); // make sure that the value set to the motor is 0
|
||||
}
|
||||
|
||||
TEST_F(IntakeTest, WorksWhenOpen) {
|
||||
intake.Deploy();
|
||||
intake.Activate(0.5);
|
||||
EXPECT_DOUBLE_EQ(0.5, simMotor.GetSpeed());
|
||||
}
|
||||
|
||||
TEST_F(IntakeTest, Retract) {
|
||||
intake.Retract();
|
||||
EXPECT_EQ(frc::DoubleSolenoid::Value::kReverse, simPiston.Get());
|
||||
}
|
||||
|
||||
TEST_F(IntakeTest, Deploy) {
|
||||
intake.Deploy();
|
||||
EXPECT_EQ(frc::DoubleSolenoid::Value::kForward, simPiston.Get());
|
||||
}
|
||||
@@ -174,8 +174,9 @@ model {
|
||||
|
||||
new groovy.json.JsonSlurper().parseText(exampleFile.text).each { entry ->
|
||||
project.tasks.create("run${entry.foldername}", JavaExec) { run ->
|
||||
mainClass = "edu.wpi.first.wpilibj.examples." + entry.foldername + ".Main"
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
run.group "run examples"
|
||||
run.mainClass = "edu.wpi.first.wpilibj.examples." + entry.foldername + "." + entry.mainclass
|
||||
run.classpath = sourceSets.main.runtimeClasspath
|
||||
run.dependsOn it.tasks.install
|
||||
run.systemProperty 'java.library.path', filePath
|
||||
run.environment 'LD_LIBRARY_PATH', filePath
|
||||
@@ -186,6 +187,23 @@ model {
|
||||
run.jvmArgs = ['-XstartOnFirstThread']
|
||||
}
|
||||
}
|
||||
project.tasks.create("test${entry.foldername}", Test) { testTask ->
|
||||
testTask.group "verification"
|
||||
testTask.useJUnitPlatform()
|
||||
testTask.filter {
|
||||
includeTestsMatching("edu.wpi.first.wpilibj.examples.${entry.foldername}.*")
|
||||
setFailOnNoMatchingTests(false)
|
||||
}
|
||||
testTask.classpath = sourceSets.test.runtimeClasspath
|
||||
testTask.systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true'
|
||||
testTask.testLogging {
|
||||
events "failed"
|
||||
exceptionFormat "full"
|
||||
}
|
||||
testTask.systemProperty 'java.library.path', filePath
|
||||
testTask.environment 'LD_LIBRARY_PATH', filePath
|
||||
testTask.workingDir filePath
|
||||
}
|
||||
}
|
||||
|
||||
found = true
|
||||
|
||||
@@ -733,6 +733,17 @@
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
"name": "UnitTesting",
|
||||
"description": "Demonstrates basic unit testing for a robot project.",
|
||||
"tags": [
|
||||
"Testing"
|
||||
],
|
||||
"foldername": "unittest",
|
||||
"gradlebase": "java",
|
||||
"mainclass": "Main",
|
||||
"commandversion": 2
|
||||
},
|
||||
{
|
||||
"name": "DifferentialDrivePoseEstimator",
|
||||
"description": "Demonstrates the use of the DifferentialDrivePoseEstimator as a replacement for differential drive odometry.",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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 edu.wpi.first.wpilibj.examples.unittest;
|
||||
|
||||
/**
|
||||
* The Constants class provides a convenient place for teams to hold robot-wide numerical or boolean
|
||||
* constants. This class should not be used for any other purpose. All constants should be declared
|
||||
* globally (i.e. public static). Do not put anything functional in this class.
|
||||
*
|
||||
* <p>It is advised to statically import this class (or one of its inner classes) wherever the
|
||||
* constants are needed, to reduce verbosity.
|
||||
*/
|
||||
public final class Constants {
|
||||
public static final int kJoystickIndex = 0;
|
||||
|
||||
public static final class IntakeConstants {
|
||||
public static final int kMotorPort = 1;
|
||||
|
||||
public static final int kPistonFwdChannel = 0;
|
||||
public static final int kPistonRevChannel = 1;
|
||||
public static final double kIntakeSpeed = 0.5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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 edu.wpi.first.wpilibj.examples.unittest;
|
||||
|
||||
import edu.wpi.first.wpilibj.RobotBase;
|
||||
|
||||
/**
|
||||
* Do NOT add any static variables to this class, or any initialization at all. Unless you know what
|
||||
* you are doing, do not modify this file except to change the parameter class to the startRobot
|
||||
* call.
|
||||
*/
|
||||
public final class Main {
|
||||
private Main() {}
|
||||
|
||||
/**
|
||||
* Main initialization function. Do not perform any initialization here.
|
||||
*
|
||||
* <p>If you change your main robot class, change the parameter type.
|
||||
*/
|
||||
public static void main(String... args) {
|
||||
RobotBase.startRobot(Robot::new);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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 edu.wpi.first.wpilibj.examples.unittest;
|
||||
|
||||
import edu.wpi.first.wpilibj.Joystick;
|
||||
import edu.wpi.first.wpilibj.TimedRobot;
|
||||
import edu.wpi.first.wpilibj.examples.unittest.Constants.IntakeConstants;
|
||||
import edu.wpi.first.wpilibj.examples.unittest.subsystems.Intake;
|
||||
|
||||
/**
|
||||
* The VM is configured to automatically run this class, and to call the functions corresponding to
|
||||
* each mode, as described in the TimedRobot documentation. If you change the name of this class or
|
||||
* the package after creating this project, you must also update the build.gradle file in the
|
||||
* project.
|
||||
*/
|
||||
public class Robot extends TimedRobot {
|
||||
private Intake m_intake = new Intake();
|
||||
private Joystick m_joystick = new Joystick(Constants.kJoystickIndex);
|
||||
|
||||
/** This function is called periodically during operator control. */
|
||||
@Override
|
||||
public void teleopPeriodic() {
|
||||
// Activate the intake while the trigger is held
|
||||
if (m_joystick.getTrigger()) {
|
||||
m_intake.activate(IntakeConstants.kIntakeSpeed);
|
||||
} else {
|
||||
m_intake.activate(0);
|
||||
}
|
||||
|
||||
// Toggle deploying the intake when the top button is pressed
|
||||
if (m_joystick.getTop()) {
|
||||
if (m_intake.isDeployed()) {
|
||||
m_intake.retract();
|
||||
} else {
|
||||
m_intake.deploy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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 edu.wpi.first.wpilibj.examples.unittest.subsystems;
|
||||
|
||||
import edu.wpi.first.wpilibj.DoubleSolenoid;
|
||||
import edu.wpi.first.wpilibj.PneumaticsModuleType;
|
||||
import edu.wpi.first.wpilibj.examples.unittest.Constants.IntakeConstants;
|
||||
import edu.wpi.first.wpilibj.motorcontrol.PWMSparkMax;
|
||||
|
||||
public class Intake implements AutoCloseable {
|
||||
private final PWMSparkMax m_motor;
|
||||
private final DoubleSolenoid m_piston;
|
||||
|
||||
public Intake() {
|
||||
m_motor = new PWMSparkMax(IntakeConstants.kMotorPort);
|
||||
m_piston =
|
||||
new DoubleSolenoid(
|
||||
PneumaticsModuleType.CTREPCM,
|
||||
IntakeConstants.kPistonFwdChannel,
|
||||
IntakeConstants.kPistonRevChannel);
|
||||
}
|
||||
|
||||
public void deploy() {
|
||||
m_piston.set(DoubleSolenoid.Value.kForward);
|
||||
}
|
||||
|
||||
public void retract() {
|
||||
m_piston.set(DoubleSolenoid.Value.kReverse);
|
||||
m_motor.set(0); // turn off the motor
|
||||
}
|
||||
|
||||
public void activate(double speed) {
|
||||
if (isDeployed()) {
|
||||
m_motor.set(speed);
|
||||
} else { // if piston isn't open, do nothing
|
||||
m_motor.set(0);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDeployed() {
|
||||
return m_piston.get() == DoubleSolenoid.Value.kForward;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
m_piston.close();
|
||||
m_motor.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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 edu.wpi.first.wpilibj.examples.unittest.subsystems;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import edu.wpi.first.hal.HAL;
|
||||
import edu.wpi.first.wpilibj.DoubleSolenoid;
|
||||
import edu.wpi.first.wpilibj.PneumaticsModuleType;
|
||||
import edu.wpi.first.wpilibj.examples.unittest.Constants.IntakeConstants;
|
||||
import edu.wpi.first.wpilibj.simulation.DoubleSolenoidSim;
|
||||
import edu.wpi.first.wpilibj.simulation.PWMSim;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class IntakeTest {
|
||||
static final double DELTA = 1e-2; // acceptable deviation range
|
||||
Intake m_intake;
|
||||
PWMSim m_simMotor;
|
||||
DoubleSolenoidSim m_simPiston;
|
||||
|
||||
@BeforeEach // this method will run before each test
|
||||
void setup() {
|
||||
assert HAL.initialize(500, 0); // initialize the HAL, crash if failed
|
||||
m_intake = new Intake(); // create our intake
|
||||
m_simMotor =
|
||||
new PWMSim(IntakeConstants.kMotorPort); // create our simulation PWM motor controller
|
||||
m_simPiston =
|
||||
new DoubleSolenoidSim(
|
||||
PneumaticsModuleType.CTREPCM,
|
||||
IntakeConstants.kPistonFwdChannel,
|
||||
IntakeConstants.kPistonRevChannel); // create our simulation solenoid
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.SignatureDeclareThrowsException")
|
||||
@AfterEach // this method will run after each test
|
||||
void shutdown() throws Exception {
|
||||
m_intake.close(); // destroy our intake object
|
||||
}
|
||||
|
||||
@Test // marks this method as a test
|
||||
void doesntWorkWhenClosed() {
|
||||
m_intake.retract(); // close the intake
|
||||
m_intake.activate(0.5); // try to activate the motor
|
||||
assertEquals(
|
||||
0.0, m_simMotor.getSpeed(), DELTA); // make sure that the value set to the motor is 0
|
||||
}
|
||||
|
||||
@Test
|
||||
void worksWhenOpen() {
|
||||
m_intake.deploy();
|
||||
m_intake.activate(0.5);
|
||||
assertEquals(0.5, m_simMotor.getSpeed(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
void retractTest() {
|
||||
m_intake.retract();
|
||||
assertEquals(DoubleSolenoid.Value.kReverse, m_simPiston.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deployTest() {
|
||||
m_intake.deploy();
|
||||
assertEquals(DoubleSolenoid.Value.kForward, m_simPiston.get());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user