Ensure that maps accesses are protected
[genius.git] / mdsalutil / mdsalutil-api / src / test / java / org / opendaylight / genius / mdsalutil / interfaces / testutils / TestIMdsalApiManager.java
index 81b3bd258ef82e32e8b5af96085fea773ee24676..ef571cccecfce724a00ca4d887af67cf43e35f37 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016 Red Hat, Inc. and others. All rights reserved.
+ * Copyright (c) 2016, 2017 Red Hat, Inc. and others. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
@@ -7,14 +7,41 @@
  */
 package org.opendaylight.genius.mdsalutil.interfaces.testutils;
 
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.opendaylight.mdsal.binding.testutils.AssertDataObjects.assertEqualBeans;
 import static org.opendaylight.yangtools.testutils.mockito.MoreAnswers.realOrException;
 
+import ch.vorburger.xtendbeans.XtendBeanGenerator;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.common.util.concurrent.Futures;
+import java.math.BigInteger;
 import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import org.junit.ComparisonFailure;
 import org.mockito.Mockito;
+import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
+import org.opendaylight.genius.infra.Datastore.Configuration;
+import org.opendaylight.genius.infra.TypedReadWriteTransaction;
+import org.opendaylight.genius.infra.TypedWriteTransaction;
 import org.opendaylight.genius.mdsalutil.FlowEntity;
+import org.opendaylight.genius.mdsalutil.GroupEntity;
 import org.opendaylight.genius.mdsalutil.interfaces.IMdsalApiManager;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.flow.inventory.rev130819.tables.table.Flow;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.flow.inventory.rev130819.tables.table.FlowKey;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.group.types.rev131018.group.buckets.Bucket;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.group.types.rev131018.groups.Group;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Fake IMdsalApiManager useful for tests.
@@ -28,29 +55,320 @@ import org.opendaylight.genius.mdsalutil.interfaces.IMdsalApiManager;
  * of it using it's static {@link #newInstance()} method.
  *
  * @author Michael Vorburger
