/*----------------------------------------------------------------------------*/ /* Copyright (c) FIRST 2008-2016. All Rights Reserved. */ /* Open Source Software - may be modified and shared by FRC teams. The code */ /* must be accompanied by the FIRST BSD license file in the root directory of */ /* the project. */ /*----------------------------------------------------------------------------*/ package edu.wpi.first.wpilibj; 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 java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.junit.After; import org.junit.Test; import edu.wpi.first.wpilibj.test.AbstractComsSetup; /** * This class should not be run as a test explicitly. Instead it should be * extended by tests that use the InterruptableSensorBase * * @author jonathanleitschuh * */ public abstract class AbstractInterruptTest extends AbstractComsSetup { private InterruptableSensorBase interruptable = null; private InterruptableSensorBase getInterruptable() { if (interruptable == null) { interruptable = giveInterruptableSensorBase(); } return interruptable; } @After public void interruptTeardown() { if (interruptable != null) { freeInterruptableSensorBase(); interruptable = null; } } /** * Give the interruptible sensor base that interrupts can be attached to. *$ * @return */ abstract InterruptableSensorBase giveInterruptableSensorBase(); /** * Cleans up the interruptible sensor base. This is only called if * {@link #giveInterruptableSensorBase()} is called. */ abstract void freeInterruptableSensorBase(); /** * Perform whatever action is required to set the interrupt high. */ abstract void setInterruptHigh(); /** * Perform whatever action is required to set the interrupt low. */ abstract void setInterruptLow(); private class InterruptCounter { private final AtomicInteger count = new AtomicInteger(); void increment() { count.addAndGet(1); } int getCount() { return count.get(); } }; private class TestInterruptHandlerFunction extends InterruptHandlerFunction { protected final AtomicBoolean exceptionThrown = new AtomicBoolean(false); /** Stores the time that the interrupt fires */ final AtomicLong interruptFireTime = new AtomicLong(); /** Stores if the interrupt has completed at least once */ final AtomicBoolean interruptComplete = new AtomicBoolean(false); protected Exception ex; final InterruptCounter counter; TestInterruptHandlerFunction(InterruptCounter counter) { this.counter = counter; } @Override public void interruptFired(int interruptAssertedMask, InterruptCounter param) { interruptFireTime.set(Utility.getFPGATime()); counter.increment(); try { // This won't cause the test to fail assertSame(counter, param); } catch (Exception ex) { // So we must throw the exception within the test exceptionThrown.set(true); this.ex = ex; } interruptComplete.set(true); }; @Override public InterruptCounter overridableParameter() { return counter; } }; @Test(timeout = 1000) public void testSingleInterruptsTriggering() throws Exception { // Given final InterruptCounter counter = new InterruptCounter(); TestInterruptHandlerFunction function = new TestInterruptHandlerFunction(counter); // When getInterruptable().requestInterrupts(function); getInterruptable().enableInterrupts(); 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 = Utility.getFPGATime(); setInterruptHigh(); // Delay until the interrupt is complete while (!function.interruptComplete.get()) { Timer.delay(.005); } // Then assertEquals("The interrupt did not fire the expected number of times", 1, counter.getCount()); // If the test within the interrupt failed if (function.exceptionThrown.get()) { throw function.ex; } final long range = 10000; // in microseconds assertThat( "The interrupt did not fire within the expected time period (values in milliseconds)", function.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() throws Exception { // Given 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.interruptComplete.getAndSet(false)) { Timer.delay(.005); } } // Then assertEquals("The interrupt did not fire the expected number of times", fireCount, counter.getCount()); } /** The timeout length for this test in seconds */ private static final int synchronousTimeout = 5; @Test(timeout = (long) (synchronousTimeout * 1e3)) public void testSynchronousInterruptsTriggering() { // Given getInterruptable().requestInterrupts(); final double synchronousDelay = synchronousTimeout / 2.; Runnable r = new Runnable() { @Override public void run() { Timer.delay(synchronousDelay); setInterruptLow(); setInterruptHigh(); } }; // When // Note: the long time value is used because doubles can flip if the robot // is left running for long enough final long startTimeStamp = Utility.getFPGATime(); new Thread(r).start(); // Delay for twice as long as the timeout so the test should fail first getInterruptable().waitForInterrupt(synchronousTimeout * 2); final long stopTimeStamp = Utility.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, .1); } @Test(timeout = (long) (synchronousTimeout * 1e3)) public void testSynchronousInterruptsWaitResultTimeout() { // Given getInterruptable().requestInterrupts(); //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); } @Test(timeout = (long) (synchronousTimeout * 1e3)) public void testSynchronousInterruptsWaitResultRisingEdge() { // Given getInterruptable().requestInterrupts(); final double synchronousDelay = synchronousTimeout / 2.; Runnable r = new Runnable() { @Override public void run() { Timer.delay(synchronousDelay); setInterruptLow(); setInterruptHigh(); } }; new Thread(r).start(); // Delay for twice as long as the timeout so the test should fail first InterruptableSensorBase.WaitResult result = getInterruptable().waitForInterrupt(synchronousTimeout * 2); assertEquals("The interrupt did not fire on the rising edge.", result, InterruptableSensorBase.WaitResult.kRisingEdge); } @Test(timeout = (long) (synchronousTimeout * 1e3)) public void testSynchronousInterruptsWaitResultFallingEdge() { // Given getInterruptable().requestInterrupts(); getInterruptable().setUpSourceEdge(false, true); final double synchronousDelay = synchronousTimeout / 2.; Runnable r = new Runnable() { @Override public void run() { Timer.delay(synchronousDelay); setInterruptHigh(); setInterruptLow(); } }; new Thread(r).start(); // Delay for twice as long as the timeout so the test should fail first InterruptableSensorBase.WaitResult result = getInterruptable().waitForInterrupt(synchronousTimeout * 2); assertEquals("The interrupt did not fire on the falling edge.", result, InterruptableSensorBase.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.interruptComplete.getAndSet(false)) { Timer.delay(.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(.005); } // Then assertEquals("The interrupt did not fire the expected number of times", fireCount, counter.getCount()); } }