Adding Intent API tests
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");
+            }
+        }
+    }
+
+}