diff --git a/wpilibc/src/test/python/drive/test_differential_drive.py b/wpilibc/src/test/python/drive/test_differential_drive.py new file mode 100644 index 00000000000..b148a144949 --- /dev/null +++ b/wpilibc/src/test/python/drive/test_differential_drive.py @@ -0,0 +1,434 @@ +import pytest + +from wpilib import DifferentialDrive + +class MockMotorController: + def __init__(self): + self.throttle = 0 + + def setThrottle(self, throttle): + self.throttle = throttle + + def getThrottle(self): + return self.throttle + + +def test_arcade_drive_ik(): + # Forward + v = DifferentialDrive.arcadeDriveIK(1.0, 0.0, False) + assert v.left == pytest.approx(1.0) + assert v.right == pytest.approx(1.0) + + # Forward left turn + v = DifferentialDrive.arcadeDriveIK(0.5, 0.5, False) + assert v.left == pytest.approx(0.0) + assert v.right == pytest.approx(0.5) + + # Forward right turn + v = DifferentialDrive.arcadeDriveIK(0.5, -0.5, False) + assert v.left == pytest.approx(0.5) + assert v.right == pytest.approx(0.0) + + # Backward + v = DifferentialDrive.arcadeDriveIK(-1.0, 0.0, False) + assert v.left == pytest.approx(-1.0) + assert v.right == pytest.approx(-1.0) + + # Backward left turn + v = DifferentialDrive.arcadeDriveIK(-0.5, 0.5, False) + assert v.left == pytest.approx(-0.5) + assert v.right == pytest.approx(0.0) + + # Backward right turn + v = DifferentialDrive.arcadeDriveIK(-0.5, -0.5, False) + assert v.left == pytest.approx(0.0) + assert v.right == pytest.approx(-0.5) + + # Left turn (negative sign xSpeed) + v = DifferentialDrive.arcadeDriveIK(-0.0, 1.0, False) + assert v.left == pytest.approx(-1.0) + assert v.right == pytest.approx(1.0) + + # Left turn (positive sign xSpeed) + v = DifferentialDrive.arcadeDriveIK(0.0, 1.0, False) + assert v.left == pytest.approx(-1.0) + assert v.right == pytest.approx(1.0) + + # Right turn (negative sign xSpeed) + v = DifferentialDrive.arcadeDriveIK(-0.0, -1.0, False) + assert v.left == pytest.approx(1.0) + assert v.right == pytest.approx(-1.0) + + # Right turn (positive sign xSpeed) + v = DifferentialDrive.arcadeDriveIK(0.0, -1.0, False) + assert v.left == pytest.approx(1.0) + assert v.right == pytest.approx(-1.0) + + +def test_arcade_drive_ik_squared(): + # Forward + v = DifferentialDrive.arcadeDriveIK(1.0, 0.0, True) + assert v.left == pytest.approx(1.0) + assert v.right == pytest.approx(1.0) + + # Forward left turn + v = DifferentialDrive.arcadeDriveIK(0.5, 0.5, True) + assert v.left == pytest.approx(0.0) + assert v.right == pytest.approx(0.25) + + # Forward right turn + v = DifferentialDrive.arcadeDriveIK(0.5, -0.5, True) + assert v.left == pytest.approx(0.25) + assert v.right == pytest.approx(0.0) + + # Backward + v = DifferentialDrive.arcadeDriveIK(-1.0, 0.0, True) + assert v.left == pytest.approx(-1.0) + assert v.right == pytest.approx(-1.0) + + # Backward left turn + v = DifferentialDrive.arcadeDriveIK(-0.5, 0.5, True) + assert v.left == pytest.approx(-0.25) + assert v.right == pytest.approx(0.0) + + # Backward right turn + v = DifferentialDrive.arcadeDriveIK(-0.5, -0.5, True) + assert v.left == pytest.approx(0.0) + assert v.right == pytest.approx(-0.25) + + # Rotation-only cases same as unsquared + v = DifferentialDrive.arcadeDriveIK(-0.0, 1.0, False) + assert v.left == pytest.approx(-1.0) + assert v.right == pytest.approx(1.0) + + v = DifferentialDrive.arcadeDriveIK(0.0, 1.0, False) + assert v.left == pytest.approx(-1.0) + assert v.right == pytest.approx(1.0) + + v = DifferentialDrive.arcadeDriveIK(-0.0, -1.0, False) + assert v.left == pytest.approx(1.0) + assert v.right == pytest.approx(-1.0) + + v = DifferentialDrive.arcadeDriveIK(0.0, -1.0, False) + assert v.left == pytest.approx(1.0) + assert v.right == pytest.approx(-1.0) + + +def test_curvature_drive_ik(): + # Forward + v = DifferentialDrive.curvatureDriveIK(1.0, 0.0, False) + assert v.left == pytest.approx(1.0) + assert v.right == pytest.approx(1.0) + + # Forward left turn + v = DifferentialDrive.curvatureDriveIK(0.5, 0.5, False) + assert v.left == pytest.approx(0.25) + assert v.right == pytest.approx(0.75) + + # Forward right turn + v = DifferentialDrive.curvatureDriveIK(0.5, -0.5, False) + assert v.left == pytest.approx(0.75) + assert v.right == pytest.approx(0.25) + + # Backward + v = DifferentialDrive.curvatureDriveIK(-1.0, 0.0, False) + assert v.left == pytest.approx(-1.0) + assert v.right == pytest.approx(-1.0) + + # Backward left turn + v = DifferentialDrive.curvatureDriveIK(-0.5, 0.5, False) + assert v.left == pytest.approx(-0.75) + assert v.right == pytest.approx(-0.25) + + # Backward right turn + v = DifferentialDrive.curvatureDriveIK(-0.5, -0.5, False) + assert v.left == pytest.approx(-0.25) + assert v.right == pytest.approx(-0.75) + + +def test_curvature_drive_ik_turn_in_place(): + # Forward + v = DifferentialDrive.curvatureDriveIK(1.0, 0.0, True) + assert v.left == pytest.approx(1.0) + assert v.right == pytest.approx(1.0) + + # Forward left turn + v = DifferentialDrive.curvatureDriveIK(0.5, 0.5, True) + assert v.left == pytest.approx(0.0) + assert v.right == pytest.approx(1.0) + + # Forward right turn + v = DifferentialDrive.curvatureDriveIK(0.5, -0.5, True) + assert v.left == pytest.approx(1.0) + assert v.right == pytest.approx(0.0) + + # Backward + v = DifferentialDrive.curvatureDriveIK(-1.0, 0.0, True) + assert v.left == pytest.approx(-1.0) + assert v.right == pytest.approx(-1.0) + + # Backward left turn + v = DifferentialDrive.curvatureDriveIK(-0.5, 0.5, True) + assert v.left == pytest.approx(-1.0) + assert v.right == pytest.approx(0.0) + + # Backward right turn + v = DifferentialDrive.curvatureDriveIK(-0.5, -0.5, True) + assert v.left == pytest.approx(0.0) + assert v.right == pytest.approx(-1.0) + + +def test_tank_drive_ik(): + v = DifferentialDrive.tankDriveIK(1.0, 1.0, False) + assert v.left == pytest.approx(1.0) + assert v.right == pytest.approx(1.0) + + v = DifferentialDrive.tankDriveIK(0.5, 1.0, False) + assert v.left == pytest.approx(0.5) + assert v.right == pytest.approx(1.0) + + v = DifferentialDrive.tankDriveIK(1.0, 0.5, False) + assert v.left == pytest.approx(1.0) + assert v.right == pytest.approx(0.5) + + v = DifferentialDrive.tankDriveIK(-1.0, -1.0, False) + assert v.left == pytest.approx(-1.0) + assert v.right == pytest.approx(-1.0) + + v = DifferentialDrive.tankDriveIK(-0.5, -1.0, False) + assert v.left == pytest.approx(-0.5) + assert v.right == pytest.approx(-1.0) + + v = DifferentialDrive.tankDriveIK(-0.5, 1.0, False) + assert v.left == pytest.approx(-0.5) + assert v.right == pytest.approx(1.0) + + +def test_tank_drive_ik_squared(): + v = DifferentialDrive.tankDriveIK(1.0, 1.0, True) + assert v.left == pytest.approx(1.0) + assert v.right == pytest.approx(1.0) + + v = DifferentialDrive.tankDriveIK(0.5, 1.0, True) + assert v.left == pytest.approx(0.25) + assert v.right == pytest.approx(1.0) + + v = DifferentialDrive.tankDriveIK(1.0, 0.5, True) + assert v.left == pytest.approx(1.0) + assert v.right == pytest.approx(0.25) + + v = DifferentialDrive.tankDriveIK(-1.0, -1.0, True) + assert v.left == pytest.approx(-1.0) + assert v.right == pytest.approx(-1.0) + + v = DifferentialDrive.tankDriveIK(-0.5, -1.0, True) + assert v.left == pytest.approx(-0.25) + assert v.right == pytest.approx(-1.0) + + v = DifferentialDrive.tankDriveIK(-1.0, -0.5, True) + assert v.left == pytest.approx(-1.0) + assert v.right == pytest.approx(-0.25) + + +def test_arcade_drive(wpilib_state): + left = MockMotorController() + right = MockMotorController() + drive = DifferentialDrive( + left.setThrottle, + right.setThrottle, + ) + drive.setDeadband(0.0) + + drive.arcadeDrive(1.0, 0.0, False) + assert left.getThrottle() == pytest.approx(1.0) + assert right.getThrottle() == pytest.approx(1.0) + + drive.arcadeDrive(0.5, 0.5, False) + assert left.getThrottle() == pytest.approx(0.0) + assert right.getThrottle() == pytest.approx(0.5) + + drive.arcadeDrive(0.5, -0.5, False) + assert left.getThrottle() == pytest.approx(0.5) + assert right.getThrottle() == pytest.approx(0.0) + + drive.arcadeDrive(-1.0, 0.0, False) + assert left.getThrottle() == pytest.approx(-1.0) + assert right.getThrottle() == pytest.approx(-1.0) + + drive.arcadeDrive(-0.5, 0.5, False) + assert left.getThrottle() == pytest.approx(-0.5) + assert right.getThrottle() == pytest.approx(0.0) + + drive.arcadeDrive(-0.5, -0.5, False) + assert left.getThrottle() == pytest.approx(0.0) + assert right.getThrottle() == pytest.approx(-0.5) + + +def test_arcade_drive_squared(wpilib_state): + left = MockMotorController() + right = MockMotorController() + drive = DifferentialDrive( + left.setThrottle, + right.setThrottle, + ) + drive.setDeadband(0.0) + + drive.arcadeDrive(1.0, 0.0, True) + assert left.getThrottle() == pytest.approx(1.0) + assert right.getThrottle() == pytest.approx(1.0) + + drive.arcadeDrive(0.5, 0.5, True) + assert left.getThrottle() == pytest.approx(0.0) + assert right.getThrottle() == pytest.approx(0.25) + + drive.arcadeDrive(0.5, -0.5, True) + assert left.getThrottle() == pytest.approx(0.25) + assert right.getThrottle() == pytest.approx(0.0) + + drive.arcadeDrive(-1.0, 0.0, True) + assert left.getThrottle() == pytest.approx(-1.0) + assert right.getThrottle() == pytest.approx(-1.0) + + drive.arcadeDrive(-0.5, 0.5, True) + assert left.getThrottle() == pytest.approx(-0.25) + assert right.getThrottle() == pytest.approx(0.0) + + drive.arcadeDrive(-0.5, -0.5, True) + assert left.getThrottle() == pytest.approx(0.0) + assert right.getThrottle() == pytest.approx(-0.25) + + +def test_curvature_drive(wpilib_state): + left = MockMotorController() + right = MockMotorController() + drive = DifferentialDrive( + left.setThrottle, + right.setThrottle, + ) + drive.setDeadband(0.0) + + drive.curvatureDrive(1.0, 0.0, False) + assert left.getThrottle() == pytest.approx(1.0) + assert right.getThrottle() == pytest.approx(1.0) + + drive.curvatureDrive(0.5, 0.5, False) + assert left.getThrottle() == pytest.approx(0.25) + assert right.getThrottle() == pytest.approx(0.75) + + drive.curvatureDrive(0.5, -0.5, False) + assert left.getThrottle() == pytest.approx(0.75) + assert right.getThrottle() == pytest.approx(0.25) + + drive.curvatureDrive(-1.0, 0.0, False) + assert left.getThrottle() == pytest.approx(-1.0) + assert right.getThrottle() == pytest.approx(-1.0) + + drive.curvatureDrive(-0.5, 0.5, False) + assert left.getThrottle() == pytest.approx(-0.75) + assert right.getThrottle() == pytest.approx(-0.25) + + drive.curvatureDrive(-0.5, -0.5, False) + assert left.getThrottle() == pytest.approx(-0.25) + assert right.getThrottle() == pytest.approx(-0.75) + + +def test_curvature_drive_turn_in_place(wpilib_state): + left = MockMotorController() + right = MockMotorController() + drive = DifferentialDrive( + left.setThrottle, + right.setThrottle, + ) + drive.setDeadband(0.0) + + drive.curvatureDrive(1.0, 0.0, True) + assert left.getThrottle() == pytest.approx(1.0) + assert right.getThrottle() == pytest.approx(1.0) + + drive.curvatureDrive(0.5, 0.5, True) + assert left.getThrottle() == pytest.approx(0.0) + assert right.getThrottle() == pytest.approx(1.0) + + drive.curvatureDrive(0.5, -0.5, True) + assert left.getThrottle() == pytest.approx(1.0) + assert right.getThrottle() == pytest.approx(0.0) + + drive.curvatureDrive(-1.0, 0.0, True) + assert left.getThrottle() == pytest.approx(-1.0) + assert right.getThrottle() == pytest.approx(-1.0) + + drive.curvatureDrive(-0.5, 0.5, True) + assert left.getThrottle() == pytest.approx(-1.0) + assert right.getThrottle() == pytest.approx(0.0) + + drive.curvatureDrive(-0.5, -0.5, True) + assert left.getThrottle() == pytest.approx(0.0) + assert right.getThrottle() == pytest.approx(-1.0) + + +def test_tank_drive(wpilib_state): + left = MockMotorController() + right = MockMotorController() + drive = DifferentialDrive( + left.setThrottle, + right.setThrottle, + ) + drive.setDeadband(0.0) + + drive.tankDrive(1.0, 1.0, False) + assert left.getThrottle() == pytest.approx(1.0) + assert right.getThrottle() == pytest.approx(1.0) + + drive.tankDrive(0.5, 1.0, False) + assert left.getThrottle() == pytest.approx(0.5) + assert right.getThrottle() == pytest.approx(1.0) + + drive.tankDrive(1.0, 0.5, False) + assert left.getThrottle() == pytest.approx(1.0) + assert right.getThrottle() == pytest.approx(0.5) + + drive.tankDrive(-1.0, -1.0, False) + assert left.getThrottle() == pytest.approx(-1.0) + assert right.getThrottle() == pytest.approx(-1.0) + + drive.tankDrive(-0.5, -1.0, False) + assert left.getThrottle() == pytest.approx(-0.5) + assert right.getThrottle() == pytest.approx(-1.0) + + drive.tankDrive(-0.5, 1.0, False) + assert left.getThrottle() == pytest.approx(-0.5) + assert right.getThrottle() == pytest.approx(1.0) + + +def test_tank_drive_squared(wpilib_state): + left = MockMotorController() + right = MockMotorController() + drive = DifferentialDrive( + left.setThrottle, + right.setThrottle, + ) + drive.setDeadband(0.0) + + drive.tankDrive(1.0, 1.0, True) + assert left.getThrottle() == pytest.approx(1.0) + assert right.getThrottle() == pytest.approx(1.0) + + drive.tankDrive(0.5, 1.0, True) + assert left.getThrottle() == pytest.approx(0.25) + assert right.getThrottle() == pytest.approx(1.0) + + drive.tankDrive(1.0, 0.5, True) + assert left.getThrottle() == pytest.approx(1.0) + assert right.getThrottle() == pytest.approx(0.25) + + drive.tankDrive(-1.0, -1.0, True) + assert left.getThrottle() == pytest.approx(-1.0) + assert right.getThrottle() == pytest.approx(-1.0) + + drive.tankDrive(-0.5, -1.0, True) + assert left.getThrottle() == pytest.approx(-0.25) + assert right.getThrottle() == pytest.approx(-1.0) + + drive.tankDrive(-1.0, -0.5, True) + assert left.getThrottle() == pytest.approx(-1.0) + assert right.getThrottle() == pytest.approx(-0.25) diff --git a/wpilibc/src/test/python/drive/test_mecanum_drive.py b/wpilibc/src/test/python/drive/test_mecanum_drive.py new file mode 100644 index 00000000000..871e02d0d8f --- /dev/null +++ b/wpilibc/src/test/python/drive/test_mecanum_drive.py @@ -0,0 +1,242 @@ +import math + +import pytest +from wpimath import Rotation2d + +from wpilib import MecanumDrive + + +class MockMotorController: + def __init__(self): + self.throttle = 0 + + def setThrottle(self, throttle): + self.throttle = throttle + + def getThrottle(self): + return self.throttle + + +def test_cartesian_ik(): + # Forward + v = MecanumDrive.driveCartesianIK(1.0, 0.0, 0.0) + assert v.frontLeft == pytest.approx(1.0) + assert v.frontRight == pytest.approx(1.0) + assert v.rearLeft == pytest.approx(1.0) + assert v.rearRight == pytest.approx(1.0) + + # Left + v = MecanumDrive.driveCartesianIK(0.0, -1.0, 0.0) + assert v.frontLeft == pytest.approx(-1.0) + assert v.frontRight == pytest.approx(1.0) + assert v.rearLeft == pytest.approx(1.0) + assert v.rearRight == pytest.approx(-1.0) + + # Right + v = MecanumDrive.driveCartesianIK(0.0, 1.0, 0.0) + assert v.frontLeft == pytest.approx(1.0) + assert v.frontRight == pytest.approx(-1.0) + assert v.rearLeft == pytest.approx(-1.0) + assert v.rearRight == pytest.approx(1.0) + + # Rotate CCW + v = MecanumDrive.driveCartesianIK(0.0, 0.0, -1.0) + assert v.frontLeft == pytest.approx(-1.0) + assert v.frontRight == pytest.approx(1.0) + assert v.rearLeft == pytest.approx(-1.0) + assert v.rearRight == pytest.approx(1.0) + + # Rotate CW + v = MecanumDrive.driveCartesianIK(0.0, 0.0, 1.0) + assert v.frontLeft == pytest.approx(1.0) + assert v.frontRight == pytest.approx(-1.0) + assert v.rearLeft == pytest.approx(1.0) + assert v.rearRight == pytest.approx(-1.0) + + +def test_cartesian_ik_gyro_90_cw(): + gyro = Rotation2d.fromDegrees(90) + + # Forward in global frame; left in robot frame + v = MecanumDrive.driveCartesianIK(1.0, 0.0, 0.0, gyro) + assert v.frontLeft == pytest.approx(-1.0) + assert v.frontRight == pytest.approx(1.0) + assert v.rearLeft == pytest.approx(1.0) + assert v.rearRight == pytest.approx(-1.0) + + # Left in global frame; backward in robot frame + v = MecanumDrive.driveCartesianIK(0.0, -1.0, 0.0, gyro) + assert v.frontLeft == pytest.approx(-1.0) + assert v.frontRight == pytest.approx(-1.0) + assert v.rearLeft == pytest.approx(-1.0) + assert v.rearRight == pytest.approx(-1.0) + + # Right in global frame; forward in robot frame + v = MecanumDrive.driveCartesianIK(0.0, 1.0, 0.0, gyro) + assert v.frontLeft == pytest.approx(1.0) + assert v.frontRight == pytest.approx(1.0) + assert v.rearLeft == pytest.approx(1.0) + assert v.rearRight == pytest.approx(1.0) + + # Rotate CCW + v = MecanumDrive.driveCartesianIK(0.0, 0.0, -1.0, gyro) + assert v.frontLeft == pytest.approx(-1.0) + assert v.frontRight == pytest.approx(1.0) + assert v.rearLeft == pytest.approx(-1.0) + assert v.rearRight == pytest.approx(1.0) + + # Rotate CW + v = MecanumDrive.driveCartesianIK(0.0, 0.0, 1.0, gyro) + assert v.frontLeft == pytest.approx(1.0) + assert v.frontRight == pytest.approx(-1.0) + assert v.rearLeft == pytest.approx(1.0) + assert v.rearRight == pytest.approx(-1.0) + + +def test_cartesian(wpilib_state): + fl = MockMotorController() + rl = MockMotorController() + fr = MockMotorController() + rr = MockMotorController() + drive = MecanumDrive( + fl.setThrottle, + rl.setThrottle, + fr.setThrottle, + rr.setThrottle, + ) + drive.setDeadband(0.0) + + # Forward + drive.driveCartesian(1.0, 0.0, 0.0) + assert fl.getThrottle() == pytest.approx(1.0) + assert fr.getThrottle() == pytest.approx(1.0) + assert rl.getThrottle() == pytest.approx(1.0) + assert rr.getThrottle() == pytest.approx(1.0) + + # Left + drive.driveCartesian(0.0, -1.0, 0.0) + assert fl.getThrottle() == pytest.approx(-1.0) + assert fr.getThrottle() == pytest.approx(1.0) + assert rl.getThrottle() == pytest.approx(1.0) + assert rr.getThrottle() == pytest.approx(-1.0) + + # Right + drive.driveCartesian(0.0, 1.0, 0.0) + assert fl.getThrottle() == pytest.approx(1.0) + assert fr.getThrottle() == pytest.approx(-1.0) + assert rl.getThrottle() == pytest.approx(-1.0) + assert rr.getThrottle() == pytest.approx(1.0) + + # Rotate CCW + drive.driveCartesian(0.0, 0.0, -1.0) + assert fl.getThrottle() == pytest.approx(-1.0) + assert fr.getThrottle() == pytest.approx(1.0) + assert rl.getThrottle() == pytest.approx(-1.0) + assert rr.getThrottle() == pytest.approx(1.0) + + # Rotate CW + drive.driveCartesian(0.0, 0.0, 1.0) + assert fl.getThrottle() == pytest.approx(1.0) + assert fr.getThrottle() == pytest.approx(-1.0) + assert rl.getThrottle() == pytest.approx(1.0) + assert rr.getThrottle() == pytest.approx(-1.0) + + +def test_cartesian_gyro_90_cw(wpilib_state): + fl = MockMotorController() + rl = MockMotorController() + fr = MockMotorController() + rr = MockMotorController() + drive = MecanumDrive( + fl.setThrottle, + rl.setThrottle, + fr.setThrottle, + rr.setThrottle, + ) + drive.setDeadband(0.0) + + gyro = Rotation2d.fromDegrees(90) + + # Forward in global frame; left in robot frame + drive.driveCartesian(1.0, 0.0, 0.0, gyro) + assert fl.getThrottle() == pytest.approx(-1.0) + assert fr.getThrottle() == pytest.approx(1.0) + assert rl.getThrottle() == pytest.approx(1.0) + assert rr.getThrottle() == pytest.approx(-1.0) + + # Left in global frame; backward in robot frame + drive.driveCartesian(0.0, -1.0, 0.0, gyro) + assert fl.getThrottle() == pytest.approx(-1.0) + assert fr.getThrottle() == pytest.approx(-1.0) + assert rl.getThrottle() == pytest.approx(-1.0) + assert rr.getThrottle() == pytest.approx(-1.0) + + # Right in global frame; forward in robot frame + drive.driveCartesian(0.0, 1.0, 0.0, gyro) + assert fl.getThrottle() == pytest.approx(1.0) + assert fr.getThrottle() == pytest.approx(1.0) + assert rl.getThrottle() == pytest.approx(1.0) + assert rr.getThrottle() == pytest.approx(1.0) + + # Rotate CCW + drive.driveCartesian(0.0, 0.0, -1.0, gyro) + assert fl.getThrottle() == pytest.approx(-1.0) + assert fr.getThrottle() == pytest.approx(1.0) + assert rl.getThrottle() == pytest.approx(-1.0) + assert rr.getThrottle() == pytest.approx(1.0) + + # Rotate CW + drive.driveCartesian(0.0, 0.0, 1.0, gyro) + assert fl.getThrottle() == pytest.approx(1.0) + assert fr.getThrottle() == pytest.approx(-1.0) + assert rl.getThrottle() == pytest.approx(1.0) + assert rr.getThrottle() == pytest.approx(-1.0) + + +def test_polar(wpilib_state): + fl = MockMotorController() + rl = MockMotorController() + fr = MockMotorController() + rr = MockMotorController() + drive = MecanumDrive( + fl.setThrottle, + rl.setThrottle, + fr.setThrottle, + rr.setThrottle, + ) + drive.setDeadband(0.0) + + # Forward + drive.drivePolar(1.0, Rotation2d.fromDegrees(0), 0.0) + assert fl.getThrottle() == pytest.approx(1.0) + assert fr.getThrottle() == pytest.approx(1.0) + assert rl.getThrottle() == pytest.approx(1.0) + assert rr.getThrottle() == pytest.approx(1.0) + + # Left + drive.drivePolar(1.0, Rotation2d.fromDegrees(-90), 0.0) + assert fl.getThrottle() == pytest.approx(-1.0) + assert fr.getThrottle() == pytest.approx(1.0) + assert rl.getThrottle() == pytest.approx(1.0) + assert rr.getThrottle() == pytest.approx(-1.0) + + # Right + drive.drivePolar(1.0, Rotation2d.fromDegrees(90), 0.0) + assert fl.getThrottle() == pytest.approx(1.0) + assert fr.getThrottle() == pytest.approx(-1.0) + assert rl.getThrottle() == pytest.approx(-1.0) + assert rr.getThrottle() == pytest.approx(1.0) + + # Rotate CCW + drive.drivePolar(0.0, Rotation2d.fromDegrees(0), -1.0) + assert fl.getThrottle() == pytest.approx(-1.0) + assert fr.getThrottle() == pytest.approx(1.0) + assert rl.getThrottle() == pytest.approx(-1.0) + assert rr.getThrottle() == pytest.approx(1.0) + + # Rotate CW + drive.drivePolar(0.0, Rotation2d.fromDegrees(0), 1.0) + assert fl.getThrottle() == pytest.approx(1.0) + assert fr.getThrottle() == pytest.approx(-1.0) + assert rl.getThrottle() == pytest.approx(1.0) + assert rr.getThrottle() == pytest.approx(-1.0) diff --git a/wpilibc/src/test/python/driverstation/joystick_test_macros.py b/wpilibc/src/test/python/driverstation/joystick_test_macros.py new file mode 100644 index 00000000000..078088ea90b --- /dev/null +++ b/wpilibc/src/test/python/driverstation/joystick_test_macros.py @@ -0,0 +1,45 @@ +import pytest + + +def axis_test(controller_clazz, sim_clazz, axis_name): + joy = controller_clazz(2) + joysim = sim_clazz(joy) + + sim_setter_func = getattr(joysim, "set" + axis_name) + joy_get_func = getattr(joy, "get" + axis_name) + + sim_setter_func(0.35) + joysim.notifyNewData() + assert joy_get_func() == pytest.approx(0.35, abs=0.001) + + +def button_test(controller_clazz, sim_clazz, btn_name): + joy = controller_clazz(1) + joysim = sim_clazz(joy) + + joy_get_func = getattr(joy, "get" + btn_name) + joy_get_pressed_func = getattr(joy, "get" + btn_name + "Pressed") + joy_get_released_func = getattr(joy, "get" + btn_name + "Released") + + sim_setter_func = getattr(joysim, "set" + btn_name) + + sim_setter_func(False) + joysim.notifyNewData() + assert False == joy_get_func() + + # need to call pressed and released to clear flags + joy_get_pressed_func() + joy_get_released_func() + + sim_setter_func(True) + joysim.notifyNewData() + assert True == joy_get_func() + assert True == joy_get_pressed_func() + assert False == joy_get_released_func() + + + sim_setter_func(False) + joysim.notifyNewData() + assert False == joy_get_func() + assert False == joy_get_pressed_func() + assert True == joy_get_released_func() diff --git a/wpilibc/src/test/python/driverstation/test_driver_station.py b/wpilibc/src/test/python/driverstation/test_driver_station.py new file mode 100644 index 00000000000..e86cbb43663 --- /dev/null +++ b/wpilibc/src/test/python/driverstation/test_driver_station.py @@ -0,0 +1,53 @@ +import pytest + +from wpilib import DriverStationBackend, Joystick +from wpilib.simulation import DriverStationSim, stepTiming + + +@pytest.mark.parametrize( + "axes_max, buttons_max, povs_max, expected", + [ + (0, 0, 0, False), + (1, 0, 0, True), + (0, 1, 0, True), + (0, 0, 1, True), + (1, 1, 1, True), + (4, 10, 1, True), + ], +) +def test_is_joystick_connected(wpilib_state, axes_max, buttons_max, povs_max, expected): + DriverStationSim.setJoystickAxesMaximumIndex(1, axes_max) + DriverStationSim.setJoystickButtonsMaximumIndex(1, buttons_max) + DriverStationSim.setJoystickPOVsMaximumIndex(1, povs_max) + DriverStationSim.notifyNewData() + + assert DriverStationBackend.isJoystickConnected(1) == expected + + +@pytest.mark.parametrize( + "fms_attached, silenced, expected_silenced, expected_warning", + [ + (False, True, True, ""), + (False, False, False, "Warning: Joystick on port 0 not available, check if all controllers are plugged in"), + (True, True, False, "Warning: Joystick on port 0 not available, check if all controllers are plugged in"), # FMS overrides silence + (True, False, False, "Warning: Joystick on port 0 not available, check if all controllers are plugged in"), + ], +) +def test_joystick_connection_warnings( + wpilib_state, fms_attached, silenced, expected_silenced, expected_warning, capsys +): + DriverStationSim.setFmsAttached(fms_attached) + DriverStationSim.notifyNewData() + DriverStationBackend.silenceJoystickConnectionWarning(silenced) + + joystick = Joystick(0) + joystick.getRawButton(1) + + stepTiming(1.0) + assert ( + DriverStationBackend.isJoystickConnectionWarningSilenced() == expected_silenced + ) + + # Capture stderr to check for warnings + captured = capsys.readouterr() + assert expected_warning in captured.err \ No newline at end of file diff --git a/wpilibc/src/test/python/driverstation/test_generic_hid.py b/wpilibc/src/test/python/driverstation/test_generic_hid.py new file mode 100644 index 00000000000..23f7de6351d --- /dev/null +++ b/wpilibc/src/test/python/driverstation/test_generic_hid.py @@ -0,0 +1,78 @@ +import pytest + +from wpilib import GenericHID +from wpilib.simulation import GenericHIDSim + +RumbleType = GenericHID.RumbleType +_EPSILON = 0.0001 + + +def test_rumble_range(wpilib_state): + hid = GenericHID(0) + sim = GenericHIDSim(0) + + for i in range(101): + value = i / 100.0 + + hid.setRumble(RumbleType.LEFT_RUMBLE, value) + assert sim.getRumble(RumbleType.LEFT_RUMBLE) == pytest.approx(value, abs=_EPSILON) + + hid.setRumble(RumbleType.RIGHT_RUMBLE, value) + assert sim.getRumble(RumbleType.RIGHT_RUMBLE) == pytest.approx(value, abs=_EPSILON) + + hid.setRumble(RumbleType.LEFT_TRIGGER_RUMBLE, value) + assert sim.getRumble(RumbleType.LEFT_TRIGGER_RUMBLE) == pytest.approx( + value, abs=_EPSILON + ) + + hid.setRumble(RumbleType.RIGHT_TRIGGER_RUMBLE, value) + assert sim.getRumble(RumbleType.RIGHT_TRIGGER_RUMBLE) == pytest.approx( + value, abs=_EPSILON + ) + + +def test_rumble_types(wpilib_state): + hid = GenericHID(0) + sim = GenericHIDSim(0) + + # Make sure all are off + hid.setRumble(RumbleType.LEFT_RUMBLE, 0) + hid.setRumble(RumbleType.LEFT_TRIGGER_RUMBLE, 0) + hid.setRumble(RumbleType.RIGHT_RUMBLE, 0) + hid.setRumble(RumbleType.RIGHT_TRIGGER_RUMBLE, 0) + assert sim.getRumble(RumbleType.LEFT_RUMBLE) == pytest.approx(0, abs=_EPSILON) + assert sim.getRumble(RumbleType.LEFT_TRIGGER_RUMBLE) == pytest.approx(0, abs=_EPSILON) + assert sim.getRumble(RumbleType.RIGHT_RUMBLE) == pytest.approx(0, abs=_EPSILON) + assert sim.getRumble(RumbleType.RIGHT_TRIGGER_RUMBLE) == pytest.approx(0, abs=_EPSILON) + + # Left only + hid.setRumble(RumbleType.LEFT_RUMBLE, 1) + assert sim.getRumble(RumbleType.LEFT_RUMBLE) == pytest.approx(1, abs=_EPSILON) + assert sim.getRumble(RumbleType.RIGHT_RUMBLE) == pytest.approx(0, abs=_EPSILON) + assert sim.getRumble(RumbleType.LEFT_TRIGGER_RUMBLE) == pytest.approx(0, abs=_EPSILON) + assert sim.getRumble(RumbleType.RIGHT_TRIGGER_RUMBLE) == pytest.approx(0, abs=_EPSILON) + hid.setRumble(RumbleType.LEFT_RUMBLE, 0) + + # Right only + hid.setRumble(RumbleType.RIGHT_RUMBLE, 1) + assert sim.getRumble(RumbleType.LEFT_RUMBLE) == pytest.approx(0, abs=_EPSILON) + assert sim.getRumble(RumbleType.RIGHT_RUMBLE) == pytest.approx(1, abs=_EPSILON) + assert sim.getRumble(RumbleType.LEFT_TRIGGER_RUMBLE) == pytest.approx(0, abs=_EPSILON) + assert sim.getRumble(RumbleType.RIGHT_TRIGGER_RUMBLE) == pytest.approx(0, abs=_EPSILON) + hid.setRumble(RumbleType.RIGHT_RUMBLE, 0) + + # Left trigger only + hid.setRumble(RumbleType.LEFT_TRIGGER_RUMBLE, 1) + assert sim.getRumble(RumbleType.LEFT_RUMBLE) == pytest.approx(0, abs=_EPSILON) + assert sim.getRumble(RumbleType.RIGHT_RUMBLE) == pytest.approx(0, abs=_EPSILON) + assert sim.getRumble(RumbleType.LEFT_TRIGGER_RUMBLE) == pytest.approx(1, abs=_EPSILON) + assert sim.getRumble(RumbleType.RIGHT_TRIGGER_RUMBLE) == pytest.approx(0, abs=_EPSILON) + hid.setRumble(RumbleType.LEFT_TRIGGER_RUMBLE, 0) + + # Right trigger only + hid.setRumble(RumbleType.RIGHT_TRIGGER_RUMBLE, 1) + assert sim.getRumble(RumbleType.LEFT_RUMBLE) == pytest.approx(0, abs=_EPSILON) + assert sim.getRumble(RumbleType.RIGHT_RUMBLE) == pytest.approx(0, abs=_EPSILON) + assert sim.getRumble(RumbleType.LEFT_TRIGGER_RUMBLE) == pytest.approx(0, abs=_EPSILON) + assert sim.getRumble(RumbleType.RIGHT_TRIGGER_RUMBLE) == pytest.approx(1, abs=_EPSILON) + hid.setRumble(RumbleType.RIGHT_TRIGGER_RUMBLE, 0) diff --git a/wpilibc/src/test/python/test_joystick.py b/wpilibc/src/test/python/driverstation/test_joystick.py similarity index 96% rename from wpilibc/src/test/python/test_joystick.py rename to wpilibc/src/test/python/driverstation/test_joystick.py index f58dcbaa6b1..6d7f1a43096 100644 --- a/wpilibc/src/test/python/test_joystick.py +++ b/wpilibc/src/test/python/driverstation/test_joystick.py @@ -4,6 +4,11 @@ from wpilib.simulation import JoystickSim +def test_fast_deconstruction(wpilib_state): + # https://github.com/wpilibsuite/allwpilib/issues/1550 + Joystick(0) + + def test_getX() -> None: joy = Joystick(1) joysim = JoystickSim(joy) diff --git a/wpilibc/src/test/python/driverstation/test_nids_ps4_controller.py b/wpilibc/src/test/python/driverstation/test_nids_ps4_controller.py new file mode 100644 index 00000000000..2f149481a25 --- /dev/null +++ b/wpilibc/src/test/python/driverstation/test_nids_ps4_controller.py @@ -0,0 +1,38 @@ + + +from wpilib import NiDsPS4Controller +from wpilib.simulation import NiDsPS4ControllerSim +from driverstation.joystick_test_macros import button_test, axis_test + +def test_buttons(): + def ps4_button_test(btn_name): + button_test(NiDsPS4Controller, NiDsPS4ControllerSim, btn_name) + + def ps4_axis_test(axis_name): + axis_test(NiDsPS4Controller, NiDsPS4ControllerSim, axis_name) + + ps4_button_test("SquareButton") + ps4_button_test("CrossButton") + ps4_button_test("CircleButton") + ps4_button_test("TriangleButton") + + ps4_button_test("L1Button") + ps4_button_test("R1Button") + ps4_button_test("L2Button") + ps4_button_test("R2Button") + + ps4_button_test("ShareButton") + ps4_button_test("OptionsButton") + + ps4_button_test("L3Button") + ps4_button_test("R3Button") + + ps4_button_test("PSButton") + ps4_button_test("TouchpadButton") + + ps4_axis_test("LeftX") + ps4_axis_test("RightX") + ps4_axis_test("LeftY") + ps4_axis_test("RightY") + ps4_axis_test("L2Axis") + ps4_axis_test("R2Axis") \ No newline at end of file diff --git a/wpilibc/src/test/python/driverstation/test_nids_ps5_controller.py b/wpilibc/src/test/python/driverstation/test_nids_ps5_controller.py new file mode 100644 index 00000000000..26f0cafc181 --- /dev/null +++ b/wpilibc/src/test/python/driverstation/test_nids_ps5_controller.py @@ -0,0 +1,38 @@ + + +from wpilib import NiDsPS5Controller +from wpilib.simulation import NiDsPS5ControllerSim +from driverstation.joystick_test_macros import button_test, axis_test + +def test_buttons(): + def ps5_button_test(btn_name): + button_test(NiDsPS5Controller, NiDsPS5ControllerSim, btn_name) + + def ps5_axis_test(axis_name): + axis_test(NiDsPS5Controller, NiDsPS5ControllerSim, axis_name) + + ps5_button_test("SquareButton") + ps5_button_test("CrossButton") + ps5_button_test("CircleButton") + ps5_button_test("TriangleButton") + + ps5_button_test("L1Button") + ps5_button_test("R1Button") + ps5_button_test("L2Button") + ps5_button_test("R2Button") + + ps5_button_test("CreateButton") + ps5_button_test("OptionsButton") + + ps5_button_test("L3Button") + ps5_button_test("R3Button") + + ps5_button_test("PSButton") + ps5_button_test("TouchpadButton") + + ps5_axis_test("LeftX") + ps5_axis_test("RightX") + ps5_axis_test("LeftY") + ps5_axis_test("RightY") + ps5_axis_test("L2Axis") + ps5_axis_test("R2Axis") \ No newline at end of file diff --git a/wpilibc/src/test/python/driverstation/test_nids_xbox_controller.py b/wpilibc/src/test/python/driverstation/test_nids_xbox_controller.py new file mode 100644 index 00000000000..e7aa091a047 --- /dev/null +++ b/wpilibc/src/test/python/driverstation/test_nids_xbox_controller.py @@ -0,0 +1,33 @@ + + +from wpilib import NiDsXboxController +from wpilib.simulation import NiDsXboxControllerSim +from driverstation.joystick_test_macros import button_test, axis_test + +def test_buttons(): + def xbox_button_test(btn_name): + button_test(NiDsXboxController, NiDsXboxControllerSim, btn_name) + + def xbox_axis_test(axis_name): + axis_test(NiDsXboxController, NiDsXboxControllerSim, axis_name) + + xbox_button_test("LeftBumperButton") + xbox_button_test("RightBumperButton") + + xbox_button_test("LeftStickButton") + xbox_button_test("RightStickButton") + + xbox_button_test("AButton") + xbox_button_test("BButton") + xbox_button_test("XButton") + xbox_button_test("YButton") + xbox_button_test("BackButton") + xbox_button_test("StartButton") + + xbox_axis_test("LeftX") + xbox_axis_test("RightX") + xbox_axis_test("LeftY") + xbox_axis_test("RightY") + + xbox_axis_test("LeftTriggerAxis") + xbox_axis_test("RightTriggerAxis") \ No newline at end of file diff --git a/wpilibc/src/test/python/test_boolean_event.py b/wpilibc/src/test/python/event/test_boolean_event.py similarity index 100% rename from wpilibc/src/test/python/test_boolean_event.py rename to wpilibc/src/test/python/event/test_boolean_event.py diff --git a/wpilibc/src/test/python/event/test_event_loop.py b/wpilibc/src/test/python/event/test_event_loop.py new file mode 100644 index 00000000000..cddd4a910c7 --- /dev/null +++ b/wpilibc/src/test/python/event/test_event_loop.py @@ -0,0 +1,23 @@ +import pytest + +from wpilib import EventLoop + + +def test_concurrent_modification(): + loop = EventLoop() + + def bind_during_poll(): + with pytest.raises(RuntimeError): + loop.bind(lambda: None) + + loop.bind(bind_during_poll) + loop.poll() + + loop.clear() + + def clear_during_poll(): + with pytest.raises(RuntimeError): + loop.clear() + + loop.bind(clear_during_poll) + loop.poll() diff --git a/wpilibc/src/test/python/event/test_network_boolean_event.py b/wpilibc/src/test/python/event/test_network_boolean_event.py new file mode 100644 index 00000000000..749f772b83d --- /dev/null +++ b/wpilibc/src/test/python/event/test_network_boolean_event.py @@ -0,0 +1,39 @@ +import pytest + +from wpilib import EventLoop, NetworkBooleanEvent + + +@pytest.fixture +def nt_inst(wpilib_state): + import ntcore + + inst = ntcore.NetworkTableInstance.getDefault() + inst.startLocal() + try: + yield inst + finally: + inst.stopLocal() + inst._reset() + + +def test_set(nt_inst): + loop = EventLoop() + counter = [0] + + pub = nt_inst.getTable("TestTable").getBooleanTopic("Test").publish() + + NetworkBooleanEvent(loop, nt_inst, "TestTable", "Test").ifHigh( + lambda: counter.__setitem__(0, counter[0] + 1) + ) + + pub.set(False) + loop.poll() + assert counter[0] == 0 + + pub.set(True) + loop.poll() + assert counter[0] == 1 + + pub.set(False) + loop.poll() + assert counter[0] == 1 diff --git a/wpilibc/src/test/python/test_opmode_robot.py b/wpilibc/src/test/python/framework/test_opmode_robot.py similarity index 99% rename from wpilibc/src/test/python/test_opmode_robot.py rename to wpilibc/src/test/python/framework/test_opmode_robot.py index 1498ea11612..33bf3f304f6 100644 --- a/wpilibc/src/test/python/test_opmode_robot.py +++ b/wpilibc/src/test/python/framework/test_opmode_robot.py @@ -1,7 +1,6 @@ import pytest import threading from wpilib import simulation as wsim -from wpimath.units import seconds from wpilib.opmoderobot import OpModeRobot from wpilib import OpMode, RobotState from hal._wpiHal import RobotMode diff --git a/wpilibc/src/test/python/framework/test_timed_robot.py b/wpilibc/src/test/python/framework/test_timed_robot.py new file mode 100644 index 00000000000..68eb52cf869 --- /dev/null +++ b/wpilibc/src/test/python/framework/test_timed_robot.py @@ -0,0 +1,394 @@ +import threading + +import pytest + +from wpilib import TimedRobot +from wpilib.simulation import ( + DriverStationSim, + pauseTiming, + resumeTiming, + setProgramStarted, + stepTiming, + waitForProgramStart, +) +from hal._wpiHal import _RobotMode + +_PERIOD = 0.02 # 20 ms + + +@pytest.fixture(autouse=True) +def timed_robot_setup(wpilib_state): + pauseTiming() + setProgramStarted(False) + yield + resumeTiming() + + +class MockRobot(TimedRobot): + def __init__(self): + super().__init__(_PERIOD) + self.simulation_init_count = 0 + self.disabled_init_count = 0 + self.autonomous_init_count = 0 + self.teleop_init_count = 0 + self.utility_init_count = 0 + + self.disabled_exit_count = 0 + self.autonomous_exit_count = 0 + self.teleop_exit_count = 0 + self.utility_exit_count = 0 + + self.robot_periodic_count = 0 + self.simulation_periodic_count = 0 + self.disabled_periodic_count = 0 + self.autonomous_periodic_count = 0 + self.teleop_periodic_count = 0 + self.utility_periodic_count = 0 + + def simulationInit(self): + self.simulation_init_count += 1 + + def disabledInit(self): + self.disabled_init_count += 1 + + def autonomousInit(self): + self.autonomous_init_count += 1 + + def teleopInit(self): + self.teleop_init_count += 1 + + def utilityInit(self): + self.utility_init_count += 1 + + def robotPeriodic(self): + self.robot_periodic_count += 1 + + def simulationPeriodic(self): + self.simulation_periodic_count += 1 + + def disabledPeriodic(self): + self.disabled_periodic_count += 1 + + def autonomousPeriodic(self): + self.autonomous_periodic_count += 1 + + def teleopPeriodic(self): + self.teleop_periodic_count += 1 + + def utilityPeriodic(self): + self.utility_periodic_count += 1 + + def disabledExit(self): + self.disabled_exit_count += 1 + + def autonomousExit(self): + self.autonomous_exit_count += 1 + + def teleopExit(self): + self.teleop_exit_count += 1 + + def utilityExit(self): + self.utility_exit_count += 1 + + +def test_disabled_mode(): + robot = MockRobot() + robot_thread = threading.Thread(target=robot.startCompetition, daemon=True) + robot_thread.start() + waitForProgramStart() + + DriverStationSim.setEnabled(False) + DriverStationSim.notifyNewData() + + assert robot.simulation_init_count == 1 + assert robot.disabled_init_count == 0 + assert robot.robot_periodic_count == 0 + assert robot.simulation_periodic_count == 0 + assert robot.disabled_periodic_count == 0 + + stepTiming(_PERIOD) + + assert robot.simulation_init_count == 1 + assert robot.disabled_init_count == 1 + assert robot.autonomous_init_count == 0 + assert robot.teleop_init_count == 0 + assert robot.utility_init_count == 0 + + assert robot.robot_periodic_count == 1 + assert robot.simulation_periodic_count == 1 + assert robot.disabled_periodic_count == 1 + assert robot.autonomous_periodic_count == 0 + assert robot.teleop_periodic_count == 0 + assert robot.utility_periodic_count == 0 + + assert robot.disabled_exit_count == 0 + + stepTiming(_PERIOD) + + assert robot.robot_periodic_count == 2 + assert robot.simulation_periodic_count == 2 + assert robot.disabled_periodic_count == 2 + + robot.endCompetition() + robot_thread.join() + + +def test_autonomous_mode(): + robot = MockRobot() + robot_thread = threading.Thread(target=robot.startCompetition, daemon=True) + robot_thread.start() + waitForProgramStart() + + DriverStationSim.setEnabled(True) + DriverStationSim.setRobotMode(_RobotMode.AUTONOMOUS) + DriverStationSim.notifyNewData() + + assert robot.simulation_init_count == 1 + assert robot.disabled_init_count == 0 + assert robot.autonomous_init_count == 0 + assert robot.robot_periodic_count == 0 + + stepTiming(_PERIOD) + + assert robot.simulation_init_count == 1 + assert robot.disabled_init_count == 0 + assert robot.autonomous_init_count == 1 + assert robot.teleop_init_count == 0 + assert robot.utility_init_count == 0 + + assert robot.robot_periodic_count == 1 + assert robot.simulation_periodic_count == 1 + assert robot.disabled_periodic_count == 0 + assert robot.autonomous_periodic_count == 1 + assert robot.teleop_periodic_count == 0 + assert robot.utility_periodic_count == 0 + + assert robot.autonomous_exit_count == 0 + + stepTiming(_PERIOD) + + assert robot.robot_periodic_count == 2 + assert robot.simulation_periodic_count == 2 + assert robot.autonomous_periodic_count == 2 + + robot.endCompetition() + robot_thread.join() + + +def test_teleop_mode(): + robot = MockRobot() + robot_thread = threading.Thread(target=robot.startCompetition, daemon=True) + robot_thread.start() + waitForProgramStart() + + DriverStationSim.setEnabled(True) + DriverStationSim.setRobotMode(_RobotMode.TELEOPERATED) + DriverStationSim.notifyNewData() + + assert robot.simulation_init_count == 1 + assert robot.teleop_init_count == 0 + assert robot.robot_periodic_count == 0 + + stepTiming(_PERIOD) + + assert robot.disabled_init_count == 0 + assert robot.autonomous_init_count == 0 + assert robot.teleop_init_count == 1 + assert robot.utility_init_count == 0 + + assert robot.robot_periodic_count == 1 + assert robot.simulation_periodic_count == 1 + assert robot.disabled_periodic_count == 0 + assert robot.autonomous_periodic_count == 0 + assert robot.teleop_periodic_count == 1 + assert robot.utility_periodic_count == 0 + + stepTiming(_PERIOD) + + assert robot.robot_periodic_count == 2 + assert robot.teleop_periodic_count == 2 + + robot.endCompetition() + robot_thread.join() + + +def test_utility_mode(): + robot = MockRobot() + robot_thread = threading.Thread(target=robot.startCompetition, daemon=True) + robot_thread.start() + waitForProgramStart() + + DriverStationSim.setEnabled(True) + DriverStationSim.setRobotMode(_RobotMode.UTILITY) + DriverStationSim.notifyNewData() + + assert robot.simulation_init_count == 1 + assert robot.utility_init_count == 0 + assert robot.robot_periodic_count == 0 + + stepTiming(_PERIOD) + + assert robot.utility_init_count == 1 + assert robot.robot_periodic_count == 1 + assert robot.utility_periodic_count == 1 + + stepTiming(_PERIOD) + + assert robot.robot_periodic_count == 2 + assert robot.utility_periodic_count == 2 + + DriverStationSim.setEnabled(False) + DriverStationSim.notifyNewData() + stepTiming(_PERIOD) + + assert robot.disabled_init_count == 1 + assert robot.robot_periodic_count == 3 + assert robot.disabled_periodic_count == 1 + assert robot.utility_exit_count == 1 + + robot.endCompetition() + robot_thread.join() + + +def test_mode_change(): + robot = MockRobot() + robot_thread = threading.Thread(target=robot.startCompetition, daemon=True) + robot_thread.start() + waitForProgramStart() + + DriverStationSim.setEnabled(False) + DriverStationSim.notifyNewData() + + assert robot.disabled_init_count == 0 + assert robot.disabled_exit_count == 0 + + stepTiming(_PERIOD) + + assert robot.disabled_init_count == 1 + assert robot.disabled_exit_count == 0 + + DriverStationSim.setEnabled(True) + DriverStationSim.setRobotMode(_RobotMode.AUTONOMOUS) + DriverStationSim.notifyNewData() + stepTiming(_PERIOD) + + assert robot.disabled_init_count == 1 + assert robot.autonomous_init_count == 1 + assert robot.teleop_init_count == 0 + assert robot.utility_init_count == 0 + assert robot.disabled_exit_count == 1 + assert robot.autonomous_exit_count == 0 + + DriverStationSim.setRobotMode(_RobotMode.TELEOPERATED) + DriverStationSim.notifyNewData() + stepTiming(_PERIOD) + + assert robot.autonomous_init_count == 1 + assert robot.teleop_init_count == 1 + assert robot.utility_init_count == 0 + assert robot.autonomous_exit_count == 1 + assert robot.teleop_exit_count == 0 + + DriverStationSim.setRobotMode(_RobotMode.UTILITY) + DriverStationSim.notifyNewData() + stepTiming(_PERIOD) + + assert robot.utility_init_count == 1 + assert robot.teleop_exit_count == 1 + assert robot.utility_exit_count == 0 + + DriverStationSim.setEnabled(False) + DriverStationSim.notifyNewData() + stepTiming(_PERIOD) + + assert robot.disabled_init_count == 2 + assert robot.autonomous_init_count == 1 + assert robot.teleop_init_count == 1 + assert robot.utility_init_count == 1 + assert robot.disabled_exit_count == 1 + assert robot.autonomous_exit_count == 1 + assert robot.teleop_exit_count == 1 + assert robot.utility_exit_count == 1 + + robot.endCompetition() + robot_thread.join() + + +def test_add_periodic(): + robot = MockRobot() + callback_count = [0] + robot.addPeriodic(lambda: callback_count.__setitem__(0, callback_count[0] + 1), _PERIOD / 2.0) + + robot_thread = threading.Thread(target=robot.startCompetition, daemon=True) + robot_thread.start() + waitForProgramStart() + + DriverStationSim.setEnabled(False) + DriverStationSim.notifyNewData() + + assert robot.disabled_init_count == 0 + assert robot.disabled_periodic_count == 0 + assert callback_count[0] == 0 + + stepTiming(_PERIOD / 2.0) + + assert robot.disabled_init_count == 0 + assert robot.disabled_periodic_count == 0 + assert callback_count[0] == 1 + + stepTiming(_PERIOD / 2.0) + + assert robot.disabled_init_count == 1 + assert robot.disabled_periodic_count == 1 + assert callback_count[0] == 2 + + robot.endCompetition() + robot_thread.join() + + +def test_add_periodic_with_offset(): + robot = MockRobot() + callback_count = [0] + robot.addPeriodic( + lambda: callback_count.__setitem__(0, callback_count[0] + 1), + _PERIOD / 2.0, + _PERIOD / 4.0, + ) + + robot_thread = threading.Thread(target=robot.startCompetition, daemon=True) + robot_thread.start() + waitForProgramStart() + + DriverStationSim.setEnabled(False) + DriverStationSim.notifyNewData() + + assert robot.disabled_init_count == 0 + assert robot.disabled_periodic_count == 0 + assert callback_count[0] == 0 + + stepTiming(_PERIOD * 3.0 / 8.0) + + assert robot.disabled_init_count == 0 + assert robot.disabled_periodic_count == 0 + assert callback_count[0] == 0 + + stepTiming(_PERIOD * 3.0 / 8.0) + + assert robot.disabled_init_count == 0 + assert robot.disabled_periodic_count == 0 + assert callback_count[0] == 1 + + stepTiming(_PERIOD / 4.0) + + assert robot.disabled_init_count == 1 + assert robot.disabled_periodic_count == 1 + assert callback_count[0] == 1 + + stepTiming(_PERIOD / 4.0) + + assert robot.disabled_init_count == 1 + assert robot.disabled_periodic_count == 1 + assert callback_count[0] == 2 + + robot.endCompetition() + robot_thread.join() diff --git a/wpilibc/src/test/python/framework/test_timeslice_robot.py b/wpilibc/src/test/python/framework/test_timeslice_robot.py new file mode 100644 index 00000000000..95a1ceddeb9 --- /dev/null +++ b/wpilibc/src/test/python/framework/test_timeslice_robot.py @@ -0,0 +1,87 @@ +import threading + +import pytest + +from wpilib import TimesliceRobot +from wpilib.simulation import ( + DriverStationSim, + pauseTiming, + resumeTiming, + setProgramStarted, + stepTiming, + waitForProgramStart, +) + + +@pytest.fixture(autouse=True) +def timeslice_setup(wpilib_state): + pauseTiming() + setProgramStarted(False) + yield + resumeTiming() + + +class MockRobot(TimesliceRobot): + def __init__(self): + super().__init__(0.002, 0.005) + self.robot_periodic_count = 0 + + def robotPeriodic(self): + self.robot_periodic_count += 1 + + +def test_schedule(): + robot = MockRobot() + + callback_count1 = [0] + callback_count2 = [0] + + robot.schedule(lambda: callback_count1.__setitem__(0, callback_count1[0] + 1), 0.0005) + robot.schedule(lambda: callback_count2.__setitem__(0, callback_count2[0] + 1), 0.001) + + robot_thread = threading.Thread(target=robot.startCompetition, daemon=True) + robot_thread.start() + waitForProgramStart() + + DriverStationSim.setEnabled(False) + DriverStationSim.notifyNewData() + + # First 5 ms: no callbacks fired yet (delayed by one period) + stepTiming(0.005) + assert robot.robot_periodic_count == 0 + assert callback_count1[0] == 0 + assert callback_count2[0] == 0 + + # Step to 1.5 ms into next period — nothing yet + stepTiming(0.0015) + assert robot.robot_periodic_count == 0 + assert callback_count1[0] == 0 + assert callback_count2[0] == 0 + + # Step to 2.25 ms — callback1 fires (offset 2 ms, period 0.5 ms) + stepTiming(0.00075) + assert robot.robot_periodic_count == 0 + assert callback_count1[0] == 1 + assert callback_count2[0] == 0 + + # Step to 2.75 ms — callback2 fires (offset 2.5 ms, period 1 ms) + stepTiming(0.0005) + assert robot.robot_periodic_count == 0 + assert callback_count1[0] == 1 + assert callback_count2[0] == 1 + + robot.endCompetition() + robot_thread.join() + + +def test_schedule_overrun(): + robot = MockRobot() + + robot.schedule(lambda: None, 0.0005) + robot.schedule(lambda: None, 0.001) + + # offset = 2 ms + 0.5 ms + 1 ms = 3.5 ms; 3.5 ms + 3 ms = 6.5 ms > 5 ms max + with pytest.raises(RuntimeError): + robot.schedule(lambda: None, 0.003) + + robot.endCompetition() diff --git a/wpilibc/src/test/python/test_onboard_imu.py b/wpilibc/src/test/python/hardware/imu/test_onboard_imu.py similarity index 100% rename from wpilibc/src/test/python/test_onboard_imu.py rename to wpilibc/src/test/python/hardware/imu/test_onboard_imu.py diff --git a/wpilibc/src/test/python/hardware/pneumatic/test_double_solenoid.py b/wpilibc/src/test/python/hardware/pneumatic/test_double_solenoid.py new file mode 100644 index 00000000000..9df7f031952 --- /dev/null +++ b/wpilibc/src/test/python/hardware/pneumatic/test_double_solenoid.py @@ -0,0 +1,115 @@ +import pytest + +from wpilib import DoubleSolenoid, PneumaticsModuleType, Solenoid + + +def test_valid_initialization_ctre(wpilib_state): + solenoid = DoubleSolenoid(0, 3, PneumaticsModuleType.CTRE_PCM, 2, 3) + solenoid.set(DoubleSolenoid.Value.REVERSE) + assert solenoid.get() == DoubleSolenoid.Value.REVERSE + + solenoid.set(DoubleSolenoid.Value.FORWARD) + assert solenoid.get() == DoubleSolenoid.Value.FORWARD + + solenoid.set(DoubleSolenoid.Value.OFF) + assert solenoid.get() == DoubleSolenoid.Value.OFF + + +def test_throw_forward_port_already_initialized_ctre(wpilib_state): + s = Solenoid(0, 5, PneumaticsModuleType.CTRE_PCM, 2) + with pytest.raises(RuntimeError): + DoubleSolenoid(0, 5, PneumaticsModuleType.CTRE_PCM, 2, 3) + + +def test_throw_reverse_port_already_initialized_ctre(wpilib_state): + s = Solenoid(0, 6, PneumaticsModuleType.CTRE_PCM, 3) + with pytest.raises(RuntimeError): + DoubleSolenoid(0, 6, PneumaticsModuleType.CTRE_PCM, 2, 3) + + +def test_throw_both_ports_already_initialized_ctre(wpilib_state): + s0 = Solenoid(0, 6, PneumaticsModuleType.CTRE_PCM, 2) + s1 = Solenoid(0, 6, PneumaticsModuleType.CTRE_PCM, 3) + with pytest.raises(RuntimeError): + DoubleSolenoid(0, 6, PneumaticsModuleType.CTRE_PCM, 2, 3) + + +def test_toggle_ctre(wpilib_state): + solenoid = DoubleSolenoid(0, 4, PneumaticsModuleType.CTRE_PCM, 2, 3) + solenoid.set(DoubleSolenoid.Value.REVERSE) + + solenoid.toggle() + assert solenoid.get() == DoubleSolenoid.Value.FORWARD + + solenoid.toggle() + assert solenoid.get() == DoubleSolenoid.Value.REVERSE + + solenoid.set(DoubleSolenoid.Value.OFF) + solenoid.toggle() + assert solenoid.get() == DoubleSolenoid.Value.OFF + + +def test_invalid_forward_port_ctre(wpilib_state): + with pytest.raises(RuntimeError): + DoubleSolenoid(0, 0, PneumaticsModuleType.CTRE_PCM, 100, 1) + + +def test_invalid_reverse_port_ctre(wpilib_state): + with pytest.raises(RuntimeError): + DoubleSolenoid(0, 0, PneumaticsModuleType.CTRE_PCM, 0, 100) + + +def test_valid_initialization_rev(wpilib_state): + solenoid = DoubleSolenoid(0, 3, PneumaticsModuleType.REV_PH, 2, 3) + solenoid.set(DoubleSolenoid.Value.REVERSE) + assert solenoid.get() == DoubleSolenoid.Value.REVERSE + + solenoid.set(DoubleSolenoid.Value.FORWARD) + assert solenoid.get() == DoubleSolenoid.Value.FORWARD + + solenoid.set(DoubleSolenoid.Value.OFF) + assert solenoid.get() == DoubleSolenoid.Value.OFF + + +def test_throw_forward_port_already_initialized_rev(wpilib_state): + s = Solenoid(0, 5, PneumaticsModuleType.REV_PH, 2) + with pytest.raises(RuntimeError): + DoubleSolenoid(0, 5, PneumaticsModuleType.REV_PH, 2, 3) + + +def test_throw_reverse_port_already_initialized_rev(wpilib_state): + s = Solenoid(0, 6, PneumaticsModuleType.REV_PH, 3) + with pytest.raises(RuntimeError): + DoubleSolenoid(0, 6, PneumaticsModuleType.REV_PH, 2, 3) + + +def test_throw_both_ports_already_initialized_rev(wpilib_state): + s0 = Solenoid(0, 6, PneumaticsModuleType.REV_PH, 2) + s1 = Solenoid(0, 6, PneumaticsModuleType.REV_PH, 3) + with pytest.raises(RuntimeError): + DoubleSolenoid(0, 6, PneumaticsModuleType.REV_PH, 2, 3) + + +def test_toggle_rev(wpilib_state): + solenoid = DoubleSolenoid(0, 4, PneumaticsModuleType.REV_PH, 2, 3) + solenoid.set(DoubleSolenoid.Value.REVERSE) + + solenoid.toggle() + assert solenoid.get() == DoubleSolenoid.Value.FORWARD + + solenoid.toggle() + assert solenoid.get() == DoubleSolenoid.Value.REVERSE + + solenoid.set(DoubleSolenoid.Value.OFF) + solenoid.toggle() + assert solenoid.get() == DoubleSolenoid.Value.OFF + + +def test_invalid_forward_port_rev(wpilib_state): + with pytest.raises(RuntimeError): + DoubleSolenoid(0, 0, PneumaticsModuleType.REV_PH, 100, 1) + + +def test_invalid_reverse_port_rev(wpilib_state): + with pytest.raises(RuntimeError): + DoubleSolenoid(0, 0, PneumaticsModuleType.REV_PH, 0, 100) diff --git a/wpilibc/src/test/python/hardware/pneumatic/test_solenoid.py b/wpilibc/src/test/python/hardware/pneumatic/test_solenoid.py new file mode 100644 index 00000000000..21f63fdb834 --- /dev/null +++ b/wpilibc/src/test/python/hardware/pneumatic/test_solenoid.py @@ -0,0 +1,83 @@ +import pytest + +from wpilib import DoubleSolenoid, PneumaticsModuleType, Solenoid + + +def test_valid_initialization_ctre(wpilib_state): + solenoid = Solenoid(0, 3, PneumaticsModuleType.CTRE_PCM, 2) + assert solenoid.getChannel() == 2 + + solenoid.set(True) + assert solenoid.get() + + solenoid.set(False) + assert not solenoid.get() + + +def test_double_initialization_ctre(wpilib_state): + s = Solenoid(0, 3, PneumaticsModuleType.CTRE_PCM, 2) + with pytest.raises(RuntimeError): + Solenoid(0, 3, PneumaticsModuleType.CTRE_PCM, 2) + + +def test_double_initialization_from_double_solenoid_ctre(wpilib_state): + ds = DoubleSolenoid(0, 3, PneumaticsModuleType.CTRE_PCM, 2, 3) + with pytest.raises(RuntimeError): + Solenoid(0, 3, PneumaticsModuleType.CTRE_PCM, 2) + + +def test_invalid_channel_ctre(wpilib_state): + with pytest.raises(RuntimeError): + Solenoid(0, 3, PneumaticsModuleType.CTRE_PCM, 100) + + +def test_toggle_ctre(wpilib_state): + solenoid = Solenoid(0, 3, PneumaticsModuleType.CTRE_PCM, 2) + solenoid.set(True) + assert solenoid.get() + + solenoid.toggle() + assert not solenoid.get() + + solenoid.toggle() + assert solenoid.get() + + +def test_valid_initialization_rev(wpilib_state): + solenoid = Solenoid(0, 3, PneumaticsModuleType.REV_PH, 2) + assert solenoid.getChannel() == 2 + + solenoid.set(True) + assert solenoid.get() + + solenoid.set(False) + assert not solenoid.get() + + +def test_double_initialization_rev(wpilib_state): + s = Solenoid(0, 3, PneumaticsModuleType.REV_PH, 2) + with pytest.raises(RuntimeError): + Solenoid(0, 3, PneumaticsModuleType.REV_PH, 2) + + +def test_double_initialization_from_double_solenoid_rev(wpilib_state): + ds = DoubleSolenoid(0, 3, PneumaticsModuleType.REV_PH, 2, 3) + with pytest.raises(RuntimeError): + Solenoid(0, 3, PneumaticsModuleType.REV_PH, 2) + + +def test_invalid_channel_rev(wpilib_state): + with pytest.raises(RuntimeError): + Solenoid(0, 3, PneumaticsModuleType.REV_PH, 100) + + +def test_toggle_rev(wpilib_state): + solenoid = Solenoid(0, 3, PneumaticsModuleType.REV_PH, 2) + solenoid.set(True) + assert solenoid.get() + + solenoid.toggle() + assert not solenoid.get() + + solenoid.toggle() + assert solenoid.get() diff --git a/wpilibc/src/test/python/hardware/range/test_sharp_ir.py b/wpilibc/src/test/python/hardware/range/test_sharp_ir.py new file mode 100644 index 00000000000..fe2756caf6a --- /dev/null +++ b/wpilibc/src/test/python/hardware/range/test_sharp_ir.py @@ -0,0 +1,18 @@ +import pytest + +from wpilib import SharpIR +from wpilib.simulation import SharpIRSim + + +def test_sim_devices(wpilib_state): + s = SharpIR.GP2Y0A02YK0F(1) + sim = SharpIRSim(s) + + assert s.getRange() == pytest.approx(0.2) + + sim.setRange(0.3) + assert s.getRange() == pytest.approx(0.3) + + # Clamped to max range of 1.5 m for GP2Y0A02YK0F + sim.setRange(3.0) + assert s.getRange() == pytest.approx(1.5) diff --git a/wpilibc/src/test/python/hardware/rotation/test_analog_potentiometer.py b/wpilibc/src/test/python/hardware/rotation/test_analog_potentiometer.py new file mode 100644 index 00000000000..24dec806d3e --- /dev/null +++ b/wpilibc/src/test/python/hardware/rotation/test_analog_potentiometer.py @@ -0,0 +1,75 @@ +import pytest + +from wpilib import AnalogInput, AnalogPotentiometer +from wpilib.simulation import AnalogInputSim, RoboRioSim + + +def test_initialize_with_analog_input(wpilib_state): + ai = AnalogInput(0) + pot = AnalogPotentiometer(ai) + sim = AnalogInputSim(ai) + + RoboRioSim.resetData() + sim.setVoltage(2.8) + assert pot.get() == pytest.approx(2.8 / 3.3) + + +def test_initialize_with_analog_input_and_scale(wpilib_state): + ai = AnalogInput(0) + pot = AnalogPotentiometer(ai, 270.0) + RoboRioSim.resetData() + sim = AnalogInputSim(ai) + + sim.setVoltage(3.3) + assert pot.get() == pytest.approx(270.0) + + sim.setVoltage(2.5) + assert pot.get() == pytest.approx((2.5 / 3.3) * 270.0) + + sim.setVoltage(0.0) + assert pot.get() == pytest.approx(0.0) + + +def test_initialize_with_channel(wpilib_state): + pot = AnalogPotentiometer(1) + sim = AnalogInputSim(1) + + sim.setVoltage(3.3) + assert pot.get() == pytest.approx(1.0) + + +def test_initialize_with_channel_and_scale(wpilib_state): + pot = AnalogPotentiometer(1, 180.0) + RoboRioSim.resetData() + sim = AnalogInputSim(1) + + sim.setVoltage(3.3) + assert pot.get() == pytest.approx(180.0) + + sim.setVoltage(0.0) + assert pot.get() == pytest.approx(0.0) + + +def test_with_modified_battery_voltage(wpilib_state): + pot = AnalogPotentiometer(1, 180.0, 90.0) + RoboRioSim.resetData() + sim = AnalogInputSim(1) + + # Test at 3.3 V + sim.setVoltage(3.3) + assert pot.get() == pytest.approx(270.0) + + sim.setVoltage(0.0) + assert pot.get() == pytest.approx(90.0) + + # Simulate lower battery voltage + RoboRioSim.setUserVoltage3V3(2.5) + + sim.setVoltage(2.5) + assert pot.get() == pytest.approx(270.0) + + sim.setVoltage(2.0) + assert pot.get() == pytest.approx(234.0) + + sim.setVoltage(0.0) + assert pot.get() == pytest.approx(90.0) diff --git a/wpilibc/src/test/python/test_alert.py b/wpilibc/src/test/python/simulation/test_alert_sim.py similarity index 99% rename from wpilibc/src/test/python/test_alert.py rename to wpilibc/src/test/python/simulation/test_alert_sim.py index 7b8c61dd60d..e352b1e3e74 100644 --- a/wpilibc/src/test/python/test_alert.py +++ b/wpilibc/src/test/python/simulation/test_alert_sim.py @@ -10,10 +10,9 @@ @pytest.fixture(scope="function") def group_name(request): - + AlertSim.resetData() group_name = f"AlertTest_{request.node.name}" yield group_name - AlertSim.resetData() diff --git a/wpilibc/src/test/python/smartdashboard/test_mechanism2d.py b/wpilibc/src/test/python/smartdashboard/test_mechanism2d.py new file mode 100644 index 00000000000..5c63d877eb0 --- /dev/null +++ b/wpilibc/src/test/python/smartdashboard/test_mechanism2d.py @@ -0,0 +1,89 @@ +import pytest + +from ntcore import NetworkTableInstance +from wpilib import Mechanism2d, MechanismLigament2d, MechanismRoot2d, SmartDashboard +from wpiutil import Color8Bit + + +def test_create_mechanism(): + m = Mechanism2d(100, 100) + r1 = m.getRoot("r1", 10, 10) + l1 = r1.appendLigament("l1", 4, 3) + l2 = l1.appendLigament("l2", 4, 3) + assert l2 is not None + + +def test_canvas(nt: NetworkTableInstance): + mechanism = Mechanism2d(5, 10) + dims_entry = nt.getEntry("/SmartDashboard/mechanism/dims") + color_entry = nt.getEntry("/SmartDashboard/mechanism/backgroundColor") + + SmartDashboard.putData("mechanism", mechanism) + SmartDashboard.updateValues() + + dims = dims_entry.getDoubleArray([]) + assert dims[0] == pytest.approx(5.0) + assert dims[1] == pytest.approx(10.0) + assert color_entry.getString("") == "#000020" + + mechanism.setBackgroundColor(Color8Bit(255, 255, 255)) + SmartDashboard.updateValues() + assert color_entry.getString("") == "#FFFFFF" + + +def test_root(nt: NetworkTableInstance): + mechanism = Mechanism2d(5, 10) + x_entry = nt.getEntry("/SmartDashboard/mechanism/root/x") + y_entry = nt.getEntry("/SmartDashboard/mechanism/root/y") + + root = mechanism.getRoot("root", 1, 2) + SmartDashboard.putData("mechanism", mechanism) + SmartDashboard.updateValues() + + assert x_entry.getDouble(0.0) == pytest.approx(1.0) + assert y_entry.getDouble(0.0) == pytest.approx(2.0) + + root.setPosition(2, 4) + SmartDashboard.updateValues() + + assert x_entry.getDouble(0.0) == pytest.approx(2.0) + assert y_entry.getDouble(0.0) == pytest.approx(4.0) + + +def test_ligament(nt: NetworkTableInstance): + mechanism = Mechanism2d(5, 10) + angle_entry = nt.getEntry("/SmartDashboard/mechanism/root/ligament/angle") + color_entry = nt.getEntry("/SmartDashboard/mechanism/root/ligament/color") + length_entry = nt.getEntry("/SmartDashboard/mechanism/root/ligament/length") + weight_entry = nt.getEntry("/SmartDashboard/mechanism/root/ligament/weight") + + root = mechanism.getRoot("root", 1, 2) + ligament = root.appendLigament("ligament", 3, 90, 1, Color8Bit(255, 255, 255)) + SmartDashboard.putData("mechanism", mechanism) + + assert ligament.getAngle() == pytest.approx(angle_entry.getDouble(0.0)) + assert ligament.getColor().hexString() == color_entry.getString("") + assert ligament.getLength() == pytest.approx(length_entry.getDouble(0.0)) + assert ligament.getLineWeight() == pytest.approx(weight_entry.getDouble(0.0)) + + ligament.setAngle(45) + ligament.setColor(Color8Bit(0, 0, 0)) + ligament.setLength(2) + ligament.setLineWeight(4) + SmartDashboard.updateValues() + + assert ligament.getAngle() == pytest.approx(angle_entry.getDouble(0.0)) + assert ligament.getColor().hexString() == color_entry.getString("") + assert ligament.getLength() == pytest.approx(length_entry.getDouble(0.0)) + assert ligament.getLineWeight() == pytest.approx(weight_entry.getDouble(0.0)) + + angle_entry.setDouble(22.5) + color_entry.setString("#FF00FF") + length_entry.setDouble(4.0) + weight_entry.setDouble(6.0) + SmartDashboard.updateValues() + + assert ligament.getAngle() == pytest.approx(angle_entry.getDouble(0.0)) + assert ligament.getColor().hexString() == color_entry.getString("") + assert ligament.getLength() == pytest.approx(length_entry.getDouble(0.0)) + assert ligament.getLineWeight() == pytest.approx(weight_entry.getDouble(0.0)) diff --git a/wpilibc/src/test/python/test_sendable_chooser.py b/wpilibc/src/test/python/smartdashboard/test_sendable_chooser.py similarity index 100% rename from wpilibc/src/test/python/test_sendable_chooser.py rename to wpilibc/src/test/python/smartdashboard/test_sendable_chooser.py diff --git a/wpilibc/src/test/python/test_datalogmanager.py b/wpilibc/src/test/python/system/test_data_log_manager.py similarity index 100% rename from wpilibc/src/test/python/test_datalogmanager.py rename to wpilibc/src/test/python/system/test_data_log_manager.py diff --git a/wpilibc/src/test/python/test_notifier.py b/wpilibc/src/test/python/system/test_notifier.py similarity index 100% rename from wpilibc/src/test/python/test_notifier.py rename to wpilibc/src/test/python/system/test_notifier.py diff --git a/wpilibc/src/test/python/system/test_timer.py b/wpilibc/src/test/python/system/test_timer.py new file mode 100644 index 00000000000..c8cd048d630 --- /dev/null +++ b/wpilibc/src/test/python/system/test_timer.py @@ -0,0 +1,104 @@ +import pytest + +from wpilib import Timer +from wpilib.simulation import pauseTiming, restartTiming, resumeTiming, stepTiming + + +@pytest.fixture(autouse=True) +def timer_setup(): + pauseTiming() + restartTiming() + yield + resumeTiming() + + +def test_start_stop(): + timer = Timer() + + # Stopped timer doesn't advance + assert timer.get() == pytest.approx(0.0) + assert not timer.isRunning() + stepTiming(0.5) + assert timer.get() == pytest.approx(0.0) + assert not timer.isRunning() + + # Running timer advances + timer.start() + stepTiming(0.5) + assert timer.get() == pytest.approx(0.5) + assert timer.isRunning() + + # Stopped timer freezes + timer.stop() + stepTiming(0.5) + assert timer.get() == pytest.approx(0.5) + assert not timer.isRunning() + + +def test_reset(): + timer = Timer() + timer.start() + + assert timer.get() == pytest.approx(0.0) + stepTiming(0.5) + assert timer.get() == pytest.approx(0.5) + + timer.reset() + assert timer.get() == pytest.approx(0.0) + + stepTiming(0.5) + assert timer.get() == pytest.approx(0.5) + + # Reset while stopped keeps timer stopped + timer.stop() + timer.reset() + stepTiming(0.5) + assert timer.get() == pytest.approx(0.0) + + +def test_has_elapsed(): + timer = Timer() + + # 0 s elapsed since timer hasn't started + assert timer.hasElapsed(0.0) + + # Stopped timer doesn't accumulate elapsed time + stepTiming(0.5) + assert not timer.hasElapsed(0.4) + + timer.start() + + stepTiming(0.5) + assert timer.hasElapsed(0.4) + assert timer.hasElapsed(0.4) + + +def test_advance_if_elapsed(): + timer = Timer() + + # 0 s has elapsed + assert timer.advanceIfElapsed(0.0) + + # Stopped timer doesn't accumulate elapsed time + stepTiming(0.5) + assert not timer.advanceIfElapsed(0.4) + + timer.start() + + # First call returns True, second returns False (advanced past threshold) + stepTiming(0.5) + assert timer.advanceIfElapsed(0.4) + assert not timer.advanceIfElapsed(0.4) + + # After 1 more second, two 400 ms periods have elapsed + stepTiming(1.0) + assert timer.advanceIfElapsed(0.4) + assert timer.advanceIfElapsed(0.4) + assert not timer.advanceIfElapsed(0.4) + + +def test_get_monotonic_timestamp(): + start = Timer.getMonotonicTimestamp() + stepTiming(0.5) + end = Timer.getMonotonicTimestamp() + assert (end - start) == pytest.approx(0.5) diff --git a/wpilibc/src/test/python/system/test_watchdog.py b/wpilibc/src/test/python/system/test_watchdog.py new file mode 100644 index 00000000000..33f8e26ab69 --- /dev/null +++ b/wpilibc/src/test/python/system/test_watchdog.py @@ -0,0 +1,128 @@ +import pytest + +from wpilib import Watchdog +from wpilib.simulation import pauseTiming, resumeTiming, stepTiming + + +@pytest.fixture(autouse=True) +def watchdog_setup(): + pauseTiming() + yield + resumeTiming() + + +def test_enable_disable(): + counter = [0] + watchdog = Watchdog(0.4, lambda: counter.__setitem__(0, counter[0] + 1)) + + # Run 1: disable before timeout + watchdog.enable() + stepTiming(0.2) + watchdog.disable() + assert counter[0] == 0 + + # Run 2: step past timeout + counter[0] = 0 + watchdog.enable() + stepTiming(0.4) + watchdog.disable() + assert counter[0] == 1 + + # Run 3: step well past timeout, only triggers once + counter[0] = 0 + watchdog.enable() + stepTiming(1.0) + watchdog.disable() + assert counter[0] == 1 + + +def test_reset(): + counter = [0] + watchdog = Watchdog(0.4, lambda: counter.__setitem__(0, counter[0] + 1)) + + watchdog.enable() + stepTiming(0.2) + watchdog.reset() + stepTiming(0.2) + watchdog.disable() + + assert counter[0] == 0 + + +def test_set_timeout(): + counter = [0] + watchdog = Watchdog(1.0, lambda: counter.__setitem__(0, counter[0] + 1)) + + watchdog.enable() + stepTiming(0.2) + watchdog.setTimeout(0.2) + + assert watchdog.getTimeout() == pytest.approx(0.2) + assert counter[0] == 0 + + stepTiming(0.3) + watchdog.disable() + + assert counter[0] == 1 + + +def test_is_expired(): + watchdog = Watchdog(0.2, lambda: None) + assert not watchdog.isExpired() + watchdog.enable() + + assert not watchdog.isExpired() + stepTiming(0.3) + assert watchdog.isExpired() + + watchdog.disable() + assert watchdog.isExpired() + + watchdog.reset() + assert not watchdog.isExpired() + + +def test_epochs(): + counter = [0] + watchdog = Watchdog(0.4, lambda: counter.__setitem__(0, counter[0] + 1)) + + # Run 1: under timeout with epochs + watchdog.enable() + watchdog.addEpoch("Epoch 1") + stepTiming(0.1) + watchdog.addEpoch("Epoch 2") + stepTiming(0.1) + watchdog.addEpoch("Epoch 3") + watchdog.disable() + assert counter[0] == 0 + + # Run 2: reset mid-run keeps under timeout + watchdog.enable() + watchdog.addEpoch("Epoch 1") + stepTiming(0.2) + watchdog.reset() + stepTiming(0.2) + watchdog.addEpoch("Epoch 2") + watchdog.disable() + assert counter[0] == 0 + + +def test_multi_watchdog(): + counter1 = [0] + counter2 = [0] + watchdog1 = Watchdog(0.2, lambda: counter1.__setitem__(0, counter1[0] + 1)) + watchdog2 = Watchdog(0.6, lambda: counter2.__setitem__(0, counter2[0] + 1)) + + watchdog2.enable() + stepTiming(0.25) + assert counter1[0] == 0 + assert counter2[0] == 0 + + # watchdog1 enabled later but has shorter timeout — expires first + watchdog1.enable() + stepTiming(0.25) + watchdog1.disable() + watchdog2.disable() + + assert counter1[0] == 1 + assert counter2[0] == 0 diff --git a/wpilibc/src/test/python/test_mechanism2d.py b/wpilibc/src/test/python/test_mechanism2d.py deleted file mode 100644 index 82a9abb5eba..00000000000 --- a/wpilibc/src/test/python/test_mechanism2d.py +++ /dev/null @@ -1,11 +0,0 @@ -from wpilib import Mechanism2d - - -def test_create_mechanism(): - m = Mechanism2d(100, 100) - r1 = m.getRoot("r1", 10, 10) - l1 = r1.appendLigament("l1", 4, 3) - l2 = l1.appendLigament("l2", 4, 3) - assert l2 is not None - - # TODO... check that they do something?