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..5c5e3ba2e59 --- /dev/null +++ b/wpilibc/src/test/python/drive/test_differential_drive.py @@ -0,0 +1,435 @@ +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..7f2b6111358 --- /dev/null +++ b/wpilibc/src/test/python/driverstation/joystick_test_macros.py @@ -0,0 +1,44 @@ +import pytest + + +def axis_test(controller_class, sim_class, axis_name): + joy = controller_class(2) + joysim = sim_class(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_class, sim_class, btn_name): + joy = controller_class(1) + joysim = sim_class(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..6e89f30016c --- /dev/null +++ b/wpilibc/src/test/python/driverstation/test_driver_station.py @@ -0,0 +1,68 @@ +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 diff --git a/wpilibc/src/test/python/test_generic_hid.py b/wpilibc/src/test/python/driverstation/test_generic_hid.py similarity index 100% rename from wpilibc/src/test/python/test_generic_hid.py rename to wpilibc/src/test/python/driverstation/test_generic_hid.py 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..3ad88b89617 --- /dev/null +++ b/wpilibc/src/test/python/driverstation/test_nids_ps4_controller.py @@ -0,0 +1,37 @@ +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") 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..afa8b6e6ea3 --- /dev/null +++ b/wpilibc/src/test/python/driverstation/test_nids_ps5_controller.py @@ -0,0 +1,37 @@ +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") 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..da1ac1cf7dc --- /dev/null +++ b/wpilibc/src/test/python/driverstation/test_nids_xbox_controller.py @@ -0,0 +1,32 @@ +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") 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..84175071aa9 --- /dev/null +++ b/wpilibc/src/test/python/event/test_network_boolean_event.py @@ -0,0 +1,41 @@ +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 + + def on_high() -> None: + nonlocal counter + counter += 1 + + pub = nt_inst.getTable("TestTable").getBooleanTopic("Test").publish() + + NetworkBooleanEvent(loop, nt_inst, "TestTable", "Test").ifHigh(on_high) + + pub.set(False) + loop.poll() + assert counter == 0 + + pub.set(True) + loop.poll() + assert counter == 1 + + pub.set(False) + loop.poll() + assert counter == 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..870445f081d --- /dev/null +++ b/wpilibc/src/test/python/framework/test_timed_robot.py @@ -0,0 +1,400 @@ +import threading + +import pytest + +from wpilib import TimedRobot +from wpilib.simulation import ( + DriverStationSim, + pauseTiming, + resumeTiming, + setProgramStarted, + stepTiming, + waitForProgramStart, +) +from hal 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 + + def on_periodic() -> None: + nonlocal callback_count + callback_count += 1 + + robot.addPeriodic(on_periodic, _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 + + stepTiming(_PERIOD / 2.0) + + assert robot.disabled_init_count == 0 + assert robot.disabled_periodic_count == 0 + assert callback_count == 1 + + stepTiming(_PERIOD / 2.0) + + assert robot.disabled_init_count == 1 + assert robot.disabled_periodic_count == 1 + assert callback_count == 2 + + robot.endCompetition() + robot_thread.join() + + +def test_add_periodic_with_offset(): + robot = MockRobot() + callback_count = 0 + + def on_periodic() -> None: + nonlocal callback_count + callback_count += 1 + + robot.addPeriodic(on_periodic, _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 + + stepTiming(_PERIOD * 3.0 / 8.0) + + assert robot.disabled_init_count == 0 + assert robot.disabled_periodic_count == 0 + assert callback_count == 0 + + stepTiming(_PERIOD * 3.0 / 8.0) + + assert robot.disabled_init_count == 0 + assert robot.disabled_periodic_count == 0 + assert callback_count == 1 + + stepTiming(_PERIOD / 4.0) + + assert robot.disabled_init_count == 1 + assert robot.disabled_periodic_count == 1 + assert callback_count == 1 + + stepTiming(_PERIOD / 4.0) + + assert robot.disabled_init_count == 1 + assert robot.disabled_periodic_count == 1 + assert callback_count == 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..910db62a559 --- /dev/null +++ b/wpilibc/src/test/python/framework/test_timeslice_robot.py @@ -0,0 +1,95 @@ +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 + + def callback1() -> None: + nonlocal callback_count1 + callback_count1 += 1 + + def callback2() -> None: + nonlocal callback_count2 + callback_count2 += 1 + + robot.schedule(callback1, 0.0005) + robot.schedule(callback2, 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 + assert callback_count2 == 0 + + # Step to 1.5 ms into next period — nothing yet + stepTiming(0.0015) + assert robot.robot_periodic_count == 0 + assert callback_count1 == 0 + assert callback_count2 == 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 == 1 + assert callback_count2 == 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 == 1 + assert callback_count2 == 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/test_addressable_led_buffer.py b/wpilibc/src/test/python/hardware/led/test_addressable_led_buffer.py similarity index 100% rename from wpilibc/src/test/python/test_addressable_led_buffer.py rename to wpilibc/src/test/python/hardware/led/test_addressable_led_buffer.py diff --git a/wpilibc/src/test/python/test_led_pattern.py b/wpilibc/src/test/python/hardware/led/test_led_pattern.py similarity index 100% rename from wpilibc/src/test/python/test_led_pattern.py rename to wpilibc/src/test/python/hardware/led/test_led_pattern.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_watchdog.py b/wpilibc/src/test/python/system/test_watchdog.py new file mode 100644 index 00000000000..bcd4eba7491 --- /dev/null +++ b/wpilibc/src/test/python/system/test_watchdog.py @@ -0,0 +1,157 @@ +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 + + def on_expire() -> None: + nonlocal counter + counter += 1 + + watchdog = Watchdog(0.4, on_expire) + + # Run 1: disable before timeout + watchdog.enable() + stepTiming(0.2) + watchdog.disable() + assert counter == 0 + + # Run 2: step past timeout + counter = 0 + watchdog.enable() + stepTiming(0.4) + watchdog.disable() + assert counter == 1 + + # Run 3: step well past timeout, only triggers once + counter = 0 + watchdog.enable() + stepTiming(1.0) + watchdog.disable() + assert counter == 1 + + +def test_reset(): + counter = 0 + + def on_expire() -> None: + nonlocal counter + counter += 1 + + watchdog = Watchdog(0.4, on_expire) + + watchdog.enable() + stepTiming(0.2) + watchdog.reset() + stepTiming(0.2) + watchdog.disable() + + assert counter == 0 + + +def test_set_timeout(): + counter = 0 + + def on_expire() -> None: + nonlocal counter + counter += 1 + + watchdog = Watchdog(1.0, on_expire) + + watchdog.enable() + stepTiming(0.2) + watchdog.setTimeout(0.2) + + assert watchdog.getTimeout() == pytest.approx(0.2) + assert counter == 0 + + stepTiming(0.3) + watchdog.disable() + + assert counter == 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 + + def on_expire() -> None: + nonlocal counter + counter += 1 + + watchdog = Watchdog(0.4, on_expire) + + # 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 + + # 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 + + +def test_multi_watchdog(): + counter1 = 0 + counter2 = 0 + + def on_expire1() -> None: + nonlocal counter1 + counter1 += 1 + + def on_expire2() -> None: + nonlocal counter2 + counter2 += 1 + + watchdog1 = Watchdog(0.2, on_expire1) + watchdog2 = Watchdog(0.6, on_expire2) + + watchdog2.enable() + stepTiming(0.25) + assert counter1 == 0 + assert counter2 == 0 + + # watchdog1 enabled later but has shorter timeout — expires first + watchdog1.enable() + stepTiming(0.25) + watchdog1.disable() + watchdog2.disable() + + assert counter1 == 1 + assert counter2 == 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?