+ * @author Faseela K
  */
 public abstract class TestIMdsalApiManager implements IMdsalApiManager {
 
-    private List<FlowEntity> flows;
+    private static final Logger LOG = LoggerFactory.getLogger(TestIMdsalApiManager.class);
+
+    private Map<InternalFlowKey, FlowEntity> flows;
+    private Map<InternalGroupKey, Group> groups;
+    private Map<InternalBucketKey, Bucket> buckets;
 
     public static TestIMdsalApiManager newInstance() {
-        return Mockito.mock(TestIMdsalApiManager.class, realOrException());
+        TestIMdsalApiManager instance = Mockito.mock(TestIMdsalApiManager.class, realOrException());
+        instance.flows = new HashMap<>();
+        instance.groups = new HashMap<>();
+        instance.buckets = new HashMap<>();
+        return instance;
     }
 
-    private synchronized List<FlowEntity> initializeFlows() {
-        return Collections.synchronizedList(new ArrayList<>());
+    /**
+     * Get list of installed flows.
+     * Prefer the {@link #assertFlows(Iterable)} instead of using this and checking yourself.
+     * @return immutable copy of list of flows
+     */
+    public List<FlowEntity> getFlows() {
+        return retrieveFlows();
     }
 
-    public List<FlowEntity> getFlows() {
-        if (flows == null) {
-            flows = initializeFlows();
+    public void assertFlows(Iterable<FlowEntity> expectedFlows) {
+        checkNonEmptyFlows(expectedFlows);
+        Collection<FlowEntity> nonNullFlows = retrieveFlows();
+        if (!Iterables.isEmpty(expectedFlows)) {
+            assertTrue("No Flows created (bean wiring may be broken?)", !nonNullFlows.isEmpty());
+        }
+        // TODO Support Iterable <-> List directly within XtendBeanGenerator
+        List<FlowEntity> expectedFlowsAsNewArrayList = Lists.newArrayList(expectedFlows);
+        assertEqualBeans(expectedFlowsAsNewArrayList, new ArrayList<>(nonNullFlows));
+    }
+
+
+    private void checkNonEmptyFlows(Iterable<FlowEntity> expectedFlows) {
+        if (!Iterables.isEmpty(expectedFlows)) {
+            assertTrue("No Flows created (bean wiring may be broken?)", !retrieveFlows().isEmpty());
+        }
+    }
+
+    // ComparisonException doesn’t allow us to keep the cause (which we don’t care about anyway)
+    @SuppressWarnings("checkstyle:AvoidHidingCauseException")
+    public void assertFlowsInAnyOrder(Iterable<FlowEntity> expectedFlows) {
+        checkNonEmptyFlows(expectedFlows);
+        // TODO Support Iterable <-> List directly within XtendBeanGenerator
+
+        List<FlowEntity> sortedFlows = sortFlows(retrieveFlows());
+        Map<InternalFlowKey, FlowEntity> keyedExpectedFlows = new HashMap<>();
+        for (FlowEntity expectedFlow : expectedFlows) {
+            keyedExpectedFlows.put(
+                new InternalFlowKey(expectedFlow.getDpnId(), expectedFlow.getFlowId(), expectedFlow.getTableId()),
+                expectedFlow);
         }
-        return flows;
+        List<FlowEntity> sortedExpectedFlows = sortFlows(keyedExpectedFlows.values());
+
+        // FYI: This containsExactlyElementsIn() assumes that FlowEntity, and everything in it,
+        // has correctly working equals() implementations.  assertEqualBeans() does not assume
+        // that, and would work even without equals, because it only uses property reflection.
+        // Normally this will lead to the same result, but if one day it doesn't (because of
+        // a bug in an equals() implementation somewhere), then it's worth to keep this diff
+        // in mind.
+
+        // FTR: This use of G Truth and then catch AssertionError and using assertEqualBeans iff NOK
+        // (thus discarding the message from G Truth) is a bit of a hack, but it works well...
+        // If you're tempted to improve this, please remember that correctly re-implementing
+        // containsExactlyElementsIn (or Hamcrest's similar containsInAnyOrder) isn't a 1 line
+        // trivia... e.g. a.containsAll(b) && b.containsAll(a) isn't sufficient, because it
+        // won't work for duplicates (which we frequently have here); and ordering before is
+        // not viable because FlowEntity is not Comparable, and Comparator based on hashCode
+        // is not a good idea (different instances can have same hashCode), and e.g. on
+        // System#identityHashCode even less so.
+        try {
+            assertThat(sortedFlows).containsExactlyElementsIn(sortedExpectedFlows);
+        } catch (AssertionError e) {
+            // We LOG the AssertionError just for clarity why containsExactlyElementsIn() failed
+            LOG.warn("assert containsExactlyElementsIn() failed", e);
+            // We LOG the expected and actual flow in case of a failed assertion
+            // because, even though that is typically just a HUGE String that's
+            // hard to read (the diff printed subsequently by assertEqualBeans
+            // is, much, more readable), there are cases when looking more closely
+            // at the full toString() output of the flows is still useful, so:
+            // TIP: Use e.g. 'wdiff -n expected.txt actual.txt | colordiff' to compare these!
+            LOG.warn("assert failed [order ignored!]; expected flows ({}): {}", sortedExpectedFlows.size(),
+                sortedExpectedFlows);
+            LOG.warn("assert failed [order ignored!]; actual flows   ({}): {}", sortedFlows.size(), sortedFlows);
+            for (int i = 0; i < sortedExpectedFlows.size() && i < sortedFlows.size(); i++) {
+                if (!sortedExpectedFlows.get(i).equals(sortedFlows.get(i))) {
+                    LOG.warn("First mismatch at index {};", i);
+                    LOG.warn("               expected {}", sortedExpectedFlows.get(i));
+                    LOG.warn("                    got {}", sortedFlows.get(i));
+                    break;
+                }
+            }
+            // The point of now also doing assertEqualBeans() is just that its output,
+            // in case of a comparison failure, is *A LOT* more clearly readable
+            // than what G Truth (or Hamcrest) can do based on toString.
+            assertEqualBeans(sortedExpectedFlows, sortedFlows);
+            if (sortedExpectedFlows.toString().equals(sortedFlows.toString())
+                    && !sortedExpectedFlows.equals(sortedFlows)) {
+                fail("Suspected toString, missing getter, equals (hashCode) bug in FlowEntity related class!!! :-(");
+            }
+            throw new ComparisonFailure(
+                    "assertEqualBeans() MUST fail - given that the assertThat.containsExactlyElementsIn() just failed!"
+                    // Beware, we're using XtendBeanGenerator instead of XtendYangBeanGenerator like in
+                    // AssertDataObjects, but for FlowEntity it's the same... it only makes a difference for DataObjects
+                    + " What is missing in: " + new XtendBeanGenerator().getExpression(sortedFlows),
+                    sortedExpectedFlows.toString(), sortedFlows.toString());
+            // If this ^^^ occurs, then there is probably a bug in ch.vorburger.xtendbeans
+        }
+    }
+
+    private List<FlowEntity> sortFlows(Iterable<FlowEntity> flowsToSort) {
+        List<FlowEntity> sortedFlows = Lists.newArrayList(flowsToSort);
+        sortedFlows.sort((flow1, flow2) -> ComparisonChain.start()
+                .compare(flow1.getTableId(), flow2.getTableId())
+                .compare(flow1.getPriority(), flow2.getPriority())
+                .compare(flow1.getFlowId(), flow2.getFlowId())
+                .result());
+        return sortedFlows;
+    }
+
+    private synchronized void storeFlow(FlowEntity flowEntity) {
+        flows.put(new InternalFlowKey(flowEntity.getDpnId(), flowEntity.getFlowId(), flowEntity.getTableId()),
+            flowEntity);
+    }
+
+    private synchronized List<FlowEntity> retrieveFlows() {
+        return ImmutableList.copyOf(flows.values());
+    }
+
+    private synchronized void deleteFlow(BigInteger dpId, String flowId, short tableId) {
+        flows.remove(new InternalFlowKey(dpId, flowId, tableId));
+    }
+
+    private synchronized void storeGroup(BigInteger dpnId, Group group) {
+        groups.put(new InternalGroupKey(dpnId, group.key().getGroupId().getValue()), group);
+    }
+
+    private synchronized void deleteGroup(BigInteger dpnId, long groupId) {
+        groups.remove(new InternalGroupKey(dpnId, groupId));
+    }
+
+    private synchronized void storeBucket(BigInteger dpnId, long groupId, Bucket bucket) {
+        buckets.put(new InternalBucketKey(dpnId, groupId, bucket.getBucketId().getValue()), bucket);
+    }
+
+    private synchronized void deleteBucket(BigInteger dpnId, long groupId, long bucketId) {
+        buckets.remove(new InternalBucketKey(dpnId, groupId, bucketId));
+    }
+
+    @Override
+    public void addFlow(TypedWriteTransaction<Configuration> tx, FlowEntity flowEntity) {
+        storeFlow(flowEntity);
+    }
+
+    @Override
+    public void addFlow(TypedWriteTransaction<Configuration> tx, BigInteger dpId, Flow flow) {
+        throw new UnsupportedOperationException("addFlow(..., BigInteger, Flow) isn't supported yet");
+    }
+
+    @Override
+    public void removeFlow(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, Flow flow) {
+        removeFlow(tx, dpId, flow.key(), flow.getTableId());
+    }
+
+    @Override
+    public void removeFlow(TypedReadWriteTransaction<Configuration> tx, FlowEntity flowEntity) {
+        deleteFlow(flowEntity.getDpnId(), flowEntity.getFlowId(), flowEntity.getTableId());
+    }
+
+    @Override
+    public void removeFlow(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, FlowKey flowKey,
+            short tableId) {
+        deleteFlow(dpId, flowKey.getId().getValue(), tableId);
+    }
+
+    @Override
+    public void removeFlow(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, String flowId,
+            short tableId) {
+        deleteFlow(dpId, flowId, tableId);
+    }
+
+    @Override
+    public void addGroup(TypedWriteTransaction<Configuration> tx, GroupEntity groupEntity) {
+        storeGroup(groupEntity.getDpnId(), groupEntity.getGroupBuilder().build());
+    }
+
+    @Override
+    public void addGroup(TypedWriteTransaction<Configuration> tx, BigInteger dpId, Group group) {
+        storeGroup(dpId, group);
+    }
+
+    @Override
+    public void removeGroup(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, Group group) {
+        deleteGroup(dpId, group.getGroupId().getValue());
+    }
+
+    @Override
+    public void removeGroup(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, long groupId) {
+        deleteGroup(dpId, groupId);
+    }
+
+    @Override
+    public void addBucket(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, long groupId,
+            Bucket bucket) {
+        storeBucket(dpId, groupId, bucket);
+    }
+
+    @Override
+    public void removeBucket(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, long groupId,
+            long bucketId) {
+        deleteBucket(dpId, groupId, bucketId);
+    }
+
+    @Override
+    public CheckedFuture<Void, TransactionCommitFailedException> installFlow(FlowEntity flowEntity) {
+        storeFlow(flowEntity);
+        return Futures.immediateCheckedFuture(null);
     }
 
     @Override
-    public void installFlow(FlowEntity flowEntity) {
-        getFlows().add(flowEntity);
+    public CheckedFuture<Void, TransactionCommitFailedException> installFlow(BigInteger dpId,
+            FlowEntity flowEntity) {
+        // TODO should dpId be considered here? how? Copy clone FlowEntity and change its dpId?
+        return installFlow(flowEntity);
     }
 
+    private static final class InternalFlowKey {
+        private final BigInteger dpnId;
+        private final String flowId;
+        private final short tableId;
+
+        private InternalFlowKey(BigInteger dpnId, String flowId, short tableId) {
+            this.dpnId = dpnId;
+            this.flowId = flowId;
+            this.tableId = tableId;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null || getClass() != obj.getClass()) {
+                return false;
+            }
+            InternalFlowKey that = (InternalFlowKey) obj;
+            return tableId == that.tableId && Objects.equals(dpnId, that.dpnId) && Objects.equals(flowId, that.flowId);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(dpnId, flowId, tableId);
+        }
+    }
+
+    private static final class InternalGroupKey {
+        private final BigInteger dpnId;
+        private final long groupId;
+
+        private InternalGroupKey(BigInteger dpnId, long groupId) {
+            this.dpnId = dpnId;
+            this.groupId = groupId;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null || getClass() != obj.getClass()) {
+                return false;
+            }
+            InternalGroupKey that = (InternalGroupKey) obj;
+            return groupId == that.groupId && Objects.equals(dpnId, that.dpnId);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(dpnId, groupId);
+        }
+    }
+
+    private static final class InternalBucketKey {
+        private final BigInteger dpnId;
+        private final long groupId;
+        private final long bucketId;
+
+        private InternalBucketKey(BigInteger dpnId, long groupId, long bucketId) {
+            this.dpnId = dpnId;
+            this.groupId = groupId;
+            this.bucketId = bucketId;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null || getClass() != obj.getClass()) {
+                return false;
+            }
+            InternalBucketKey that = (InternalBucketKey) obj;
+            return groupId == that.groupId && bucketId == that.bucketId && Objects.equals(dpnId, that.dpnId);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(dpnId, groupId, bucketId);
+        }
+    }
 }