Adding Intent API tests
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/ConnectivityIntentTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/ConnectivityIntentTest.java
new file mode 100644
index 0000000..fb1efee
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/ConnectivityIntentTest.java
@@ -0,0 +1,28 @@
+package org.onlab.onos.net.intent;
+
+import java.util.Set;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.flow.DefaultTrafficSelector;
+import org.onlab.onos.net.flow.DefaultTrafficTreatment;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+/**
+ * Base facilities to test various connectivity tests.
+ */
+public abstract class ConnectivityIntentTest extends IntentTest {
+
+ public static final IntentId IID = new IntentId(123);
+ public static final TrafficSelector MATCH = (new DefaultTrafficSelector.Builder()).build();
+ public static final TrafficTreatment NOP = (new DefaultTrafficTreatment.Builder()).build();
+
+ public static final ConnectPoint P1 = new ConnectPoint(DeviceId.deviceId("111"), PortNumber.portNumber(0x1));
+ public static final ConnectPoint P2 = new ConnectPoint(DeviceId.deviceId("222"), PortNumber.portNumber(0x2));
+ public static final ConnectPoint P3 = new ConnectPoint(DeviceId.deviceId("333"), PortNumber.portNumber(0x3));
+
+ public static final Set<ConnectPoint> PS1 = itemSet(new ConnectPoint[]{P1, P3});
+ public static final Set<ConnectPoint> PS2 = itemSet(new ConnectPoint[]{P2, P3});
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/FakeIntentManager.java b/core/api/src/test/java/org/onlab/onos/net/intent/FakeIntentManager.java
new file mode 100644
index 0000000..df46ec5
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/FakeIntentManager.java
@@ -0,0 +1,268 @@
+package org.onlab.onos.net.intent;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Fake implementation of the intent service to assist in developing tests
+ * of the interface contract.
+ */
+public class FakeIntentManager implements TestableIntentService {
+
+ private final Map<IntentId, Intent> intents = new HashMap<>();
+ private final Map<IntentId, IntentState> intentStates = new HashMap<>();
+ private final Map<IntentId, List<InstallableIntent>> installables = new HashMap<>();
+ private final Set<IntentEventListener> listeners = new HashSet<>();
+
+ private final Map<Class<? extends Intent>, IntentCompiler<? extends Intent>> compilers = new HashMap<>();
+ private final Map<Class<? extends InstallableIntent>,
+ IntentInstaller<? extends InstallableIntent>> installers = new HashMap<>();
+
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+ private final List<IntentException> exceptions = new ArrayList<>();
+
+ @Override
+ public List<IntentException> getExceptions() {
+ return exceptions;
+ }
+
+ // Provides an out-of-thread simulation of intent submit life-cycle
+ private void executeSubmit(final Intent intent) {
+ registerSubclassCompilerIfNeeded(intent);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ List<InstallableIntent> installable = compileIntent(intent);
+ installIntents(intent, installable);
+ } catch (IntentException e) {
+ exceptions.add(e);
+ }
+ }
+ });
+ }
+
+ // Provides an out-of-thread simulation of intent withdraw life-cycle
+ private void executeWithdraw(final Intent intent) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ List<InstallableIntent> installable = getInstallable(intent.getId());
+ uninstallIntents(intent, installable);
+ } catch (IntentException e) {
+ exceptions.add(e);
+ }
+
+ }
+ });
+ }
+
+ private <T extends Intent> IntentCompiler<T> getCompiler(T intent) {
+ @SuppressWarnings("unchecked")
+ IntentCompiler<T> compiler = (IntentCompiler<T>) compilers.get(intent.getClass());
+ if (compiler == null) {
+ throw new IntentException("no compiler for class " + intent.getClass());
+ }
+ return compiler;
+ }
+
+ private <T extends InstallableIntent> IntentInstaller<T> getInstaller(T intent) {
+ @SuppressWarnings("unchecked")
+ IntentInstaller<T> installer = (IntentInstaller<T>) installers.get(intent.getClass());
+ if (installer == null) {
+ throw new IntentException("no installer for class " + intent.getClass());
+ }
+ return installer;
+ }
+
+ private <T extends Intent> List<InstallableIntent> compileIntent(T intent) {
+ try {
+ // For the fake, we compile using a single level pass
+ List<InstallableIntent> installable = new ArrayList<>();
+ for (Intent compiled : getCompiler(intent).compile(intent)) {
+ installable.add((InstallableIntent) compiled);
+ }
+ setState(intent, IntentState.COMPILED);
+ return installable;
+ } catch (IntentException e) {
+ setState(intent, IntentState.FAILED);
+ throw e;
+ }
+ }
+
+ private void installIntents(Intent intent, List<InstallableIntent> installable) {
+ try {
+ for (InstallableIntent ii : installable) {
+ registerSubclassInstallerIfNeeded(ii);
+ getInstaller(ii).install(ii);
+ }
+ setState(intent, IntentState.INSTALLED);
+ putInstallable(intent.getId(), installable);
+ } catch (IntentException e) {
+ setState(intent, IntentState.FAILED);
+ throw e;
+ }
+ }
+
+ private void uninstallIntents(Intent intent, List<InstallableIntent> installable) {
+ try {
+ for (InstallableIntent ii : installable) {
+ getInstaller(ii).uninstall(ii);
+ }
+ setState(intent, IntentState.WITHDRAWN);
+ removeInstallable(intent.getId());
+ } catch (IntentException e) {
+ setState(intent, IntentState.FAILED);
+ throw e;
+ }
+ }
+
+
+ // Sets the internal state for the given intent and dispatches an event
+ private void setState(Intent intent, IntentState state) {
+ IntentState previous = intentStates.get(intent.getId());
+ intentStates.put(intent.getId(), state);
+ dispatch(new IntentEvent(intent, state, previous, System.currentTimeMillis()));
+ }
+
+ private void putInstallable(IntentId id, List<InstallableIntent> installable) {
+ installables.put(id, installable);
+ }
+
+ private void removeInstallable(IntentId id) {
+ installables.remove(id);
+ }
+
+ private List<InstallableIntent> getInstallable(IntentId id) {
+ List<InstallableIntent> installable = installables.get(id);
+ if (installable != null) {
+ return installable;
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ @Override
+ public void submit(Intent intent) {
+ intents.put(intent.getId(), intent);
+ setState(intent, IntentState.SUBMITTED);
+ executeSubmit(intent);
+ }
+
+ @Override
+ public void withdraw(Intent intent) {
+ intents.remove(intent.getId());
+ setState(intent, IntentState.WITHDRAWING);
+ executeWithdraw(intent);
+ }
+
+ @Override
+ public void execute(IntentOperations operations) {
+ // TODO: implement later
+ }
+
+ @Override
+ public Set<Intent> getIntents() {
+ return Collections.unmodifiableSet(new HashSet<>(intents.values()));
+ }
+
+ @Override
+ public Intent getIntent(IntentId id) {
+ return intents.get(id);
+ }
+
+ @Override
+ public IntentState getIntentState(IntentId id) {
+ return intentStates.get(id);
+ }
+
+ @Override
+ public void addListener(IntentEventListener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public void removeListener(IntentEventListener listener) {
+ listeners.remove(listener);
+ }
+
+ private void dispatch(IntentEvent event) {
+ for (IntentEventListener listener : listeners) {
+ listener.event(event);
+ }
+ }
+
+ @Override
+ public <T extends Intent> void registerCompiler(Class<T> cls, IntentCompiler<T> compiler) {
+ compilers.put(cls, compiler);
+ }
+
+ @Override
+ public <T extends Intent> void unregisterCompiler(Class<T> cls) {
+ compilers.remove(cls);
+ }
+
+ @Override
+ public Map<Class<? extends Intent>, IntentCompiler<? extends Intent>> getCompilers() {
+ return Collections.unmodifiableMap(compilers);
+ }
+
+ @Override
+ public <T extends InstallableIntent> void registerInstaller(Class<T> cls, IntentInstaller<T> installer) {
+ installers.put(cls, installer);
+ }
+
+ @Override
+ public <T extends InstallableIntent> void unregisterInstaller(Class<T> cls) {
+ installers.remove(cls);
+ }
+
+ @Override
+ public Map<Class<? extends InstallableIntent>,
+ IntentInstaller<? extends InstallableIntent>> getInstallers() {
+ return Collections.unmodifiableMap(installers);
+ }
+
+ private void registerSubclassCompilerIfNeeded(Intent intent) {
+ if (!compilers.containsKey(intent.getClass())) {
+ Class<?> cls = intent.getClass();
+ while (cls != Object.class) {
+ // As long as we're within the Intent class descendants
+ if (Intent.class.isAssignableFrom(cls)) {
+ IntentCompiler<?> compiler = compilers.get(cls);
+ if (compiler != null) {
+ compilers.put(intent.getClass(), compiler);
+ return;
+ }
+ }
+ cls = cls.getSuperclass();
+ }
+ }
+ }
+
+ private void registerSubclassInstallerIfNeeded(InstallableIntent intent) {
+ if (!installers.containsKey(intent.getClass())) {
+ Class<?> cls = intent.getClass();
+ while (cls != Object.class) {
+ // As long as we're within the InstallableIntent class descendants
+ if (InstallableIntent.class.isAssignableFrom(cls)) {
+ IntentInstaller<?> installer = installers.get(cls);
+ if (installer != null) {
+ installers.put(intent.getClass(), installer);
+ return;
+ }
+ }
+ cls = cls.getSuperclass();
+ }
+ }
+ }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/ImmutableClassChecker.java b/core/api/src/test/java/org/onlab/onos/net/intent/ImmutableClassChecker.java
new file mode 100644
index 0000000..0e63af9
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/ImmutableClassChecker.java
@@ -0,0 +1,125 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+import org.hamcrest.Description;
+import org.hamcrest.StringDescription;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+/**
+ * Hamcrest style class for verifying that a class follows the
+ * accepted rules for immutable classes.
+ *
+ * The rules that are enforced for immutable classes:
+ * - the class must be declared final
+ * - all data members of the class must be declared private and final
+ * - the class must not define any setter methods
+ */
+
+public class ImmutableClassChecker {
+
+ private String failureReason = "";
+
+ /**
+ * Method to determine if a given class is a properly specified
+ * immutable class.
+ *
+ * @param clazz the class to check
+ * @return true if the given class is a properly specified immutable class.
+ */
+ private boolean isImmutableClass(Class<?> clazz) {
+ // class must be declared final
+ if (!Modifier.isFinal(clazz.getModifiers())) {
+ failureReason = "a class that is not final";
+ return false;
+ }
+
+ // class must have only final and private data members
+ for (final Field field : clazz.getDeclaredFields()) {
+ if (field.getName().startsWith("__cobertura")) {
+ // cobertura sticks these fields into classes - ignore them
+ continue;
+ }
+ if (!Modifier.isFinal(field.getModifiers())) {
+ failureReason = "a field named '" + field.getName() +
+ "' that is not final";
+ return false;
+ }
+ if (!Modifier.isPrivate(field.getModifiers())) {
+ //
+ // NOTE: We relax the recommended rules for defining immutable
+ // objects and allow "static final" fields that are not
+ // private. The "final" check was already done above so we
+ // don't repeat it here.
+ //
+ if (!Modifier.isStatic(field.getModifiers())) {
+ failureReason = "a field named '" + field.getName() +
+ "' that is not private and is not static";
+ return false;
+ }
+ }
+ }
+
+ // class must not define any setters
+ for (final Method method : clazz.getMethods()) {
+ if (method.getDeclaringClass().equals(clazz)) {
+ if (method.getName().startsWith("set")) {
+ failureReason = "a class with a setter named '" + method.getName() + "'";
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Describe why an error was reported. Uses Hamcrest style Description
+ * interfaces.
+ *
+ * @param description the Description object to use for reporting the
+ * mismatch
+ */
+ public void describeMismatch(Description description) {
+ description.appendText(failureReason);
+ }
+
+ /**
+ * Describe the source object that caused an error, using a Hamcrest
+ * Matcher style interface. In this case, it always returns
+ * that we are looking for a properly defined utility class.
+ *
+ * @param description the Description object to use to report the "to"
+ * object
+ */
+ public void describeTo(Description description) {
+ description.appendText("a properly defined immutable class");
+ }
+
+ /**
+ * Assert that the given class adheres to the utility class rules.
+ *
+ * @param clazz the class to check
+ *
+ * @throws java.lang.AssertionError if the class is not a valid
+ * utility class
+ */
+ public static void assertThatClassIsImmutable(Class<?> clazz) {
+ final ImmutableClassChecker checker = new ImmutableClassChecker();
+ if (!checker.isImmutableClass(clazz)) {
+ final Description toDescription = new StringDescription();
+ final Description mismatchDescription = new StringDescription();
+
+ checker.describeTo(toDescription);
+ checker.describeMismatch(mismatchDescription);
+ final String reason =
+ "\n" +
+ "Expected: is \"" + toDescription.toString() + "\"\n" +
+ " but : was \"" + mismatchDescription.toString() + "\"";
+
+ throw new AssertionError(reason);
+ }
+ }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/IntentExceptionTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/IntentExceptionTest.java
new file mode 100644
index 0000000..02564e6
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/IntentExceptionTest.java
@@ -0,0 +1,33 @@
+package org.onlab.onos.net.intent;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test of the intent exception.
+ */
+public class IntentExceptionTest {
+
+ @Test
+ public void basics() {
+ validate(new IntentException(), null, null);
+ validate(new IntentException("foo"), "foo", null);
+
+ Throwable cause = new NullPointerException("bar");
+ validate(new IntentException("foo", cause), "foo", cause);
+ }
+
+ /**
+ * Validates that the specified exception has the correct message and cause.
+ *
+ * @param e exception to test
+ * @param message expected message
+ * @param cause expected cause
+ */
+ protected void validate(RuntimeException e, String message, Throwable cause) {
+ assertEquals("incorrect message", message, e.getMessage());
+ assertEquals("incorrect cause", cause, e.getCause());
+ }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdGenerator.java b/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdGenerator.java
new file mode 100644
index 0000000..0ca669b
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdGenerator.java
@@ -0,0 +1,14 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * This interface is for generator of IntentId. It is defined only for
+ * testing purpose to keep type safety on mock creation.
+ *
+ * <p>
+ * {@link #getNewId()} generates a globally unique {@link IntentId} instance
+ * on each invocation. Application developers should not generate IntentId
+ * by themselves. Instead use an implementation of this interface.
+ * </p>
+ */
+public interface IntentIdGenerator extends IdGenerator<IntentId> {
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java
new file mode 100644
index 0000000..2a0824c
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java
@@ -0,0 +1,57 @@
+package org.onlab.onos.net.intent;
+
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+/**
+ * This class tests the immutability, equality, and non-equality of
+ * {@link IntentId}.
+ */
+public class IntentIdTest {
+ /**
+ * Tests the immutability of {@link IntentId}.
+ */
+ @Test
+ public void intentIdFollowsGuidelineForImmutableObject() {
+ ImmutableClassChecker.assertThatClassIsImmutable(IntentId.class);
+ }
+
+ /**
+ * Tests equality of {@link IntentId}.
+ */
+ @Test
+ public void testEquality() {
+ IntentId id1 = new IntentId(1L);
+ IntentId id2 = new IntentId(1L);
+
+ assertThat(id1, is(id2));
+ }
+
+ /**
+ * Tests non-equality of {@link IntentId}.
+ */
+ @Test
+ public void testNonEquality() {
+ IntentId id1 = new IntentId(1L);
+ IntentId id2 = new IntentId(2L);
+
+ assertThat(id1, is(not(id2)));
+ }
+
+ @Test
+ public void valueOf() {
+ IntentId id = new IntentId(12345);
+ assertEquals("incorrect valueOf", id, IntentId.valueOf("12345"));
+ }
+
+ @Test
+ public void valueOfHex() {
+ IntentId id = new IntentId(0xdeadbeefL);
+ assertEquals("incorrect valueOf", id, IntentId.valueOf(id.toString()));
+ }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/IntentServiceTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/IntentServiceTest.java
new file mode 100644
index 0000000..c7682b1
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/IntentServiceTest.java
@@ -0,0 +1,310 @@
+package org.onlab.onos.net.intent;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.onlab.onos.net.intent.IntentState.*;
+import static org.junit.Assert.*;
+
+// TODO: consider make it categorized as integration test when it become
+// slow test or fragile test
+/**
+ * Suite of tests for the intent service contract.
+ */
+public class IntentServiceTest {
+
+ public static final IntentId IID = new IntentId(123);
+ public static final IntentId INSTALLABLE_IID = new IntentId(234);
+
+ protected static final int GRACE_MS = 500; // millis
+
+ protected TestableIntentService service;
+ protected TestListener listener = new TestListener();
+
+ @Before
+ public void setUp() {
+ service = createIntentService();
+ service.addListener(listener);
+ }
+
+ @After
+ public void tearDown() {
+ service.removeListener(listener);
+ }
+
+ /**
+ * Creates a service instance appropriately instrumented for testing.
+ *
+ * @return testable intent service
+ */
+ protected TestableIntentService createIntentService() {
+ return new FakeIntentManager();
+ }
+
+ @Test
+ public void basics() {
+ // Make sure there are no intents
+ assertEquals("incorrect intent count", 0, service.getIntents().size());
+
+ // Register a compiler and an installer both setup for success.
+ service.registerCompiler(TestIntent.class, new TestCompiler(new TestInstallableIntent(INSTALLABLE_IID)));
+ service.registerInstaller(TestInstallableIntent.class, new TestInstaller(false));
+
+ final Intent intent = new TestIntent(IID);
+ service.submit(intent);
+
+ // Allow a small window of time until the intent is in the expected state
+ TestTools.assertAfter(GRACE_MS, new Runnable() {
+ @Override
+ public void run() {
+ assertEquals("incorrect intent state", INSTALLED,
+ service.getIntentState(intent.getId()));
+ }
+ });
+
+ // Make sure that all expected events have been emitted
+ validateEvents(intent, SUBMITTED, COMPILED, INSTALLED);
+
+ // Make sure there is just one intent (and is ours)
+ assertEquals("incorrect intent count", 1, service.getIntents().size());
+ assertEquals("incorrect intent", intent, service.getIntent(intent.getId()));
+
+ // Reset the listener events
+ listener.events.clear();
+
+ // Now withdraw the intent
+ service.withdraw(intent);
+
+ // Allow a small window of time until the event is in the expected state
+ TestTools.assertAfter(GRACE_MS, new Runnable() {
+ @Override
+ public void run() {
+ assertEquals("incorrect intent state", WITHDRAWN,
+ service.getIntentState(intent.getId()));
+ }
+ });
+
+ // Make sure that all expected events have been emitted
+ validateEvents(intent, WITHDRAWING, WITHDRAWN);
+
+ // TODO: discuss what is the fate of intents after they have been withdrawn
+ // Make sure that the intent is no longer in the system
+// assertEquals("incorrect intent count", 0, service.getIntents().size());
+// assertNull("intent should not be found", service.getIntent(intent.getId()));
+// assertNull("intent state should not be found", service.getIntentState(intent.getId()));
+ }
+
+ @Test
+ public void failedCompilation() {
+ // Register a compiler programmed for success
+ service.registerCompiler(TestIntent.class, new TestCompiler(true));
+
+ // Submit an intent
+ final Intent intent = new TestIntent(IID);
+ service.submit(intent);
+
+ // Allow a small window of time until the intent is in the expected state
+ TestTools.assertAfter(GRACE_MS, new Runnable() {
+ @Override
+ public void run() {
+ assertEquals("incorrect intent state", FAILED,
+ service.getIntentState(intent.getId()));
+ }
+ });
+
+ // Make sure that all expected events have been emitted
+ validateEvents(intent, SUBMITTED, FAILED);
+ }
+
+ @Test
+ public void failedInstallation() {
+ // Register a compiler programmed for success and installer for failure
+ service.registerCompiler(TestIntent.class, new TestCompiler(new TestInstallableIntent(INSTALLABLE_IID)));
+ service.registerInstaller(TestInstallableIntent.class, new TestInstaller(true));
+
+ // Submit an intent
+ final Intent intent = new TestIntent(IID);
+ service.submit(intent);
+
+ // Allow a small window of time until the intent is in the expected state
+ TestTools.assertAfter(GRACE_MS, new Runnable() {
+ @Override
+ public void run() {
+ assertEquals("incorrect intent state", FAILED,
+ service.getIntentState(intent.getId()));
+ }
+ });
+
+ // Make sure that all expected events have been emitted
+ validateEvents(intent, SUBMITTED, COMPILED, FAILED);
+ }
+
+ /**
+ * Validates that the test event listener has received the following events
+ * for the specified intent. Events received for other intents will not be
+ * considered.
+ *
+ * @param intent intent subject
+ * @param states list of states for which events are expected
+ */
+ protected void validateEvents(Intent intent, IntentState... states) {
+ Iterator<IntentEvent> events = listener.events.iterator();
+ for (IntentState state : states) {
+ IntentEvent event = events.hasNext() ? events.next() : null;
+ if (event == null) {
+ fail("expected event not found: " + state);
+ } else if (intent.equals(event.getIntent())) {
+ assertEquals("incorrect state", state, event.getState());
+ }
+ }
+
+ // Remainder of events should not apply to this intent; make sure.
+ while (events.hasNext()) {
+ assertFalse("unexpected event for intent",
+ intent.equals(events.next().getIntent()));
+ }
+ }
+
+ @Test
+ public void compilerBasics() {
+ // Make sure there are no compilers
+ assertEquals("incorrect compiler count", 0, service.getCompilers().size());
+
+ // Add a compiler and make sure that it appears in the map
+ IntentCompiler<TestIntent> compiler = new TestCompiler(false);
+ service.registerCompiler(TestIntent.class, compiler);
+ assertEquals("incorrect compiler", compiler,
+ service.getCompilers().get(TestIntent.class));
+
+ // Remove the same and make sure that it no longer appears in the map
+ service.unregisterCompiler(TestIntent.class);
+ assertNull("compiler should not be registered",
+ service.getCompilers().get(TestIntent.class));
+ }
+
+ @Test
+ public void installerBasics() {
+ // Make sure there are no installers
+ assertEquals("incorrect installer count", 0, service.getInstallers().size());
+
+ // Add an installer and make sure that it appears in the map
+ IntentInstaller<TestInstallableIntent> installer = new TestInstaller(false);
+ service.registerInstaller(TestInstallableIntent.class, installer);
+ assertEquals("incorrect installer", installer,
+ service.getInstallers().get(TestInstallableIntent.class));
+
+ // Remove the same and make sure that it no longer appears in the map
+ service.unregisterInstaller(TestInstallableIntent.class);
+ assertNull("installer should not be registered",
+ service.getInstallers().get(TestInstallableIntent.class));
+ }
+
+ @Test
+ public void implicitRegistration() {
+ // Add a compiler and make sure that it appears in the map
+ IntentCompiler<TestIntent> compiler = new TestCompiler(new TestSubclassInstallableIntent(INSTALLABLE_IID));
+ service.registerCompiler(TestIntent.class, compiler);
+ assertEquals("incorrect compiler", compiler,
+ service.getCompilers().get(TestIntent.class));
+
+ // Add a installer and make sure that it appears in the map
+ IntentInstaller<TestInstallableIntent> installer = new TestInstaller(false);
+ service.registerInstaller(TestInstallableIntent.class, installer);
+ assertEquals("incorrect installer", installer,
+ service.getInstallers().get(TestInstallableIntent.class));
+
+
+ // Submit an intent which is a subclass of the one we registered
+ final Intent intent = new TestSubclassIntent(IID);
+ service.submit(intent);
+
+ // Allow some time for the intent to be compiled and installed
+ TestTools.assertAfter(GRACE_MS, new Runnable() {
+ @Override
+ public void run() {
+ assertEquals("incorrect intent state", INSTALLED,
+ service.getIntentState(intent.getId()));
+ }
+ });
+
+ // Make sure that now we have an implicit registration of the compiler
+ // under the intent subclass
+ assertEquals("incorrect compiler", compiler,
+ service.getCompilers().get(TestSubclassIntent.class));
+
+ // Make sure that now we have an implicit registration of the installer
+ // under the intent subclass
+ assertEquals("incorrect installer", installer,
+ service.getInstallers().get(TestSubclassInstallableIntent.class));
+
+ // TODO: discuss whether or if implicit registration should require implicit unregistration
+ // perhaps unregister by compiler or installer itself, rather than by class would be better
+ }
+
+
+ // Fixture to track emitted intent events
+ protected class TestListener implements IntentEventListener {
+ final List<IntentEvent> events = new ArrayList<>();
+
+ @Override
+ public void event(IntentEvent event) {
+ events.add(event);
+ }
+ }
+
+ // Controllable compiler
+ private class TestCompiler implements IntentCompiler<TestIntent> {
+ private final boolean fail;
+ private final List<Intent> result;
+
+ TestCompiler(boolean fail) {
+ this.fail = fail;
+ this.result = Collections.emptyList();
+ }
+
+ TestCompiler(Intent... result) {
+ this.fail = false;
+ this.result = Arrays.asList(result);
+ }
+
+ @Override
+ public List<Intent> compile(TestIntent intent) {
+ if (fail) {
+ throw new IntentException("compile failed by design");
+ }
+ List<Intent> compiled = new ArrayList<>(result);
+ return compiled;
+ }
+ }
+
+ // Controllable installer
+ private class TestInstaller implements IntentInstaller<TestInstallableIntent> {
+ private final boolean fail;
+
+ TestInstaller(boolean fail) {
+ this.fail = fail;
+ }
+
+ @Override
+ public void install(TestInstallableIntent intent) {
+ if (fail) {
+ throw new IntentException("install failed by design");
+ }
+ }
+
+ @Override
+ public void uninstall(TestInstallableIntent intent) {
+ if (fail) {
+ throw new IntentException("remove failed by design");
+ }
+ }
+ }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/IntentTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/IntentTest.java
new file mode 100644
index 0000000..a6cedf9
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/IntentTest.java
@@ -0,0 +1,65 @@
+package org.onlab.onos.net.intent;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Test;
+
+/**
+ * Base facilities to test various intent tests.
+ */
+public abstract class IntentTest {
+ /**
+ * Produces a set of items from the supplied items.
+ *
+ * @param items items to be placed in set
+ * @param <T> item type
+ * @return set of items
+ */
+ protected static <T> Set<T> itemSet(T[] items) {
+ return new HashSet<>(Arrays.asList(items));
+ }
+
+ @Test
+ public void equalsAndHashCode() {
+ Intent one = createOne();
+ Intent like = createOne();
+ Intent another = createAnother();
+
+ assertTrue("should be equal", one.equals(like));
+ assertEquals("incorrect hashCode", one.hashCode(), like.hashCode());
+
+ assertFalse("should not be equal", one.equals(another));
+
+ assertFalse("should not be equal", one.equals(null));
+ assertFalse("should not be equal", one.equals("foo"));
+ }
+
+ @Test
+ public void testToString() {
+ Intent one = createOne();
+ Intent like = createOne();
+ assertEquals("incorrect toString", one.toString(), like.toString());
+ }
+
+ /**
+ * Creates a new intent, but always a like intent, i.e. all instances will
+ * be equal, but should not be the same.
+ *
+ * @return intent
+ */
+ protected abstract Intent createOne();
+
+ /**
+ * Creates another intent, not equals to the one created by
+ * {@link #createOne()} and with a different hash code.
+ *
+ * @return another intent
+ */
+ protected abstract Intent createAnother();
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntentTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntentTest.java
new file mode 100644
index 0000000..d971ba2
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntentTest.java
@@ -0,0 +1,30 @@
+package org.onlab.onos.net.intent;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Suite of tests of the multi-to-single point intent descriptor.
+ */
+public class MultiPointToSinglePointIntentTest extends ConnectivityIntentTest {
+
+ @Test
+ public void basics() {
+ MultiPointToSinglePointIntent intent = createOne();
+ assertEquals("incorrect id", IID, intent.getId());
+ assertEquals("incorrect match", MATCH, intent.getTrafficSelector());
+ assertEquals("incorrect ingress", PS1, intent.getIngressPorts());
+ assertEquals("incorrect egress", P2, intent.getEgressPort());
+ }
+
+ @Override
+ protected MultiPointToSinglePointIntent createOne() {
+ return new MultiPointToSinglePointIntent(IID, MATCH, NOP, PS1, P2);
+ }
+
+ @Override
+ protected MultiPointToSinglePointIntent createAnother() {
+ return new MultiPointToSinglePointIntent(IID, MATCH, NOP, PS2, P1);
+ }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/PathIntentTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/PathIntentTest.java
new file mode 100644
index 0000000..bd8dc08
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/PathIntentTest.java
@@ -0,0 +1,36 @@
+package org.onlab.onos.net.intent;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.onlab.onos.net.NetTestTools;
+import org.onlab.onos.net.Path;
+
+public class PathIntentTest extends ConnectivityIntentTest {
+ // 111:11 --> 222:22
+ private static final Path PATH1 = NetTestTools.createPath("111", "222");
+
+ // 111:11 --> 333:33
+ private static final Path PATH2 = NetTestTools.createPath("222", "333");
+
+ @Test
+ public void basics() {
+ PathIntent intent = createOne();
+ assertEquals("incorrect id", IID, intent.getId());
+ assertEquals("incorrect match", MATCH, intent.getTrafficSelector());
+ assertEquals("incorrect action", NOP, intent.getTrafficTreatment());
+ assertEquals("incorrect ingress", P1, intent.getIngressPort());
+ assertEquals("incorrect egress", P2, intent.getEgressPort());
+ assertEquals("incorrect path", PATH1, intent.getPath());
+ }
+
+ @Override
+ protected PathIntent createOne() {
+ return new PathIntent(IID, MATCH, NOP, P1, P2, PATH1);
+ }
+
+ @Override
+ protected PathIntent createAnother() {
+ return new PathIntent(IID, MATCH, NOP, P1, P3, PATH2);
+ }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/PointToPointIntentTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/PointToPointIntentTest.java
new file mode 100644
index 0000000..426a3d9
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/PointToPointIntentTest.java
@@ -0,0 +1,30 @@
+package org.onlab.onos.net.intent;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Suite of tests of the point-to-point intent descriptor.
+ */
+public class PointToPointIntentTest extends ConnectivityIntentTest {
+
+ @Test
+ public void basics() {
+ PointToPointIntent intent = createOne();
+ assertEquals("incorrect id", IID, intent.getId());
+ assertEquals("incorrect match", MATCH, intent.getTrafficSelector());
+ assertEquals("incorrect ingress", P1, intent.getIngressPort());
+ assertEquals("incorrect egress", P2, intent.getEgressPort());
+ }
+
+ @Override
+ protected PointToPointIntent createOne() {
+ return new PointToPointIntent(IID, MATCH, NOP, P1, P2);
+ }
+
+ @Override
+ protected PointToPointIntent createAnother() {
+ return new PointToPointIntent(IID, MATCH, NOP, P2, P1);
+ }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntentTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntentTest.java
new file mode 100644
index 0000000..0561a87
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntentTest.java
@@ -0,0 +1,30 @@
+package org.onlab.onos.net.intent;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Suite of tests of the single-to-multi point intent descriptor.
+ */
+public class SinglePointToMultiPointIntentTest extends ConnectivityIntentTest {
+
+ @Test
+ public void basics() {
+ SinglePointToMultiPointIntent intent = createOne();
+ assertEquals("incorrect id", IID, intent.getId());
+ assertEquals("incorrect match", MATCH, intent.getTrafficSelector());
+ assertEquals("incorrect ingress", P1, intent.getIngressPort());
+ assertEquals("incorrect egress", PS2, intent.getEgressPorts());
+ }
+
+ @Override
+ protected SinglePointToMultiPointIntent createOne() {
+ return new SinglePointToMultiPointIntent(IID, MATCH, NOP, P1, PS2);
+ }
+
+ @Override
+ protected SinglePointToMultiPointIntent createAnother() {
+ return new SinglePointToMultiPointIntent(IID, MATCH, NOP, P2, PS1);
+ }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/TestInstallableIntent.java b/core/api/src/test/java/org/onlab/onos/net/intent/TestInstallableIntent.java
new file mode 100644
index 0000000..a6ce52e
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/TestInstallableIntent.java
@@ -0,0 +1,28 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+/**
+ * An installable intent used in the unit test.
+ *
+ * FIXME: we don't want to expose this class publicly, but the current Kryo
+ * serialization mechanism does not allow this class to be private and placed
+ * on testing directory.
+ */
+public class TestInstallableIntent extends AbstractIntent implements InstallableIntent {
+ /**
+ * Constructs an instance with the specified intent ID.
+ *
+ * @param id intent ID
+ */
+ public TestInstallableIntent(IntentId id) {
+ super(id);
+ }
+
+ /**
+ * Constructor for serializer.
+ */
+ protected TestInstallableIntent() {
+ super();
+ }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/TestIntent.java b/core/api/src/test/java/org/onlab/onos/net/intent/TestIntent.java
new file mode 100644
index 0000000..2f30727
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/TestIntent.java
@@ -0,0 +1,27 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+/**
+ * An intent used in the unit test.
+ *
+ * FIXME: we don't want to expose this class publicly, but the current Kryo
+ * serialization mechanism does not allow this class to be private and placed
+ * on testing directory.
+ */
+public class TestIntent extends AbstractIntent {
+ /**
+ * Constructs an instance with the specified intent ID.
+ *
+ * @param id intent ID
+ */
+ public TestIntent(IntentId id) {
+ super(id);
+ }
+
+ /**
+ * Constructor for serializer.
+ */
+ protected TestIntent() {
+ super();
+ }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/TestSubclassInstallableIntent.java b/core/api/src/test/java/org/onlab/onos/net/intent/TestSubclassInstallableIntent.java
new file mode 100644
index 0000000..40765c2
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/TestSubclassInstallableIntent.java
@@ -0,0 +1,27 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+/**
+ * An intent used in the unit test.
+ *
+ * FIXME: we don't want to expose this class publicly, but the current Kryo
+ * serialization mechanism does not allow this class to be private and placed
+ * on testing directory.
+ */
+public class TestSubclassInstallableIntent extends TestInstallableIntent implements InstallableIntent {
+ /**
+ * Constructs an instance with the specified intent ID.
+ *
+ * @param id intent ID
+ */
+ public TestSubclassInstallableIntent(IntentId id) {
+ super(id);
+ }
+
+ /**
+ * Constructor for serializer.
+ */
+ protected TestSubclassInstallableIntent() {
+ super();
+ }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/TestSubclassIntent.java b/core/api/src/test/java/org/onlab/onos/net/intent/TestSubclassIntent.java
new file mode 100644
index 0000000..43bb0dd
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/TestSubclassIntent.java
@@ -0,0 +1,27 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+/**
+ * An intent used in the unit test.
+ *
+ * FIXME: we don't want to expose this class publicly, but the current Kryo
+ * serialization mechanism does not allow this class to be private and placed
+ * on testing directory.
+ */
+public class TestSubclassIntent extends TestIntent {
+ /**
+ * Constructs an instance with the specified intent ID.
+ *
+ * @param id intent ID
+ */
+ public TestSubclassIntent(IntentId id) {
+ super(id);
+ }
+
+ /**
+ * Constructor for serializer.
+ */
+ protected TestSubclassIntent() {
+ super();
+ }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/TestTools.java b/core/api/src/test/java/org/onlab/onos/net/intent/TestTools.java
new file mode 100644
index 0000000..f22585e
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/TestTools.java
@@ -0,0 +1,126 @@
+package org.onlab.onos.net.intent;
+
+import static org.junit.Assert.fail;
+
+/**
+ * Set of test tools.
+ */
+public final class TestTools {
+
+ // Disallow construction
+ private TestTools() {
+ }
+
+ /**
+ * Utility method to pause the current thread for the specified number of
+ * milliseconds.
+ *
+ * @param ms number of milliseconds to pause
+ */
+ public static void delay(int ms) {
+ try {
+ Thread.sleep(ms);
+ } catch (InterruptedException e) {
+ fail("unexpected interrupt");
+ }
+ }
+
+ /**
+ * Periodically runs the given runnable, which should contain a series of
+ * test assertions until all the assertions succeed, in which case it will
+ * return, or until the the time expires, in which case it will throw the
+ * first failed assertion error.
+ *
+ * @param start start time, in millis since start of epoch from which the
+ * duration will be measured
+ * @param delay initial delay (in milliseconds) before the first assertion
+ * attempt
+ * @param step delay (in milliseconds) between successive assertion
+ * attempts
+ * @param duration number of milliseconds beyond the given start time,
+ * after which the failed assertions will be propagated and allowed
+ * to fail the test
+ * @param assertions runnable housing the test assertions
+ */
+ public static void assertAfter(long start, int delay, int step,
+ int duration, Runnable assertions) {
+ delay(delay);
+ while (true) {
+ try {
+ assertions.run();
+ break;
+ } catch (AssertionError e) {
+ if (System.currentTimeMillis() - start > duration) {
+ throw e;
+ }
+ }
+ delay(step);
+ }
+ }
+
+ /**
+ * Periodically runs the given runnable, which should contain a series of
+ * test assertions until all the assertions succeed, in which case it will
+ * return, or until the the time expires, in which case it will throw the
+ * first failed assertion error.
+ * <p>
+ * The start of the period is the current time.
+ *
+ * @param delay initial delay (in milliseconds) before the first assertion
+ * attempt
+ * @param step delay (in milliseconds) between successive assertion
+ * attempts
+ * @param duration number of milliseconds beyond the current time time,
+ * after which the failed assertions will be propagated and allowed
+ * to fail the test
+ * @param assertions runnable housing the test assertions
+ */
+ public static void assertAfter(int delay, int step, int duration,
+ Runnable assertions) {
+ assertAfter(System.currentTimeMillis(), delay, step, duration,
+ assertions);
+ }
+
+ /**
+ * Periodically runs the given runnable, which should contain a series of
+ * test assertions until all the assertions succeed, in which case it will
+ * return, or until the the time expires, in which case it will throw the
+ * first failed assertion error.
+ * <p>
+ * The start of the period is the current time and the first assertion
+ * attempt is delayed by the value of {@code step} parameter.
+ *
+ * @param step delay (in milliseconds) between successive assertion
+ * attempts
+ * @param duration number of milliseconds beyond the current time time,
+ * after which the failed assertions will be propagated and allowed
+ * to fail the test
+ * @param assertions runnable housing the test assertions
+ */
+ public static void assertAfter(int step, int duration,
+ Runnable assertions) {
+ assertAfter(step, step, duration, assertions);
+ }
+
+ /**
+ * Periodically runs the given runnable, which should contain a series of
+ * test assertions until all the assertions succeed, in which case it will
+ * return, or until the the time expires, in which case it will throw the
+ * first failed assertion error.
+ * <p>
+ * The start of the period is the current time and each successive
+ * assertion attempt is delayed by at least 10 milliseconds unless the
+ * {@code duration} is less than that, in which case the one and only
+ * assertion is made after that delay.
+ *
+ * @param duration number of milliseconds beyond the current time,
+ * after which the failed assertions will be propagated and allowed
+ * to fail the test
+ * @param assertions runnable housing the test assertions
+ */
+ public static void assertAfter(int duration, Runnable assertions) {
+ int step = Math.min(duration, Math.max(10, duration / 10));
+ assertAfter(step, duration, assertions);
+ }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/TestableIntentService.java b/core/api/src/test/java/org/onlab/onos/net/intent/TestableIntentService.java
new file mode 100644
index 0000000..95502d3
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/TestableIntentService.java
@@ -0,0 +1,12 @@
+package org.onlab.onos.net.intent;
+
+import java.util.List;
+
+/**
+ * Abstraction of an extensible intent service enabled for unit tests.
+ */
+public interface TestableIntentService extends IntentService, IntentExtensionService {
+
+ List<IntentException> getExceptions();
+
+}