[examples] Add unit testing infrastructure (#4646)

This commit is contained in:
Starlight220
2022-12-02 18:40:14 +02:00
committed by GitHub
parent 74cc86c4c5
commit 66bb0ffb2c
15 changed files with 444 additions and 13 deletions

View 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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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};
};

View File

@@ -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};
};

View File

@@ -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
}
]

View 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;
}

View File

@@ -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());
}

View File

@@ -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

View File

@@ -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.",

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}