[wpilib] Remove InterruptableSensorBase and replace with interrupt classes (#2410)

This commit is contained in:
Thad House
2021-06-05 11:25:21 -07:00
committed by GitHub
parent 15c521a7fe
commit 5c817082a0
28 changed files with 982 additions and 1497 deletions

View File

@@ -8,7 +8,6 @@ import static org.hamcrest.Matchers.both;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import edu.wpi.first.wpilibj.test.AbstractComsSetup;
@@ -20,34 +19,31 @@ import org.junit.Test;
/**
* This class should not be run as a test explicitly. Instead it should be extended by tests that
* use the InterruptableSensorBase.
* use DigitalSource.
*/
public abstract class AbstractInterruptTest extends AbstractComsSetup {
private InterruptableSensorBase m_interruptable = null;
private DigitalSource m_source = null;
private InterruptableSensorBase getInterruptable() {
if (m_interruptable == null) {
m_interruptable = giveInterruptableSensorBase();
private DigitalSource getSource() {
if (m_source == null) {
m_source = giveSource();
}
return m_interruptable;
return m_source;
}
@After
public void interruptTeardown() {
if (m_interruptable != null) {
freeInterruptableSensorBase();
m_interruptable = null;
if (m_source != null) {
freeSource();
m_source = null;
}
}
/** Give the interruptable sensor base that interrupts can be attached to. */
abstract InterruptableSensorBase giveInterruptableSensorBase();
/** Give the sensor source that interrupts can be attached to. */
abstract DigitalSource giveSource();
/**
* Cleans up the interruptable sensor base. This is only called if {@link
* #giveInterruptableSensorBase()} is called.
*/
abstract void freeInterruptableSensorBase();
/** Cleans up the sensor source. This is only called if {@link #giveSource()} is called. */
abstract void freeSource();
/** Perform whatever action is required to set the interrupt high. */
abstract void setInterruptHigh();
@@ -55,116 +51,78 @@ public abstract class AbstractInterruptTest extends AbstractComsSetup {
/** Perform whatever action is required to set the interrupt low. */
abstract void setInterruptLow();
private class InterruptCounter {
private final AtomicInteger m_count = new AtomicInteger();
void increment() {
m_count.addAndGet(1);
}
int getCount() {
return m_count.get();
}
}
private class TestInterruptHandlerFunction extends InterruptHandlerFunction<InterruptCounter> {
protected final AtomicBoolean m_exceptionThrown = new AtomicBoolean(false);
/** Stores the time that the interrupt fires. */
final AtomicLong m_interruptFireTime = new AtomicLong();
/** Stores if the interrupt has completed at least once. */
final AtomicBoolean m_interruptComplete = new AtomicBoolean(false);
protected Exception m_ex;
final InterruptCounter m_counter;
TestInterruptHandlerFunction(InterruptCounter counter) {
m_counter = counter;
}
@Override
public void interruptFired(int interruptAssertedMask, InterruptCounter param) {
m_interruptFireTime.set(RobotController.getFPGATime());
m_counter.increment();
try {
// This won't cause the test to fail
assertSame(m_counter, param);
} catch (Exception ex) {
// So we must throw the exception within the test
m_exceptionThrown.set(true);
m_ex = ex;
}
m_interruptComplete.set(true);
}
@Override
public InterruptCounter overridableParameter() {
return m_counter;
}
}
@Test(timeout = 1000)
public void testSingleInterruptsTriggering() throws Exception {
// Given
final InterruptCounter counter = new InterruptCounter();
TestInterruptHandlerFunction function = new TestInterruptHandlerFunction(counter);
// final InterruptCounter counter = new InterruptCounter();
// TestInterruptHandlerFunction function = new
// TestInterruptHandlerFunction(counter);
// When
getInterruptable().requestInterrupts(function);
getInterruptable().enableInterrupts();
AtomicBoolean hasFired = new AtomicBoolean(false);
AtomicInteger counter = new AtomicInteger(0);
AtomicLong interruptFireTime = new AtomicLong();
setInterruptLow();
Timer.delay(0.01);
// Note: Utility.getFPGATime() is used because double values can turn over
// after the robot has been running for a long time
final long interruptTriggerTime = RobotController.getFPGATime();
setInterruptHigh();
try (AsynchronousInterrupt interrupt =
new AsynchronousInterrupt(
getSource(),
(a, b) -> {
interruptFireTime.set(RobotController.getFPGATime());
hasFired.set(true);
counter.incrementAndGet();
})) {
interrupt.enable();
setInterruptLow();
Timer.delay(0.01);
final long interruptTriggerTime = RobotController.getFPGATime();
setInterruptHigh();
while (!hasFired.get()) {
Timer.delay(0.005);
}
// Delay until the interrupt is complete
while (!function.m_interruptComplete.get()) {
Timer.delay(0.005);
assertEquals("The interrupt did not fire the expected number of times", 1, counter.get());
final long range = 10000; // in microseconds
assertThat(
"The interrupt did not fire within the expected time period (values in milliseconds)",
interruptFireTime.get(),
both(greaterThan(interruptTriggerTime - range))
.and(lessThan(interruptTriggerTime + range)));
assertThat(
"The readRisingTimestamp() did not return the correct value (values in seconds)",
interrupt.getRisingTimestamp(),
both(greaterThan((interruptTriggerTime - range) / 1e6))
.and(lessThan((interruptTriggerTime + range) / 1e6)));
}
// Then
assertEquals("The interrupt did not fire the expected number of times", 1, counter.getCount());
// If the test within the interrupt failed
if (function.m_exceptionThrown.get()) {
throw function.m_ex;
}
final long range = 10000; // in microseconds
assertThat(
"The interrupt did not fire within the expected time period (values in milliseconds)",
function.m_interruptFireTime.get(),
both(greaterThan(interruptTriggerTime - range))
.and(lessThan(interruptTriggerTime + range)));
assertThat(
"The readRisingTimestamp() did not return the correct value (values in seconds)",
getInterruptable().readRisingTimestamp(),
both(greaterThan((interruptTriggerTime - range) / 1e6))
.and(lessThan((interruptTriggerTime + range) / 1e6)));
}
@Test(timeout = 2000)
public void testMultipleInterruptsTriggering() {
// Given
final InterruptCounter counter = new InterruptCounter();
TestInterruptHandlerFunction function = new TestInterruptHandlerFunction(counter);
// When
getInterruptable().requestInterrupts(function);
getInterruptable().enableInterrupts();
AtomicBoolean hasFired = new AtomicBoolean(false);
AtomicInteger counter = new AtomicInteger(0);
final int fireCount = 50;
for (int i = 0; i < fireCount; i++) {
setInterruptLow();
setInterruptHigh();
// Wait for the interrupt to complete before moving on
while (!function.m_interruptComplete.getAndSet(false)) {
Timer.delay(0.005);
try (AsynchronousInterrupt interrupt =
new AsynchronousInterrupt(
getSource(),
(a, b) -> {
hasFired.set(true);
counter.incrementAndGet();
})) {
interrupt.enable();
final int fireCount = 50;
for (int i = 0; i < fireCount; i++) {
setInterruptLow();
setInterruptHigh();
// Wait for the interrupt to complete before moving on
while (!hasFired.getAndSet(false)) {
Timer.delay(0.005);
}
}
// Then
assertEquals(
"The interrupt did not fire the expected number of times", fireCount, counter.get());
}
// Then
assertEquals(
"The interrupt did not fire the expected number of times", fireCount, counter.getCount());
}
/** The timeout length for this test in seconds. */
@@ -172,131 +130,128 @@ public abstract class AbstractInterruptTest extends AbstractComsSetup {
@Test(timeout = (long) (synchronousTimeout * 1e3))
public void testSynchronousInterruptsTriggering() {
// Given
getInterruptable().requestInterrupts();
final double synchronousDelay = synchronousTimeout / 2.0;
final Runnable runnable =
() -> {
Timer.delay(synchronousDelay);
setInterruptLow();
setInterruptHigh();
};
try (SynchronousInterrupt interrupt = new SynchronousInterrupt(getSource())) {
final double synchronousDelay = synchronousTimeout / 2.0;
final Runnable runnable =
() -> {
Timer.delay(synchronousDelay);
setInterruptLow();
setInterruptHigh();
};
// When
// When
// Note: the long time value is used because doubles can flip if the robot
// is left running for long enough
final long startTimeStamp = RobotController.getFPGATime();
new Thread(runnable).start();
// Delay for twice as long as the timeout so the test should fail first
getInterruptable().waitForInterrupt(synchronousTimeout * 2);
final long stopTimeStamp = RobotController.getFPGATime();
// Note: the long time value is used because doubles can flip if the robot
// is left running for long enough
final long startTimeStamp = RobotController.getFPGATime();
new Thread(runnable).start();
// Delay for twice as long as the timeout so the test should fail first
interrupt.waitForInterrupt(synchronousTimeout * 2);
final long stopTimeStamp = RobotController.getFPGATime();
// Then
// The test will not have timed out and:
final double interruptRunTime = (stopTimeStamp - startTimeStamp) * 1e-6;
assertEquals(
"The interrupt did not run for the expected amount of time (units in seconds)",
synchronousDelay,
interruptRunTime,
0.1);
// Then
// The test will not have timed out and:
final double interruptRunTime = (stopTimeStamp - startTimeStamp) * 1e-6;
assertEquals(
"The interrupt did not run for the expected amount of time (units in seconds)",
synchronousDelay,
interruptRunTime,
0.1);
}
}
@Test(timeout = (long) (synchronousTimeout * 1e3))
public void testSynchronousInterruptsWaitResultTimeout() {
// Given
getInterruptable().requestInterrupts();
try (SynchronousInterrupt interrupt = new SynchronousInterrupt(getSource())) {
SynchronousInterrupt.WaitResult result = interrupt.waitForInterrupt(synchronousTimeout / 2);
// Don't fire interrupt. Expect it to timeout.
InterruptableSensorBase.WaitResult result =
getInterruptable().waitForInterrupt(synchronousTimeout / 2);
assertEquals(
"The interrupt did not time out correctly.",
result,
InterruptableSensorBase.WaitResult.kTimeout);
assertEquals(
"The interrupt did not time out correctly.",
result,
SynchronousInterrupt.WaitResult.kTimeout);
}
}
@Test(timeout = (long) (synchronousTimeout * 1e3))
public void testSynchronousInterruptsWaitResultRisingEdge() {
// Given
getInterruptable().requestInterrupts();
try (SynchronousInterrupt interrupt = new SynchronousInterrupt(getSource())) {
final double synchronousDelay = synchronousTimeout / 2.0;
final Runnable runnable =
() -> {
Timer.delay(synchronousDelay);
setInterruptLow();
setInterruptHigh();
};
final double synchronousDelay = synchronousTimeout / 2.0;
final Runnable runnable =
() -> {
Timer.delay(synchronousDelay);
setInterruptLow();
setInterruptHigh();
};
new Thread(runnable).start();
// Delay for twice as long as the timeout so the test should fail first
InterruptableSensorBase.WaitResult result =
getInterruptable().waitForInterrupt(synchronousTimeout * 2);
new Thread(runnable).start();
// Delay for twice as long as the timeout so the test should fail first
SynchronousInterrupt.WaitResult result = interrupt.waitForInterrupt(synchronousTimeout * 2);
assertEquals(
"The interrupt did not fire on the rising edge.",
result,
InterruptableSensorBase.WaitResult.kRisingEdge);
assertEquals(
"The interrupt did not fire on the rising edge.",
result,
SynchronousInterrupt.WaitResult.kRisingEdge);
}
}
@Test(timeout = (long) (synchronousTimeout * 1e3))
public void testSynchronousInterruptsWaitResultFallingEdge() {
// Given
getInterruptable().requestInterrupts();
getInterruptable().setUpSourceEdge(false, true);
try (SynchronousInterrupt interrupt = new SynchronousInterrupt(getSource())) {
// Given
interrupt.setInterruptEdges(false, true);
final double synchronousDelay = synchronousTimeout / 2.0;
final Runnable runnable =
() -> {
Timer.delay(synchronousDelay);
setInterruptHigh();
setInterruptLow();
};
final double synchronousDelay = synchronousTimeout / 2.0;
final Runnable runnable =
() -> {
Timer.delay(synchronousDelay);
setInterruptHigh();
setInterruptLow();
};
new Thread(runnable).start();
// Delay for twice as long as the timeout so the test should fail first
InterruptableSensorBase.WaitResult result =
getInterruptable().waitForInterrupt(synchronousTimeout * 2);
new Thread(runnable).start();
// Delay for twice as long as the timeout so the test should fail first
SynchronousInterrupt.WaitResult result = interrupt.waitForInterrupt(synchronousTimeout * 2);
assertEquals(
"The interrupt did not fire on the falling edge.",
result,
InterruptableSensorBase.WaitResult.kFallingEdge);
assertEquals(
"The interrupt did not fire on the falling edge.",
result,
SynchronousInterrupt.WaitResult.kFallingEdge);
}
}
@Test(timeout = 4000)
public void testDisableStopsInterruptFiring() {
final InterruptCounter counter = new InterruptCounter();
TestInterruptHandlerFunction function = new TestInterruptHandlerFunction(counter);
// When
getInterruptable().requestInterrupts(function);
getInterruptable().enableInterrupts();
final int fireCount = 50;
for (int i = 0; i < fireCount; i++) {
setInterruptLow();
setInterruptHigh();
// Wait for the interrupt to complete before moving on
while (!function.m_interruptComplete.getAndSet(false)) {
AtomicBoolean interruptComplete = new AtomicBoolean(false);
AtomicInteger counter = new AtomicInteger(0);
try (AsynchronousInterrupt interrupt =
new AsynchronousInterrupt(
getSource(),
(a, b) -> {
interruptComplete.set(true);
counter.incrementAndGet();
})) {
interrupt.enable();
final int fireCount = 50;
for (int i = 0; i < fireCount; i++) {
setInterruptLow();
setInterruptHigh();
// Wait for the interrupt to complete before moving on
while (!interruptComplete.getAndSet(false)) {
Timer.delay(0.005);
}
}
interrupt.disable();
for (int i = 0; i < fireCount; i++) {
setInterruptLow();
setInterruptHigh();
// Just wait because the interrupt should not fire
Timer.delay(0.005);
}
}
getInterruptable().disableInterrupts();
// TestBench.out().println("Finished disabling the robot");
for (int i = 0; i < fireCount; i++) {
setInterruptLow();
setInterruptHigh();
// Just wait because the interrupt should not fire
Timer.delay(0.005);
assertEquals(
"The interrupt did not fire the expected number of times", fireCount, counter.get());
}
// Then
assertEquals(
"The interrupt did not fire the expected number of times", fireCount, counter.getCount());
}
}

View File

@@ -139,10 +139,10 @@ public class AnalogCrossConnectTest extends AbstractInterruptTest {
* (non-Javadoc)
*
* @see
* edu.wpi.first.wpilibj.AbstractInterruptTest#giveInterruptableSensorBase()
* edu.wpi.first.wpilibj.AbstractInterruptTest#giveSource()
*/
@Override
InterruptableSensorBase giveInterruptableSensorBase() {
DigitalSource giveSource() {
m_interruptTrigger = new AnalogTrigger(analogIO.getInput());
m_interruptTrigger.setLimitsVoltage(2.0, 3.0);
m_interruptTriggerOutput =
@@ -154,11 +154,10 @@ public class AnalogCrossConnectTest extends AbstractInterruptTest {
* (non-Javadoc)
*
* @see
* edu.wpi.first.wpilibj.AbstractInterruptTest#freeInterruptableSensorBase()
* edu.wpi.first.wpilibj.AbstractInterruptTest#freeSource()
*/
@Override
void freeInterruptableSensorBase() {
m_interruptTriggerOutput.cancelInterrupts();
void freeSource() {
m_interruptTriggerOutput.close();
m_interruptTriggerOutput = null;
m_interruptTrigger.close();

View File

@@ -101,10 +101,11 @@ public class DIOCrossConnectTest extends AbstractInterruptTest {
dio.getOutput().enablePWM(0.0);
Timer.delay(0.5);
dio.getOutput().updateDutyCycle(0.5);
dio.getInput().requestInterrupts();
dio.getInput().setUpSourceEdge(false, true);
// TODO: Add return value from WaitForInterrupt
dio.getInput().waitForInterrupt(3.0, true);
try (var interruptHandler = new SynchronousInterrupt(dio.getInput())) {
interruptHandler.setInterruptEdges(false, true);
// TODO: Add return value from WaitForInterrupt
interruptHandler.waitForInterrupt(3.0, true);
}
Timer.delay(0.5);
final boolean firstCycle = dio.getInput().get();
Timer.delay(0.5);
@@ -140,10 +141,10 @@ public class DIOCrossConnectTest extends AbstractInterruptTest {
* (non-Javadoc)
*
* @see
* edu.wpi.first.wpilibj.AbstractInterruptTest#giveInterruptableSensorBase()
* edu.wpi.first.wpilibj.AbstractInterruptTest#giveSource()
*/
@Override
InterruptableSensorBase giveInterruptableSensorBase() {
DigitalSource giveSource() {
return dio.getInput();
}
@@ -151,10 +152,10 @@ public class DIOCrossConnectTest extends AbstractInterruptTest {
* (non-Javadoc)
*
* @see
* edu.wpi.first.wpilibj.AbstractInterruptTest#freeInterruptableSensorBase()
* edu.wpi.first.wpilibj.AbstractInterruptTest#freeSource()
*/
@Override
void freeInterruptableSensorBase() {
void freeSource() {
// Handled in the fixture
}

View File

@@ -61,11 +61,6 @@ public class DIOCrossConnectFixture implements ITestFixture {
@Override
public boolean reset() {
try {
m_input.cancelInterrupts();
} catch (IllegalStateException ex) {
// This will happen if the interrupt has not been allocated for this test.
}
m_output.set(false);
return true;
}