Integration tests for SDN-IP.
Tests that sending add and delete route updates to SDN-IP results in the
expected intents being created.
Change-Id: I34a1b24ec133c077ba1bd7dd30f87c1ad4ce367f
diff --git a/src/main/java/net/onrc/onos/apps/sdnip/SdnIp.java b/src/main/java/net/onrc/onos/apps/sdnip/SdnIp.java
index ed7a8ee..ffa9cfa 100644
--- a/src/main/java/net/onrc/onos/apps/sdnip/SdnIp.java
+++ b/src/main/java/net/onrc/onos/apps/sdnip/SdnIp.java
@@ -513,7 +513,7 @@
*
* @param update RIB update
*/
- private void processRibDelete(RibUpdate update) {
+ protected void processRibDelete(RibUpdate update) {
synchronized (this) {
Prefix prefix = update.getPrefix();
diff --git a/src/test/java/net/onrc/onos/apps/sdnip/RadixTreeTest.java b/src/test/java/net/onrc/onos/apps/sdnip/RadixTreeTest.java
new file mode 100644
index 0000000..1d1034c
--- /dev/null
+++ b/src/test/java/net/onrc/onos/apps/sdnip/RadixTreeTest.java
@@ -0,0 +1,88 @@
+package net.onrc.onos.apps.sdnip;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.net.InetAddress;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.junit.Test;
+
+import com.google.common.net.InetAddresses;
+import com.googlecode.concurrenttrees.radix.node.concrete.DefaultByteArrayNodeFactory;
+import com.googlecode.concurrenttrees.radixinverted.ConcurrentInvertedRadixTree;
+import com.googlecode.concurrenttrees.radixinverted.InvertedRadixTree;
+
+/**
+ * Sanity tests for the InvertedRadixTree.
+ * <p/>
+ * These tests are used to verify that the InvertedRadixTree provides the
+ * functionality we need to fit our use case.
+ */
+public class RadixTreeTest {
+
+ private Map<String, Interface> interfaces;
+ private InvertedRadixTree<Interface> interfaceRoutes;
+
+ private Interface longestInterfacePrefixMatch(InetAddress address) {
+ Prefix prefixToSearchFor = new Prefix(address.getAddress(),
+ Prefix.MAX_PREFIX_LENGTH);
+ Iterator<Interface> it =
+ interfaceRoutes.getValuesForKeysPrefixing(
+ prefixToSearchFor.toBinaryString()).iterator();
+ Interface intf = null;
+ // Find the last prefix, which will be the longest prefix
+ while (it.hasNext()) {
+ intf = it.next();
+ }
+
+ return intf;
+ }
+
+ /**
+ * This is just a test of the InvertedRadixTree, rather than an actual unit
+ * test of SdnIp. It tests that the algorithm used to retrieve the
+ * longest prefix match from the tree actually does retrieve the longest
+ * prefix, and not just any matching prefix.
+ */
+ @Test
+ public void getOutgoingInterfaceTest() {
+ interfaces = new HashMap<>();
+ interfaceRoutes = new ConcurrentInvertedRadixTree<>(
+ new DefaultByteArrayNodeFactory());
+
+ Interface interface1 = new Interface("sw3-eth1", "00:00:00:00:00:00:00:a3",
+ (short) 1, "192.168.10.101", 24);
+ interfaces.put(interface1.getName(), interface1);
+ Interface interface2 = new Interface("sw5-eth1", "00:00:00:00:00:00:00:a5",
+ (short) 1, "192.168.20.101", 16);
+ interfaces.put(interface2.getName(), interface2);
+ Interface interface3 = new Interface("sw2-eth1", "00:00:00:00:00:00:00:a2",
+ (short) 1, "192.168.60.101", 16);
+ interfaces.put(interface3.getName(), interface3);
+ Interface interface4 = new Interface("sw6-eth1", "00:00:00:00:00:00:00:a6",
+ (short) 1, "192.168.60.101", 30);
+ interfaces.put(interface4.getName(), interface4);
+ Interface interface5 = new Interface("sw4-eth4", "00:00:00:00:00:00:00:a4",
+ (short) 4, "192.168.60.101", 24);
+ interfaces.put(interface5.getName(), interface5);
+
+ for (Interface intf : interfaces.values()) {
+ Prefix prefix = new Prefix(intf.getIpAddress().getAddress(),
+ intf.getPrefixLength());
+ interfaceRoutes.put(prefix.toBinaryString(), intf);
+ }
+
+ // Check whether the prefix length takes effect
+ InetAddress nextHopAddress = InetAddresses.forString("192.0.0.1");
+ assertNotNull(nextHopAddress);
+ assertNull(longestInterfacePrefixMatch(nextHopAddress));
+
+ // Check whether it returns the longest matchable address
+ nextHopAddress = InetAddresses.forString("192.168.60.101");
+ assertEquals("sw6-eth1", longestInterfacePrefixMatch(nextHopAddress).getName());
+ }
+}
diff --git a/src/test/java/net/onrc/onos/apps/sdnip/SdnIpTest.java b/src/test/java/net/onrc/onos/apps/sdnip/SdnIpTest.java
index f5a6d31..febddca 100644
--- a/src/test/java/net/onrc/onos/apps/sdnip/SdnIpTest.java
+++ b/src/test/java/net/onrc/onos/apps/sdnip/SdnIpTest.java
@@ -1,81 +1,441 @@
package net.onrc.onos.apps.sdnip;
-import static org.junit.Assert.*;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reportMatcher;
+import static org.easymock.EasyMock.reset;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
-import java.io.IOException;
import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Iterator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import net.floodlightcontroller.core.module.FloodlightModuleContext;
+import net.floodlightcontroller.core.module.FloodlightModuleException;
+import net.floodlightcontroller.util.MACAddress;
+import net.onrc.onos.api.newintent.IntentId;
+import net.onrc.onos.api.newintent.IntentService;
+import net.onrc.onos.api.newintent.MultiPointToSinglePointIntent;
+import net.onrc.onos.apps.proxyarp.IProxyArpService;
+import net.onrc.onos.apps.sdnip.RibUpdate.Operation;
+import net.onrc.onos.core.matchaction.action.ModifyDstMacAction;
+import net.onrc.onos.core.matchaction.match.PacketMatch;
+import net.onrc.onos.core.matchaction.match.PacketMatchBuilder;
+import net.onrc.onos.core.registry.IControllerRegistryService;
+import net.onrc.onos.core.util.IPv4;
+import net.onrc.onos.core.util.IdBlock;
+import net.onrc.onos.core.util.IntegrationTest;
+import net.onrc.onos.core.util.SwitchPort;
+import net.onrc.onos.core.util.TestUtils;
+
+import org.easymock.IAnswer;
+import org.easymock.IArgumentMatcher;
+import org.junit.Before;
import org.junit.Test;
+import org.junit.experimental.categories.Category;
-import com.googlecode.concurrenttrees.radix.node.concrete.DefaultByteArrayNodeFactory;
-import com.googlecode.concurrenttrees.radixinverted.ConcurrentInvertedRadixTree;
-import com.googlecode.concurrenttrees.radixinverted.InvertedRadixTree;
+import com.google.common.net.InetAddresses;
+/**
+ * Integration tests for the SDN-IP application.
+ * <p/>
+ * The tests are very coarse-grained. They feed route updates in to SDN-IP
+ * (simulating routes learnt from BGPd), then they check that the correct
+ * intents are created and submitted to the intent service. The entire route
+ * processing logic of SDN-IP is tested.
+ */
+@Category(IntegrationTest.class)
public class SdnIpTest {
+ private static final int MAC_ADDRESS_LENGTH = 6;
+ private static final InetAddress ROUTER_ID =
+ InetAddresses.forString("192.168.10.101");
+
+ private static final int MIN_PREFIX_LENGTH = 1;
+ private static final int MAX_PREFIX_LENGTH = 32;
+
+ private SdnIp sdnip;
+ private IProxyArpService proxyArp;
+ private IntentService intentService;
private Map<String, Interface> interfaces;
- private InvertedRadixTree<Interface> interfaceRoutes;
+ private Map<InetAddress, BgpPeer> peers;
- private Interface longestInterfacePrefixMatch(InetAddress address) {
- Prefix prefixToSearchFor = new Prefix(address.getAddress(),
- Prefix.MAX_PREFIX_LENGTH);
- Iterator<Interface> it =
- interfaceRoutes.getValuesForKeysPrefixing(
- prefixToSearchFor.toBinaryString()).iterator();
- Interface intf = null;
- // Find the last prefix, which will be the longest prefix
- while (it.hasNext()) {
- intf = it.next();
+ private Random random;
+
+ @Before
+ public void setUp() throws Exception {
+ interfaces = setUpInterfaces();
+ peers = setUpPeers();
+ random = new Random();
+ initSdnIp();
+ }
+
+ private Map<String, Interface> setUpInterfaces() {
+ Map<String, Interface> configuredInterfaces = new HashMap<>();
+
+ String name1 = "s1-eth1";
+ configuredInterfaces.put(name1, new Interface(name1, "00:00:00:00:00:00:00:01",
+ (short) 1, "192.168.10.101", 24));
+ String name2 = "s2-eth1";
+ configuredInterfaces.put(name2, new Interface(name2, "00:00:00:00:00:00:00:02",
+ (short) 1, "192.168.20.101", 24));
+ String name3 = "s3-eth1";
+ configuredInterfaces.put(name3, new Interface(name3, "00:00:00:00:00:00:00:03",
+ (short) 1, "192.168.30.101", 24));
+
+ return configuredInterfaces;
+ }
+
+ private Map<InetAddress, BgpPeer> setUpPeers() {
+ Map<InetAddress, BgpPeer> configuredPeers = new LinkedHashMap<>();
+
+ String peer1 = "192.168.10.1";
+ configuredPeers.put(InetAddresses.forString(peer1),
+ new BgpPeer("s1-eth1", peer1));
+
+ String peer2 = "192.168.20.1";
+ configuredPeers.put(InetAddresses.forString(peer2),
+ new BgpPeer("s2-eth1", peer2));
+
+ String peer3 = "192.168.30.1";
+ configuredPeers.put(InetAddresses.forString(peer3),
+ new BgpPeer("s3-eth1", peer3));
+
+ return configuredPeers;
+ }
+
+ private void initSdnIp() throws FloodlightModuleException {
+ sdnip = new SdnIp();
+
+ FloodlightModuleContext context = new FloodlightModuleContext();
+ context.addConfigParam(sdnip, "BgpdRestIp", "1.1.1.1");
+ context.addConfigParam(sdnip, "RouterId", "192.168.10.101");
+
+ intentService = createMock(IntentService.class);
+ replay(intentService);
+
+ proxyArp = new TestProxyArpService();
+ context.addService(IProxyArpService.class, proxyArp);
+
+ IControllerRegistryService registry =
+ createMock(IControllerRegistryService.class);
+ expect(registry.allocateUniqueIdBlock()).
+ andReturn(new IdBlock(0, Long.MAX_VALUE));
+ replay(registry);
+ context.addService(IControllerRegistryService.class, registry);
+
+ TestUtils.setField(sdnip, "intentService", intentService);
+ sdnip.init(context);
+ TestUtils.setField(sdnip, "interfaces", interfaces);
+ TestUtils.setField(sdnip, "bgpPeers", peers);
+
+ }
+
+ /**
+ * EasyMock matcher that matches {@link MultiPointToSinglePointIntent}s but
+ * ignores the {@link IntentId} when matching.
+ * <p/>
+ * The normal intent equals method tests that the intent IDs are equal,
+ * however in these tests I can't know what the intent IDs will be in
+ * advance, so I can't set up expected intents with the correct IDs. Even
+ * though I can set up an ID generator that generates a sequential stream
+ * of IDs, I don't know in which order the IDs will be assigned to intents
+ * (because intents can be processed out of order due to timing randomness
+ * introduced by the test ARP module).
+ * <p/>
+ * The solution is to use an EasyMock matcher that verifies that all the
+ * value properties of the provided intent match the expected values, but
+ * ignores the intent ID when testing equality.
+ */
+ private static final class IdAgnosticIntentMatcher implements IArgumentMatcher {
+ private final MultiPointToSinglePointIntent intent;
+ private String providedIntentString;
+
+ /**
+ * Constructor taking the expected intent to match against.
+ *
+ * @param intent the expected intent
+ */
+ public IdAgnosticIntentMatcher(MultiPointToSinglePointIntent intent) {
+ this.intent = intent;
}
- return intf;
+ @Override
+ public void appendTo(StringBuffer strBuffer) {
+ strBuffer.append("IntentMatcher unable to match: " + providedIntentString);
+ }
+
+ @Override
+ public boolean matches(Object object) {
+ if (!(object instanceof MultiPointToSinglePointIntent)) {
+ return false;
+ }
+
+ MultiPointToSinglePointIntent providedIntent =
+ (MultiPointToSinglePointIntent) object;
+ providedIntentString = providedIntent.toString();
+
+ MultiPointToSinglePointIntent matchIntent =
+ new MultiPointToSinglePointIntent(providedIntent.getId(),
+ intent.getMatch(), intent.getAction(), intent.getIngressPorts(),
+ intent.getEgressPort());
+
+ return matchIntent.equals(providedIntent);
+ }
}
+
/**
- * This is just a test of the InvertedRadixTree, rather than an actual unit
- * test of SdnIp.
+ * Matcher method to set an expected intent to match against (ignoring the
+ * the intent ID).
*
- * @throws IOException
+ * @param intent the expected intent
+ * @return something of type MultiPointToSinglePointIntent
+ */
+ private static MultiPointToSinglePointIntent eqExceptId(
+ MultiPointToSinglePointIntent intent) {
+ reportMatcher(new IdAgnosticIntentMatcher(intent));
+ return null;
+ }
+
+ /**
+ * Tests adding a set of routes into SDN-IP.
+ * <p/>
+ * Random routes are generated and fed in to the SDN-IP route processing
+ * logic (via processRibAdd). We check that the correct intents are
+ * generated and submitted to our mock intent service.
+ *
+ * @throws InterruptedException if interrupted while waiting on a latch
*/
@Test
- public void getOutgoingInterfaceTest() throws IOException {
+ public void testAddRoutes() throws InterruptedException {
+ int numRoutes = 100;
- interfaces = new HashMap<>();
- interfaceRoutes = new ConcurrentInvertedRadixTree<>(
- new DefaultByteArrayNodeFactory());
+ final CountDownLatch latch = new CountDownLatch(numRoutes);
- Interface interface1 = new Interface("sw3-eth1", "00:00:00:00:00:00:00:a3",
- (short) 1, "192.168.10.101", 24);
- interfaces.put(interface1.getName(), interface1);
- Interface interface2 = new Interface("sw5-eth1", "00:00:00:00:00:00:00:a5",
- (short) 1, "192.168.20.101", 16);
- interfaces.put(interface2.getName(), interface2);
- Interface interface3 = new Interface("sw2-eth1", "00:00:00:00:00:00:00:a2",
- (short) 1, "192.168.60.101", 16);
- interfaces.put(interface3.getName(), interface3);
- Interface interface4 = new Interface("sw6-eth1", "00:00:00:00:00:00:00:a6",
- (short) 1, "192.168.60.101", 30);
- interfaces.put(interface4.getName(), interface4);
- Interface interface5 = new Interface("sw4-eth4", "00:00:00:00:00:00:00:a4",
- (short) 4, "192.168.60.101", 24);
- interfaces.put(interface5.getName(), interface5);
+ List<RibUpdate> routeUpdates = generateRouteUpdateIntents(numRoutes);
- for (Interface intf : interfaces.values()) {
- Prefix prefix = new Prefix(intf.getIpAddress().getAddress(),
- intf.getPrefixLength());
- interfaceRoutes.put(prefix.toBinaryString(), intf);
+ reset(intentService);
+
+ // Set up expectations
+ for (RibUpdate update : routeUpdates) {
+ InetAddress nextHopPeer = update.getRibEntry().getNextHop();
+ MultiPointToSinglePointIntent intent = getIntentForUpdate(update,
+ generateMacAddress(nextHopPeer),
+ interfaces.get(peers.get(nextHopPeer).getInterfaceName()));
+ intentService.submit(eqExceptId(intent));
+ expectLastCall().andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ latch.countDown();
+ return null;
+ }
+ }).once();
}
- // Check whether the prefix length takes effect
- InetAddress nextHopAddress = InetAddress.getByName("192.0.0.1");
- assertNotNull(nextHopAddress);
- assertNull(longestInterfacePrefixMatch(nextHopAddress));
+ replay(intentService);
- // Check whether it returns the longest matchable address
- nextHopAddress = InetAddress.getByName("192.168.60.101");
- assertEquals("sw6-eth1", longestInterfacePrefixMatch(nextHopAddress).getName());
+ // Add route updates
+ for (RibUpdate update : routeUpdates) {
+ sdnip.processRibAdd(update);
+ }
+ latch.await(5000, TimeUnit.MILLISECONDS);
+
+ assertEquals(sdnip.getPtree().size(), numRoutes);
+
+ verify(intentService);
}
+
+ /**
+ * Tests adding then deleting a set of routes from SDN-IP.
+ * <p/>
+ * Random routes are generated and fed in to the SDN-IP route processing
+ * logic (via processRibAdd), and we check that the correct intents are
+ * generated. We then delete the entire set of routes (by feeding updates
+ * to processRibDelete), and check that the correct intents are withdrawn
+ * from the intent service.
+ *
+ * @throws InterruptedException if interrupted while waiting on a latch
+ */
+ @Test
+ public void testDeleteRoutes() throws InterruptedException {
+ int numRoutes = 100;
+ List<RibUpdate> routeUpdates = generateRouteUpdateIntents(numRoutes);
+
+ final CountDownLatch installCount = new CountDownLatch(numRoutes);
+ final CountDownLatch deleteCount = new CountDownLatch(numRoutes);
+
+ reset(intentService);
+
+ for (RibUpdate update : routeUpdates) {
+ InetAddress nextHopPeer = update.getRibEntry().getNextHop();
+ MultiPointToSinglePointIntent intent = getIntentForUpdate(update,
+ generateMacAddress(nextHopPeer),
+ interfaces.get(peers.get(nextHopPeer).getInterfaceName()));
+ intentService.submit(eqExceptId(intent));
+ expectLastCall().andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ installCount.countDown();
+ return null;
+ }
+ }).once();
+ intentService.withdraw(eqExceptId(intent));
+ expectLastCall().andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ deleteCount.countDown();
+ return null;
+ }
+ }).once();
+ }
+
+ replay(intentService);
+
+
+ // Send the add updates first
+ for (RibUpdate update : routeUpdates) {
+ sdnip.processRibAdd(update);
+ }
+
+ // Give some time to let the intents be submitted
+ installCount.await(5000, TimeUnit.MILLISECONDS);
+
+ // Send the DELETE updates
+ for (RibUpdate update : routeUpdates) {
+ RibUpdate deleteUpdate = new RibUpdate(Operation.DELETE,
+ update.getPrefix(), update.getRibEntry());
+ sdnip.processRibDelete(deleteUpdate);
+ }
+
+ deleteCount.await(5000, TimeUnit.MILLISECONDS);
+
+ assertEquals(0, sdnip.getPtree().size());
+
+ verify(intentService);
+ }
+
+ /**
+ * Generates a set of route updates. The prefix for each route is randomly
+ * generated, and the next hop is selected from the set of BGP peers that
+ * was generated during {@link #setUp()}. All have the UPDATE operation.
+ * The generated prefixes are unique within the batch generated by each
+ * call of this method.
+ *
+ * @param numRoutes the number of route updates to generate
+ * @return a list of generated route updates
+ */
+ private List<RibUpdate> generateRouteUpdateIntents(int numRoutes) {
+ List<RibUpdate> routeUpdates = new ArrayList<>(numRoutes);
+
+ Set<Prefix> prefixes = new HashSet<>();
+
+ for (int i = 0; i < numRoutes; i++) {
+ Prefix prefix;
+ do {
+ InetAddress prefixAddress = InetAddresses.fromInteger(random.nextInt());
+ // Generate a random prefix length between MIN_PREFIX_LENGTH and
+ // MAX_PREFIX_LENGTH
+ int prefixLength = random.nextInt(
+ (MAX_PREFIX_LENGTH - MIN_PREFIX_LENGTH) + 1) + MIN_PREFIX_LENGTH;
+ prefix = new Prefix(prefixAddress.getAddress(), prefixLength);
+ // We have to ensure we don't generate the same prefix twice
+ // (this is quite easy to do with small prefix lengths)
+ } while (prefixes.contains(prefix));
+
+ prefixes.add(prefix);
+
+ // Randomly select a peer to use as the next hop
+ BgpPeer nextHop = null;
+ int peerNumber = random.nextInt(peers.size());
+ int j = 0;
+ for (BgpPeer peer : peers.values()) {
+ if (j++ == peerNumber) {
+ nextHop = peer;
+ break;
+ }
+ }
+
+ assertNotNull(nextHop);
+
+ RibUpdate update = new RibUpdate(Operation.UPDATE, prefix,
+ new RibEntry(ROUTER_ID, nextHop.getIpAddress()));
+
+ routeUpdates.add(update);
+ }
+
+ return routeUpdates;
+ }
+
+ /**
+ * Generates the MultiPointToSinglePointIntent that should be
+ * submitted/withdrawn for a particular RibUpdate.
+ *
+ * @param update the RibUpdate to generate an intent for
+ * @param nextHopMac a MAC address to use as the dst-mac for the intent
+ * @param egressInterface the outgoing interface for the intent
+ * @return the generated intent
+ */
+ private MultiPointToSinglePointIntent getIntentForUpdate(RibUpdate update,
+ MACAddress nextHopMac, Interface egressInterface) {
+ Prefix prefix = update.getPrefix();
+
+ PacketMatchBuilder builder = new PacketMatchBuilder();
+ builder.setDstIp(new IPv4(
+ InetAddresses.coerceToInteger(prefix.getInetAddress())),
+ (short) prefix.getPrefixLength());
+ PacketMatch match = builder.build();
+
+ ModifyDstMacAction action = new ModifyDstMacAction(nextHopMac);
+
+ Set<SwitchPort> ingressPorts = new HashSet<>();
+ for (Interface intf : interfaces.values()) {
+ if (!intf.equals(egressInterface)) {
+ ingressPorts.add(intf.getSwitchPort());
+ }
+ }
+
+ // Create the intent. The intent ID is arbitrary because we don't consider
+ // it when matching against the intent passed to the mock
+ MultiPointToSinglePointIntent intent = new MultiPointToSinglePointIntent(
+ new IntentId(0), match, action, ingressPorts,
+ egressInterface.getSwitchPort());
+
+ return intent;
+ }
+
+ /**
+ * Generates a MAC address based on an IP address.
+ * For the test we need MAC addresses but the actual values don't have any
+ * meaning, so we'll just generate them based on the IP address. This means
+ * we have a deterministic mapping from IP address to MAC address.
+ *
+ * @param ipAddress IP address used to generate a MAC address
+ * @return generated MAC address
+ */
+ public static MACAddress generateMacAddress(InetAddress ipAddress) {
+ byte[] macAddress = new byte[MAC_ADDRESS_LENGTH];
+ ByteBuffer bb = ByteBuffer.wrap(macAddress);
+
+ // Put the IP address bytes into the lower four bytes of the MAC address.
+ // Leave the first two bytes set to 0.
+ bb.position(2);
+ bb.put(ipAddress.getAddress());
+
+ return MACAddress.valueOf(bb.array());
+ }
+
}
diff --git a/src/test/java/net/onrc/onos/apps/sdnip/TestProxyArpService.java b/src/test/java/net/onrc/onos/apps/sdnip/TestProxyArpService.java
new file mode 100644
index 0000000..d6129ac
--- /dev/null
+++ b/src/test/java/net/onrc/onos/apps/sdnip/TestProxyArpService.java
@@ -0,0 +1,108 @@
+package net.onrc.onos.apps.sdnip;
+
+import java.net.InetAddress;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import net.floodlightcontroller.util.MACAddress;
+import net.onrc.onos.apps.proxyarp.IArpRequester;
+import net.onrc.onos.apps.proxyarp.IProxyArpService;
+
+/**
+ * Test version of the IProxyArpService which is used to simulate delays in
+ * receiving ARP replies, as you would see in a real system due to the time
+ * it takes to proxy ARP packets to/from the host. Requests are asynchronous,
+ * and replies may come back to the requestor in a different order than the
+ * requests were sent, which again you would expect to see in a real system.
+ */
+public class TestProxyArpService implements IProxyArpService {
+
+ /**
+ * The maximum possible delay before an ARP reply is received.
+ */
+ private static final int MAX_ARP_REPLY_DELAY = 30; // milliseconds
+
+ /**
+ * The probability that we already have the MAC address cached when the
+ * caller calls {@link #getMacAddress(InetAddress)}.
+ */
+ private static final float MAC_ALREADY_KNOWN_PROBABILITY = 0.3f;
+
+ private final ScheduledExecutorService replyTaskExecutor;
+
+ private final Random random;
+
+ /**
+ * Class constructor.
+ */
+ public TestProxyArpService() {
+ replyTaskExecutor = Executors.newSingleThreadScheduledExecutor();
+ random = new Random();
+ }
+
+ /**
+ * Task used to reply to ARP requests from a different thread. Replies
+ * usually come on a different thread in the real system, so we need to
+ * ensure we test this behaviour.
+ */
+ private static class ReplyTask implements Runnable {
+ private IArpRequester requestor;
+ private InetAddress ipAddress;
+ private MACAddress macAddress;
+
+ /**
+ * Class constructor.
+ *
+ * @param requestor the client who requested the MAC address
+ * @param ipAddress the target IP address of the request
+ * @param macAddress the MAC address in the ARP reply
+ */
+ public ReplyTask(IArpRequester requestor, InetAddress ipAddress,
+ MACAddress macAddress) {
+ this.requestor = requestor;
+ this.ipAddress = ipAddress;
+ this.macAddress = macAddress;
+ }
+
+ @Override
+ public void run() {
+ requestor.arpResponse(ipAddress, macAddress);
+ }
+ }
+
+ @Override
+ public MACAddress getMacAddress(InetAddress ipAddress) {
+ float replyChance = random.nextFloat();
+ if (replyChance < MAC_ALREADY_KNOWN_PROBABILITY) {
+ // Some percentage of the time we already know the MAC address, so
+ // we reply directly when the requestor asks for the MAC address
+ return SdnIpTest.generateMacAddress(ipAddress);
+ }
+ return null;
+ }
+
+ @Override
+ public void sendArpRequest(InetAddress ipAddress, IArpRequester requester,
+ boolean retry) {
+ // Randomly select an amount of time to delay the reply coming back to
+ // the requestor (simulating time taken to proxy the request to a
+ // network host).
+ int delay = random.nextInt(MAX_ARP_REPLY_DELAY);
+
+ MACAddress macAddress = SdnIpTest.generateMacAddress(ipAddress);
+
+ ReplyTask replyTask = new ReplyTask(requester, ipAddress, macAddress);
+
+ replyTaskExecutor.schedule(replyTask , delay, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public List<String> getMappings() {
+ // We don't care about this method for the current test use cases
+ return null;
+ }
+
+}