Implement IntentInstallers

IntentInstallers for:
- PathFlowIntent
- SingleDstTreeFlowIntent
- SingleSrcTreeFlowIntent

This resolves ONOS-1884, ONOS-1885, and ONOS-1886.

Change-Id: Ie0eac4bab9a2898b582edf2e4291498f0a583d71
diff --git a/src/main/java/net/onrc/onos/core/newintent/AbstractIntentInstaller.java b/src/main/java/net/onrc/onos/core/newintent/AbstractIntentInstaller.java
new file mode 100644
index 0000000..9d21771
--- /dev/null
+++ b/src/main/java/net/onrc/onos/core/newintent/AbstractIntentInstaller.java
@@ -0,0 +1,185 @@
+package net.onrc.onos.core.newintent;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import net.onrc.onos.api.flowmanager.Flow;
+import net.onrc.onos.api.flowmanager.FlowBatchHandle;
+import net.onrc.onos.api.flowmanager.FlowBatchStateChangedEvent;
+import net.onrc.onos.api.flowmanager.FlowId;
+import net.onrc.onos.api.flowmanager.FlowManagerListener;
+import net.onrc.onos.api.flowmanager.FlowManagerService;
+import net.onrc.onos.api.flowmanager.FlowState;
+import net.onrc.onos.api.flowmanager.FlowStateChange;
+import net.onrc.onos.api.flowmanager.FlowStatesChangedEvent;
+import net.onrc.onos.api.newintent.InstallableIntent;
+import net.onrc.onos.api.newintent.Intent;
+import net.onrc.onos.api.newintent.IntentInstaller;
+
+import java.util.concurrent.CountDownLatch;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static net.onrc.onos.api.flowmanager.FlowState.FAILED;
+import static net.onrc.onos.api.flowmanager.FlowState.INSTALLED;
+import static net.onrc.onos.api.flowmanager.FlowState.WITHDRAWN;
+
+// TODO: consider naming because to call Flow manager's API will be removed
+// in long-term refactoring
+// TODO: consider unifying the install() and remove() by pulling up to this class
+/**
+ * Base class for implementing an intent installer, which use Flow Manager's API.
+ *
+ * @param <T> the type of intent
+ */
+public abstract class AbstractIntentInstaller<T extends InstallableIntent>
+        implements IntentInstaller<T> {
+    protected final FlowManagerService flowManager;
+
+    /**
+     * Constructs a base class with the specified Flow Manager service.
+     *
+     * @param flowManager Flow manager service, which is used to install/remove
+     *                    an intent
+     */
+    protected AbstractIntentInstaller(FlowManagerService flowManager) {
+        this.flowManager = flowManager;
+    }
+
+    protected void installFlow(Intent intent, Flow flow) {
+        InstallationListener listener = new InstallationListener(flow.getId());
+        flowManager.addListener(listener);
+
+        FlowBatchHandle handle = flowManager.addFlow(flow);
+        if (handle == null) {
+            throw new IntentInstallationException("intent installation failed: " + intent);
+        }
+
+        try {
+            listener.await();
+            if (listener.getFinalState() == FAILED) {
+                throw new IntentInstallationException("intent installation failed: " + intent);
+            }
+        } catch (InterruptedException e) {
+            throw new IntentInstallationException("intent installation failed: " + intent, e);
+        } finally {
+            flowManager.removeListener(listener);
+        }
+    }
+
+    protected void removeFlow(Intent intent, Flow flow) {
+        RemovalListener listener = new RemovalListener(flow.getId());
+        flowManager.addListener(listener);
+
+        FlowBatchHandle handle = flowManager.removeFlow(flow.getId());
+        if (handle == null) {
+            throw new IntentRemovalException("intent removal failed: " + intent);
+        }
+
+
+        try {
+            listener.await();
+            if (listener.getFinalState() == FAILED) {
+                throw new IntentInstallationException("intent removal failed: " + intent);
+            }
+        } catch (InterruptedException e) {
+            throw new IntentInstallationException("intent removal failed: " + intent, e);
+        } finally {
+            flowManager.removeListener(listener);
+        }
+    }
+
+    protected abstract static class SyncListener implements FlowManagerListener {
+        protected final FlowId target;
+        protected final CountDownLatch latch = new CountDownLatch(1);
+        protected FlowState finalState;
+
+        protected SyncListener(FlowId target) {
+            this.target = checkNotNull(target);
+        }
+
+        protected Optional<FlowStateChange> findTargetFlow(FlowStatesChangedEvent event) {
+            return Iterables.tryFind(event.getStateChanges(), new Predicate<FlowStateChange>() {
+                @Override
+                public boolean apply(FlowStateChange stateChange) {
+                    return stateChange.getFlowId().equals(target);
+                }
+            });
+        }
+
+        public FlowState getFinalState() {
+            return finalState;
+        }
+
+        public void await() throws InterruptedException {
+            latch.await();
+        }
+    }
+
+    protected static class InstallationListener extends SyncListener {
+        public InstallationListener(FlowId target) {
+            super(target);
+        }
+
+        @Override
+        public void flowStatesChanged(FlowStatesChangedEvent event) {
+            Optional<FlowStateChange> optional = findTargetFlow(event);
+
+            if (!optional.isPresent()) {
+                return;
+            }
+
+            FlowStateChange stateChange = optional.get();
+            switch (stateChange.getCurrentState()) {
+                case INSTALLED:
+                    latch.countDown();
+                    finalState = INSTALLED;
+                    break;
+                case FAILED:
+                    latch.countDown();
+                    finalState = FAILED;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        @Override
+        public void flowBatchStateChanged(FlowBatchStateChangedEvent event) {
+            // nop
+        }
+    }
+
+    protected static class RemovalListener extends SyncListener {
+        public RemovalListener(FlowId target) {
+            super(target);
+        }
+
+        @Override
+        public void flowStatesChanged(FlowStatesChangedEvent event) {
+            Optional<FlowStateChange> optional = findTargetFlow(event);
+
+            if (!optional.isPresent()) {
+                return;
+            }
+
+            FlowStateChange stateChange = optional.get();
+            switch (stateChange.getCurrentState()) {
+                case WITHDRAWN:
+                    latch.countDown();
+                    finalState = WITHDRAWN;
+                    break;
+                case FAILED:
+                    latch.countDown();
+                    finalState = FAILED;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        @Override
+        public void flowBatchStateChanged(FlowBatchStateChangedEvent event) {
+            // nop
+        }
+    }
+}
diff --git a/src/main/java/net/onrc/onos/core/newintent/IdBlockAllocatorBasedIntentIdGenerator.java b/src/main/java/net/onrc/onos/core/newintent/IdBlockAllocatorBasedIntentIdGenerator.java
index 2e09698..9137b29 100644
--- a/src/main/java/net/onrc/onos/core/newintent/IdBlockAllocatorBasedIntentIdGenerator.java
+++ b/src/main/java/net/onrc/onos/core/newintent/IdBlockAllocatorBasedIntentIdGenerator.java
@@ -3,8 +3,8 @@
 import net.onrc.onos.api.newintent.IntentId;
 import net.onrc.onos.api.newintent.IntentIdGenerator;
 import net.onrc.onos.core.util.IdBlock;
-import net.onrc.onos.core.util.UnavailableIdException;
 import net.onrc.onos.core.util.IdBlockAllocator;
+import net.onrc.onos.core.util.UnavailableIdException;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
diff --git a/src/main/java/net/onrc/onos/core/newintent/IntentInstallationException.java b/src/main/java/net/onrc/onos/core/newintent/IntentInstallationException.java
new file mode 100644
index 0000000..4dca400
--- /dev/null
+++ b/src/main/java/net/onrc/onos/core/newintent/IntentInstallationException.java
@@ -0,0 +1,22 @@
+package net.onrc.onos.core.newintent;
+
+import net.onrc.onos.api.newintent.IntentException;
+
+/**
+ * An exception thrown when intent installation fails.
+ */
+public class IntentInstallationException extends IntentException {
+    private static final long serialVersionUID = 3720268258616014168L;
+
+    public IntentInstallationException() {
+        super();
+    }
+
+    public IntentInstallationException(String message) {
+        super(message);
+    }
+
+    public IntentInstallationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/main/java/net/onrc/onos/core/newintent/IntentRemovalException.java b/src/main/java/net/onrc/onos/core/newintent/IntentRemovalException.java
new file mode 100644
index 0000000..1c8549f
--- /dev/null
+++ b/src/main/java/net/onrc/onos/core/newintent/IntentRemovalException.java
@@ -0,0 +1,22 @@
+package net.onrc.onos.core.newintent;
+
+import net.onrc.onos.api.newintent.IntentException;
+
+/**
+ * An exception thrown when intent removal failed.
+ */
+public class IntentRemovalException extends IntentException {
+    private static final long serialVersionUID = -5259226322037891951L;
+
+    public IntentRemovalException() {
+        super();
+    }
+
+    public IntentRemovalException(String message) {
+        super(message);
+    }
+
+    public IntentRemovalException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/main/java/net/onrc/onos/core/newintent/PathFlowIntentInstaller.java b/src/main/java/net/onrc/onos/core/newintent/PathFlowIntentInstaller.java
new file mode 100644
index 0000000..7066503
--- /dev/null
+++ b/src/main/java/net/onrc/onos/core/newintent/PathFlowIntentInstaller.java
@@ -0,0 +1,30 @@
+package net.onrc.onos.core.newintent;
+
+import net.onrc.onos.api.flowmanager.FlowManagerService;
+
+/**
+ * An intent installer for {@link PathFlowIntent}.
+ */
+public class PathFlowIntentInstaller
+        extends AbstractIntentInstaller<PathFlowIntent> {
+    /**
+     * Constructs an intent installer for {@link PathFlowIntent} with the
+     * specified Flow Manager service, which is used in this installer.
+     *
+     * @param flowManager Flow Manager service, which is used
+     *                    to install/remove an intent
+     */
+    public PathFlowIntentInstaller(FlowManagerService flowManager) {
+        super(flowManager);
+    }
+
+    @Override
+    public void install(PathFlowIntent intent) {
+        installFlow(intent, intent.getFlow());
+    }
+
+    @Override
+    public void remove(PathFlowIntent intent) {
+        removeFlow(intent, intent.getFlow());
+    }
+}
diff --git a/src/main/java/net/onrc/onos/core/newintent/SingleDstTreeFlowIntentInstaller.java b/src/main/java/net/onrc/onos/core/newintent/SingleDstTreeFlowIntentInstaller.java
new file mode 100644
index 0000000..f79237b
--- /dev/null
+++ b/src/main/java/net/onrc/onos/core/newintent/SingleDstTreeFlowIntentInstaller.java
@@ -0,0 +1,31 @@
+package net.onrc.onos.core.newintent;
+
+import net.onrc.onos.api.flowmanager.FlowManagerService;
+
+/**
+ * An intent installer for {@link SingleDstTreeFlowIntent}.
+ */
+public class SingleDstTreeFlowIntentInstaller
+        extends AbstractIntentInstaller<SingleDstTreeFlowIntent> {
+
+    /**
+     * Constructs an intent installer for {@link SingleDstTreeFlowIntent} with
+     * the specified Flow Manager service, which is used in this installer.
+     *
+     * @param flowManager Flow Manager service, which is used
+     *                    to install/remove an intent
+     */
+    public SingleDstTreeFlowIntentInstaller(FlowManagerService flowManager) {
+        super(flowManager);
+    }
+
+    @Override
+    public void install(SingleDstTreeFlowIntent intent) {
+        installFlow(intent, intent.getTree());
+    }
+
+    @Override
+    public void remove(SingleDstTreeFlowIntent intent) {
+        removeFlow(intent, intent.getTree());
+    }
+}
diff --git a/src/main/java/net/onrc/onos/core/newintent/SingleSrcTreeFlowIntentInstaller.java b/src/main/java/net/onrc/onos/core/newintent/SingleSrcTreeFlowIntentInstaller.java
new file mode 100644
index 0000000..38aec87
--- /dev/null
+++ b/src/main/java/net/onrc/onos/core/newintent/SingleSrcTreeFlowIntentInstaller.java
@@ -0,0 +1,31 @@
+package net.onrc.onos.core.newintent;
+
+import net.onrc.onos.api.flowmanager.FlowManagerService;
+
+/**
+ * An intent installer for {@link SingleSrcTreeFlowIntent}.
+ */
+public class SingleSrcTreeFlowIntentInstaller
+        extends AbstractIntentInstaller<SingleSrcTreeFlowIntent> {
+
+    /**
+     * Constructs an intent installer for {@link SingleSrcTreeFlowIntent}
+     * with the specified Flow Manager service, which is used in this class.
+     *
+     * @param flowManager Flow Manager service, which is used
+     *                    to install/remove an intent
+     */
+    public SingleSrcTreeFlowIntentInstaller(FlowManagerService flowManager) {
+        super(flowManager);
+    }
+
+    @Override
+    public void install(SingleSrcTreeFlowIntent intent) {
+        installFlow(intent, intent.getTree());
+    }
+
+    @Override
+    public void remove(SingleSrcTreeFlowIntent intent) {
+        removeFlow(intent, intent.getTree());
+    }
+}
diff --git a/src/test/java/net/onrc/onos/api/flowmanager/FakeFlowManagerService.java b/src/test/java/net/onrc/onos/api/flowmanager/FakeFlowManagerService.java
new file mode 100644
index 0000000..c231bca
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/flowmanager/FakeFlowManagerService.java
@@ -0,0 +1,119 @@
+package net.onrc.onos.api.flowmanager;
+
+import com.google.common.collect.ImmutableList;
+import net.onrc.onos.core.flowmanager.FlowBatchHandleImpl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Fake implementation of {@link FlowManagerService} for testing.
+ */
+public class FakeFlowManagerService implements FlowManagerService {
+    private final List<FlowManagerListener> listeners = new ArrayList<>();
+    private final ExecutorService executor = Executors.newSingleThreadExecutor();
+
+    private final FlowId target;
+    private final List<FlowState> transition;
+    private final boolean returnNull;
+
+    public FakeFlowManagerService(FlowId target, boolean returnNull, FlowState... transition) {
+        this.target = target;
+        this.transition = ImmutableList.copyOf(transition);
+        this.returnNull = returnNull;
+    }
+
+    public FlowBatchHandle addFlow(Flow flow) {
+        return processFlow();
+    }
+
+    @Override
+    public FlowBatchHandle removeFlow(FlowId id) {
+        return processFlow();
+    }
+
+    private FlowBatchHandle processFlow() {
+        if (returnNull) {
+            return null;
+        }
+
+        executor.execute(new Runnable() {
+            @Override
+            public void run() {
+                changeStates();
+            }
+        });
+
+        // This is a test-only workaround. Passing null to the constructor is harmful,
+        // but we could not create a FlowOperationMap instance due to visibility of
+        // the constructor.
+        // TODO: consider correct visibility of the constructor and package structure
+        return new FlowBatchHandleImpl(null, new FlowBatchId(1));
+    }
+
+    private void changeStates() {
+        for (int i = 0; i < transition.size(); i++) {
+            FlowStateChange change;
+            if (i == 0) {
+                change = new FlowStateChange(target,
+                        transition.get(i), null);
+            } else {
+                change = new FlowStateChange(target,
+                        transition.get(i), transition.get(i - 1));
+            }
+            HashSet<FlowStateChange> changes = new HashSet<>(Arrays.asList(change));
+            invokeListeners(new FlowStatesChangedEvent(System.currentTimeMillis(), changes));
+        }
+    }
+
+    @Override
+    public Flow getFlow(FlowId id) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Collection<Flow> getFlows() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public FlowBatchHandle executeBatch(FlowBatchOperation ops) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public FlowIdGenerator getFlowIdGenerator() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setConflictDetectionPolicy(ConflictDetectionPolicy policy) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ConflictDetectionPolicy getConflictDetectionPolicy() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void addListener(FlowManagerListener listener) {
+        listeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(FlowManagerListener listener) {
+        listeners.remove(listener);
+    }
+
+    private void invokeListeners(FlowStatesChangedEvent event) {
+        for (FlowManagerListener listener: listeners) {
+            listener.flowStatesChanged(event);
+        }
+    }
+}
diff --git a/src/test/java/net/onrc/onos/core/newintent/PathFlowIntentInstallerTest.java b/src/test/java/net/onrc/onos/core/newintent/PathFlowIntentInstallerTest.java
new file mode 100644
index 0000000..99863d9
--- /dev/null
+++ b/src/test/java/net/onrc/onos/core/newintent/PathFlowIntentInstallerTest.java
@@ -0,0 +1,88 @@
+package net.onrc.onos.core.newintent;
+
+import net.onrc.onos.api.flowmanager.FakeFlowManagerService;
+import net.onrc.onos.api.flowmanager.FlowId;
+import net.onrc.onos.api.flowmanager.FlowLink;
+import net.onrc.onos.api.flowmanager.FlowManagerService;
+import net.onrc.onos.api.flowmanager.PacketPathFlow;
+import net.onrc.onos.api.flowmanager.Path;
+import net.onrc.onos.api.newintent.IntentId;
+import net.onrc.onos.core.matchaction.action.Action;
+import net.onrc.onos.core.matchaction.match.PacketMatch;
+import net.onrc.onos.core.matchaction.match.PacketMatchBuilder;
+import net.onrc.onos.core.util.SwitchPort;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static net.onrc.onos.api.flowmanager.FlowState.COMPILED;
+import static net.onrc.onos.api.flowmanager.FlowState.INSTALLED;
+import static net.onrc.onos.api.flowmanager.FlowState.SUBMITTED;
+import static net.onrc.onos.api.flowmanager.FlowState.WITHDRAWING;
+
+/**
+ * Suites of test of {@link PathFlowIntentInstaller}.
+ */
+public class PathFlowIntentInstallerTest {
+    private final FlowId flowId = new FlowId(1);
+    private final IntentId intentId = new IntentId(1);
+    private final PacketMatch match = new PacketMatchBuilder().build();
+    private SwitchPort src = new SwitchPort(1, (short) 1);
+    private SwitchPort dst = new SwitchPort(2, (short) 2);
+
+    /**
+     * Tests intent installation that the state is changed
+     * to SUBMITTED, COMPILED, then INSTALLED.
+     */
+    @Test
+    public void testNormalStateTransition() {
+        FlowManagerService flowManager =
+                new FakeFlowManagerService(flowId, false, SUBMITTED, COMPILED, INSTALLED);
+        PathFlowIntentInstaller sut =
+                new PathFlowIntentInstaller(flowManager);
+
+        PacketPathFlow flow = createFlow();
+        PathFlowIntent intent = new PathFlowIntent(intentId, flow);
+
+        sut.install(intent);
+    }
+
+    /**
+     * Tests intent installation that addFlow() returns null.
+     */
+    @Test(expected = IntentInstallationException.class)
+    public void testInstallationFails() {
+        FlowManagerService flowManager =
+                new FakeFlowManagerService(flowId, true, SUBMITTED);
+        PathFlowIntentInstaller sut =
+                new PathFlowIntentInstaller(flowManager);
+
+        PacketPathFlow flow = createFlow();
+        PathFlowIntent intent = new PathFlowIntent(intentId, flow);
+
+        sut.install(intent);
+    }
+
+    /**
+     * Tests intent removal that removeFlow() returns null.
+     */
+    @Test(expected = IntentRemovalException.class)
+    public void testRemovalFails() {
+        FlowManagerService flowManager =
+                new FakeFlowManagerService(flowId, true, WITHDRAWING);
+        PathFlowIntentInstaller sut =
+                new PathFlowIntentInstaller(flowManager);
+
+        PacketPathFlow flow = createFlow();
+        PathFlowIntent intent = new PathFlowIntent(intentId, flow);
+
+        sut.remove(intent);
+    }
+
+    private PacketPathFlow createFlow() {
+        return new PacketPathFlow(flowId, match, src.getPortNumber(),
+                new Path(Arrays.asList(new FlowLink(src, dst))),
+                Collections.<Action>emptyList(), 0, 0);
+    }
+}
diff --git a/src/test/java/net/onrc/onos/core/newintent/SingleDstTreeFlowIntentInstallerTest.java b/src/test/java/net/onrc/onos/core/newintent/SingleDstTreeFlowIntentInstallerTest.java
new file mode 100644
index 0000000..a653bdc
--- /dev/null
+++ b/src/test/java/net/onrc/onos/core/newintent/SingleDstTreeFlowIntentInstallerTest.java
@@ -0,0 +1,99 @@
+package net.onrc.onos.core.newintent;
+
+import net.onrc.onos.api.flowmanager.FakeFlowManagerService;
+import net.onrc.onos.api.flowmanager.FlowId;
+import net.onrc.onos.api.flowmanager.FlowLink;
+import net.onrc.onos.api.flowmanager.FlowManagerService;
+import net.onrc.onos.api.flowmanager.SingleDstTreeFlow;
+import net.onrc.onos.api.flowmanager.Tree;
+import net.onrc.onos.api.newintent.IntentId;
+import net.onrc.onos.core.matchaction.action.Action;
+import net.onrc.onos.core.matchaction.match.PacketMatchBuilder;
+import net.onrc.onos.core.util.SwitchPort;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static net.onrc.onos.api.flowmanager.FlowState.COMPILED;
+import static net.onrc.onos.api.flowmanager.FlowState.INSTALLED;
+import static net.onrc.onos.api.flowmanager.FlowState.SUBMITTED;
+import static net.onrc.onos.api.flowmanager.FlowState.WITHDRAWING;
+
+/**
+ * Suites of test of {@link SingleDstTreeFlowIntentInstaller}.
+ */
+public class SingleDstTreeFlowIntentInstallerTest {
+    private final SwitchPort port12 = new SwitchPort(1, (short) 1);
+    private final SwitchPort port13 = new SwitchPort(1, (short) 2);
+    private final SwitchPort port21 = new SwitchPort(2, (short) 2);
+    private final SwitchPort ingress1 = new SwitchPort(2, (short) 100);
+    private final SwitchPort ingress2 = new SwitchPort(3, (short) 101);
+    private final FlowId flowId = new FlowId(1);
+
+    /**
+     * Tests intent installation that the state is changed
+     * to SUBMITTED, COMPILED, then INSTALLED.
+     */
+    @Test
+    public void testNormalStateTransition() {
+        FlowManagerService flowManager =
+                new FakeFlowManagerService(flowId, false, SUBMITTED, COMPILED, INSTALLED);
+        SingleDstTreeFlowIntentInstaller sut =
+                new SingleDstTreeFlowIntentInstaller(flowManager);
+
+        Tree tree = createTree();
+        SingleDstTreeFlowIntent intent = new SingleDstTreeFlowIntent(new IntentId(1), createFlow(tree));
+
+        sut.install(intent);
+    }
+
+    /**
+     * Tests intent installation that addFlow() returns null.
+     */
+    @Test(expected = IntentInstallationException.class)
+    public void testInstallationFails() {
+        FlowManagerService flowManager =
+                new FakeFlowManagerService(flowId, true, SUBMITTED);
+        SingleDstTreeFlowIntentInstaller sut =
+                new SingleDstTreeFlowIntentInstaller(flowManager);
+
+        Tree tree = createTree();
+        SingleDstTreeFlowIntent intent = new SingleDstTreeFlowIntent(new IntentId(1), createFlow(tree));
+
+        sut.install(intent);
+    }
+
+    /**
+     * Tests intent removal that removeFlow() returns null.
+     */
+    @Test(expected = IntentRemovalException.class)
+    public void testRemovalFails() {
+        FlowManagerService flowManager =
+                new FakeFlowManagerService(flowId, true, WITHDRAWING);
+        SingleDstTreeFlowIntentInstaller sut =
+                new SingleDstTreeFlowIntentInstaller(flowManager);
+
+        Tree tree = createTree();
+        SingleDstTreeFlowIntent intent = new SingleDstTreeFlowIntent(new IntentId(1), createFlow(tree));
+
+        sut.remove(intent);
+    }
+
+    private SingleDstTreeFlow createFlow(Tree tree) {
+        return new SingleDstTreeFlow(
+                flowId,
+                    new PacketMatchBuilder().build(),
+                    Arrays.asList(ingress1, ingress2),
+                    tree,
+                    Collections.<Action>emptyList()
+            );
+    }
+
+    private Tree createTree() {
+        Tree tree = new Tree();
+        tree.add(new FlowLink(port12, port21));
+        tree.add(new FlowLink(port13, port13));
+        return tree;
+    }
+}
diff --git a/src/test/java/net/onrc/onos/core/newintent/SingleSrcTreeFlowIntentInstallerTest.java b/src/test/java/net/onrc/onos/core/newintent/SingleSrcTreeFlowIntentInstallerTest.java
new file mode 100644
index 0000000..755fa87
--- /dev/null
+++ b/src/test/java/net/onrc/onos/core/newintent/SingleSrcTreeFlowIntentInstallerTest.java
@@ -0,0 +1,104 @@
+package net.onrc.onos.core.newintent;
+
+import net.onrc.onos.api.flowmanager.FakeFlowManagerService;
+import net.onrc.onos.api.flowmanager.FlowId;
+import net.onrc.onos.api.flowmanager.FlowLink;
+import net.onrc.onos.api.flowmanager.FlowManagerService;
+import net.onrc.onos.api.flowmanager.SingleSrcTreeFlow;
+import net.onrc.onos.api.flowmanager.Tree;
+import net.onrc.onos.api.newintent.IntentId;
+import net.onrc.onos.core.matchaction.action.OutputAction;
+import net.onrc.onos.core.matchaction.match.PacketMatchBuilder;
+import net.onrc.onos.core.util.Dpid;
+import net.onrc.onos.core.util.Pair;
+import net.onrc.onos.core.util.SwitchPort;
+import org.junit.Test;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static net.onrc.onos.api.flowmanager.FlowState.COMPILED;
+import static net.onrc.onos.api.flowmanager.FlowState.INSTALLED;
+import static net.onrc.onos.api.flowmanager.FlowState.SUBMITTED;
+import static net.onrc.onos.api.flowmanager.FlowState.WITHDRAWING;
+
+/**
+ * Suites of test of {@link SingleSrcTreeFlowIntentInstaller}.
+ */
+public class SingleSrcTreeFlowIntentInstallerTest {
+    private final FlowId flowId = new FlowId(1);
+    private final SwitchPort port12 = new SwitchPort(1, (short) 1);
+    private final SwitchPort port13 = new SwitchPort(1, (short) 3);
+    private final SwitchPort port21 = new SwitchPort(2, (short) 2);
+    private final SwitchPort port31 = new SwitchPort(3, (short) 3);
+    private final SwitchPort ingress1 = new SwitchPort(1, (short) 3);
+    private final SwitchPort egress1 = new SwitchPort(2, (short) 100);
+    private final SwitchPort egress2 = new SwitchPort(3, (short) 101);
+
+    /**
+     * Tests intent installation that the state is changed
+     * to SUBMITTED, COMPILED, then INSTALLED.
+     */
+    @Test
+    public void testNormalStateTransition() {
+        FlowManagerService flowManager =
+                new FakeFlowManagerService(flowId, false, SUBMITTED, COMPILED, INSTALLED);
+        SingleSrcTreeFlowIntentInstaller sut =
+                new SingleSrcTreeFlowIntentInstaller(flowManager);
+
+        SingleSrcTreeFlow flow = createFlow();
+        SingleSrcTreeFlowIntent intent = new SingleSrcTreeFlowIntent(new IntentId(1), flow);
+
+        sut.install(intent);
+    }
+
+    /**
+     * Tests intent installation that addFlow() returns null.
+     */
+    @Test(expected = IntentInstallationException.class)
+    public void testInstallationFails() {
+        FlowManagerService flowManager =
+                new FakeFlowManagerService(flowId, true, SUBMITTED);
+        SingleSrcTreeFlowIntentInstaller sut =
+                new SingleSrcTreeFlowIntentInstaller(flowManager);
+
+        SingleSrcTreeFlow flow = createFlow();
+        SingleSrcTreeFlowIntent intent = new SingleSrcTreeFlowIntent(new IntentId(1), flow);
+
+        sut.install(intent);
+    }
+
+    /**
+     * Tests intent removal that removeFlow() returns null.
+     */
+    @Test(expected = IntentRemovalException.class)
+    public void testRemovalFails() {
+        FlowManagerService flowManager =
+                new FakeFlowManagerService(flowId, true, WITHDRAWING);
+        SingleSrcTreeFlowIntentInstaller sut =
+                new SingleSrcTreeFlowIntentInstaller(flowManager);
+
+        SingleSrcTreeFlow flow = createFlow();
+        SingleSrcTreeFlowIntent intent = new SingleSrcTreeFlowIntent(new IntentId(1), flow);
+
+        sut.remove(intent);
+    }
+
+    private SingleSrcTreeFlow createFlow() {
+        Tree tree = new Tree();
+        tree.add(new FlowLink(port12, port21));
+        tree.add(new FlowLink(port13, port31));
+
+        Set<Pair<Dpid, OutputAction>> actions = new HashSet<>();
+        actions.add(new Pair<>(egress1.getDpid(), new OutputAction(egress1.getPortNumber())));
+        actions.add(new Pair<>(egress2.getDpid(), new OutputAction(egress2.getPortNumber())));
+
+        return new SingleSrcTreeFlow(
+                flowId,
+                new PacketMatchBuilder().build(),
+                ingress1,
+                tree,
+                actions
+        );
+    }
+}