Ensure that maps accesses are protected
[genius.git] / mdsalutil / mdsalutil-api / src / test / java / org / opendaylight / genius / mdsalutil / interfaces / testutils / TestIMdsalApiManager.java
1 /*
2  * Copyright (c) 2016, 2017 Red Hat, Inc. and others. All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.genius.mdsalutil.interfaces.testutils;
9
10 import static com.google.common.truth.Truth.assertThat;
11 import static org.junit.Assert.assertTrue;
12 import static org.junit.Assert.fail;
13 import static org.opendaylight.mdsal.binding.testutils.AssertDataObjects.assertEqualBeans;
14 import static org.opendaylight.yangtools.testutils.mockito.MoreAnswers.realOrException;
15
16 import ch.vorburger.xtendbeans.XtendBeanGenerator;
17 import com.google.common.collect.ComparisonChain;
18 import com.google.common.collect.ImmutableList;
19 import com.google.common.collect.Iterables;
20 import com.google.common.collect.Lists;
21 import com.google.common.util.concurrent.CheckedFuture;
22 import com.google.common.util.concurrent.Futures;
23 import java.math.BigInteger;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Objects;
30 import org.junit.ComparisonFailure;
31 import org.mockito.Mockito;
32 import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
33 import org.opendaylight.genius.infra.Datastore.Configuration;
34 import org.opendaylight.genius.infra.TypedReadWriteTransaction;
35 import org.opendaylight.genius.infra.TypedWriteTransaction;
36 import org.opendaylight.genius.mdsalutil.FlowEntity;
37 import org.opendaylight.genius.mdsalutil.GroupEntity;
38 import org.opendaylight.genius.mdsalutil.interfaces.IMdsalApiManager;
39 import org.opendaylight.yang.gen.v1.urn.opendaylight.flow.inventory.rev130819.tables.table.Flow;
40 import org.opendaylight.yang.gen.v1.urn.opendaylight.flow.inventory.rev130819.tables.table.FlowKey;
41 import org.opendaylight.yang.gen.v1.urn.opendaylight.group.types.rev131018.group.buckets.Bucket;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.group.types.rev131018.groups.Group;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 /**
47  * Fake IMdsalApiManager useful for tests.
48  *
49  * <p>Read e.g.
50  * http://googletesting.blogspot.ch/2013/07/testing-on-toilet-know-your-test-doubles.html
51  * and http://martinfowler.com/articles/mocksArentStubs.html for more background.
52  *
53  * <p>This class is abstract just to save reading lines and typing keystrokes to
54  * manually implement a bunch of methods we're not yet interested in.  Create instances
55  * of it using it's static {@link #newInstance()} method.
56  *
57  * @author Michael Vorburger
58  * @author Faseela K
59  */
60 public abstract class TestIMdsalApiManager implements IMdsalApiManager {
61
62     private static final Logger LOG = LoggerFactory.getLogger(TestIMdsalApiManager.class);
63
64     private Map<InternalFlowKey, FlowEntity> flows;
65     private Map<InternalGroupKey, Group> groups;
66     private Map<InternalBucketKey, Bucket> buckets;
67
68     public static TestIMdsalApiManager newInstance() {
69         TestIMdsalApiManager instance = Mockito.mock(TestIMdsalApiManager.class, realOrException());
70         instance.flows = new HashMap<>();
71         instance.groups = new HashMap<>();
72         instance.buckets = new HashMap<>();
73         return instance;
74     }
75
76     /**
77      * Get list of installed flows.
78      * Prefer the {@link #assertFlows(Iterable)} instead of using this and checking yourself.
79      * @return immutable copy of list of flows
80      */
81     public List<FlowEntity> getFlows() {
82         return retrieveFlows();
83     }
84
85     public void assertFlows(Iterable<FlowEntity> expectedFlows) {
86         checkNonEmptyFlows(expectedFlows);
87         Collection<FlowEntity> nonNullFlows = retrieveFlows();
88         if (!Iterables.isEmpty(expectedFlows)) {
89             assertTrue("No Flows created (bean wiring may be broken?)", !nonNullFlows.isEmpty());
90         }
91         // TODO Support Iterable <-> List directly within XtendBeanGenerator
92         List<FlowEntity> expectedFlowsAsNewArrayList = Lists.newArrayList(expectedFlows);
93         assertEqualBeans(expectedFlowsAsNewArrayList, new ArrayList<>(nonNullFlows));
94     }
95
96
97     private void checkNonEmptyFlows(Iterable<FlowEntity> expectedFlows) {
98         if (!Iterables.isEmpty(expectedFlows)) {
99             assertTrue("No Flows created (bean wiring may be broken?)", !retrieveFlows().isEmpty());
100         }
101     }
102
103     // ComparisonException doesn’t allow us to keep the cause (which we don’t care about anyway)
104     @SuppressWarnings("checkstyle:AvoidHidingCauseException")
105     public void assertFlowsInAnyOrder(Iterable<FlowEntity> expectedFlows) {
106         checkNonEmptyFlows(expectedFlows);
107         // TODO Support Iterable <-> List directly within XtendBeanGenerator
108
109         List<FlowEntity> sortedFlows = sortFlows(retrieveFlows());
110         Map<InternalFlowKey, FlowEntity> keyedExpectedFlows = new HashMap<>();
111         for (FlowEntity expectedFlow : expectedFlows) {
112             keyedExpectedFlows.put(
113                 new InternalFlowKey(expectedFlow.getDpnId(), expectedFlow.getFlowId(), expectedFlow.getTableId()),
114                 expectedFlow);
115         }
116         List<FlowEntity> sortedExpectedFlows = sortFlows(keyedExpectedFlows.values());
117
118         // FYI: This containsExactlyElementsIn() assumes that FlowEntity, and everything in it,
119         // has correctly working equals() implementations.  assertEqualBeans() does not assume
120         // that, and would work even without equals, because it only uses property reflection.
121         // Normally this will lead to the same result, but if one day it doesn't (because of
122         // a bug in an equals() implementation somewhere), then it's worth to keep this diff
123         // in mind.
124
125         // FTR: This use of G Truth and then catch AssertionError and using assertEqualBeans iff NOK
126         // (thus discarding the message from G Truth) is a bit of a hack, but it works well...
127         // If you're tempted to improve this, please remember that correctly re-implementing
128         // containsExactlyElementsIn (or Hamcrest's similar containsInAnyOrder) isn't a 1 line
129         // trivia... e.g. a.containsAll(b) && b.containsAll(a) isn't sufficient, because it
130         // won't work for duplicates (which we frequently have here); and ordering before is
131         // not viable because FlowEntity is not Comparable, and Comparator based on hashCode
132         // is not a good idea (different instances can have same hashCode), and e.g. on
133         // System#identityHashCode even less so.
134         try {
135             assertThat(sortedFlows).containsExactlyElementsIn(sortedExpectedFlows);
136         } catch (AssertionError e) {
137             // We LOG the AssertionError just for clarity why containsExactlyElementsIn() failed
138             LOG.warn("assert containsExactlyElementsIn() failed", e);
139             // We LOG the expected and actual flow in case of a failed assertion
140             // because, even though that is typically just a HUGE String that's
141             // hard to read (the diff printed subsequently by assertEqualBeans
142             // is, much, more readable), there are cases when looking more closely
143             // at the full toString() output of the flows is still useful, so:
144             // TIP: Use e.g. 'wdiff -n expected.txt actual.txt | colordiff' to compare these!
145             LOG.warn("assert failed [order ignored!]; expected flows ({}): {}", sortedExpectedFlows.size(),
146                 sortedExpectedFlows);
147             LOG.warn("assert failed [order ignored!]; actual flows   ({}): {}", sortedFlows.size(), sortedFlows);
148             for (int i = 0; i < sortedExpectedFlows.size() && i < sortedFlows.size(); i++) {
149                 if (!sortedExpectedFlows.get(i).equals(sortedFlows.get(i))) {
150                     LOG.warn("First mismatch at index {};", i);
151                     LOG.warn("               expected {}", sortedExpectedFlows.get(i));
152                     LOG.warn("                    got {}", sortedFlows.get(i));
153                     break;
154                 }
155             }
156             // The point of now also doing assertEqualBeans() is just that its output,
157             // in case of a comparison failure, is *A LOT* more clearly readable
158             // than what G Truth (or Hamcrest) can do based on toString.
159             assertEqualBeans(sortedExpectedFlows, sortedFlows);
160             if (sortedExpectedFlows.toString().equals(sortedFlows.toString())
161                     && !sortedExpectedFlows.equals(sortedFlows)) {
162                 fail("Suspected toString, missing getter, equals (hashCode) bug in FlowEntity related class!!! :-(");
163             }
164             throw new ComparisonFailure(
165                     "assertEqualBeans() MUST fail - given that the assertThat.containsExactlyElementsIn() just failed!"
166                     // Beware, we're using XtendBeanGenerator instead of XtendYangBeanGenerator like in
167                     // AssertDataObjects, but for FlowEntity it's the same... it only makes a difference for DataObjects
168                     + " What is missing in: " + new XtendBeanGenerator().getExpression(sortedFlows),
169                     sortedExpectedFlows.toString(), sortedFlows.toString());
170             // If this ^^^ occurs, then there is probably a bug in ch.vorburger.xtendbeans
171         }
172     }
173
174     private List<FlowEntity> sortFlows(Iterable<FlowEntity> flowsToSort) {
175         List<FlowEntity> sortedFlows = Lists.newArrayList(flowsToSort);
176         sortedFlows.sort((flow1, flow2) -> ComparisonChain.start()
177                 .compare(flow1.getTableId(), flow2.getTableId())
178                 .compare(flow1.getPriority(), flow2.getPriority())
179                 .compare(flow1.getFlowId(), flow2.getFlowId())
180                 .result());
181         return sortedFlows;
182     }
183
184     private synchronized void storeFlow(FlowEntity flowEntity) {
185         flows.put(new InternalFlowKey(flowEntity.getDpnId(), flowEntity.getFlowId(), flowEntity.getTableId()),
186             flowEntity);
187     }
188
189     private synchronized List<FlowEntity> retrieveFlows() {
190         return ImmutableList.copyOf(flows.values());
191     }
192
193     private synchronized void deleteFlow(BigInteger dpId, String flowId, short tableId) {
194         flows.remove(new InternalFlowKey(dpId, flowId, tableId));
195     }
196
197     private synchronized void storeGroup(BigInteger dpnId, Group group) {
198         groups.put(new InternalGroupKey(dpnId, group.key().getGroupId().getValue()), group);
199     }
200
201     private synchronized void deleteGroup(BigInteger dpnId, long groupId) {
202         groups.remove(new InternalGroupKey(dpnId, groupId));
203     }
204
205     private synchronized void storeBucket(BigInteger dpnId, long groupId, Bucket bucket) {
206         buckets.put(new InternalBucketKey(dpnId, groupId, bucket.getBucketId().getValue()), bucket);
207     }
208
209     private synchronized void deleteBucket(BigInteger dpnId, long groupId, long bucketId) {
210         buckets.remove(new InternalBucketKey(dpnId, groupId, bucketId));
211     }
212
213     @Override
214     public void addFlow(TypedWriteTransaction<Configuration> tx, FlowEntity flowEntity) {
215         storeFlow(flowEntity);
216     }
217
218     @Override
219     public void addFlow(TypedWriteTransaction<Configuration> tx, BigInteger dpId, Flow flow) {
220         throw new UnsupportedOperationException("addFlow(..., BigInteger, Flow) isn't supported yet");
221     }
222
223     @Override
224     public void removeFlow(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, Flow flow) {
225         removeFlow(tx, dpId, flow.key(), flow.getTableId());
226     }
227
228     @Override
229     public void removeFlow(TypedReadWriteTransaction<Configuration> tx, FlowEntity flowEntity) {
230         deleteFlow(flowEntity.getDpnId(), flowEntity.getFlowId(), flowEntity.getTableId());
231     }
232
233     @Override
234     public void removeFlow(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, FlowKey flowKey,
235             short tableId) {
236         deleteFlow(dpId, flowKey.getId().getValue(), tableId);
237     }
238
239     @Override
240     public void removeFlow(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, String flowId,
241             short tableId) {
242         deleteFlow(dpId, flowId, tableId);
243     }
244
245     @Override
246     public void addGroup(TypedWriteTransaction<Configuration> tx, GroupEntity groupEntity) {
247         storeGroup(groupEntity.getDpnId(), groupEntity.getGroupBuilder().build());
248     }
249
250     @Override
251     public void addGroup(TypedWriteTransaction<Configuration> tx, BigInteger dpId, Group group) {
252         storeGroup(dpId, group);
253     }
254
255     @Override
256     public void removeGroup(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, Group group) {
257         deleteGroup(dpId, group.getGroupId().getValue());
258     }
259
260     @Override
261     public void removeGroup(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, long groupId) {
262         deleteGroup(dpId, groupId);
263     }
264
265     @Override
266     public void addBucket(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, long groupId,
267             Bucket bucket) {
268         storeBucket(dpId, groupId, bucket);
269     }
270
271     @Override
272     public void removeBucket(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, long groupId,
273             long bucketId) {
274         deleteBucket(dpId, groupId, bucketId);
275     }
276
277     @Override
278     public CheckedFuture<Void, TransactionCommitFailedException> installFlow(FlowEntity flowEntity) {
279         storeFlow(flowEntity);
280         return Futures.immediateCheckedFuture(null);
281     }
282
283     @Override
284     public CheckedFuture<Void, TransactionCommitFailedException> installFlow(BigInteger dpId,
285             FlowEntity flowEntity) {
286         // TODO should dpId be considered here? how? Copy clone FlowEntity and change its dpId?
287         return installFlow(flowEntity);
288     }
289
290     private static final class InternalFlowKey {
291         private final BigInteger dpnId;
292         private final String flowId;
293         private final short tableId;
294
295         private InternalFlowKey(BigInteger dpnId, String flowId, short tableId) {
296             this.dpnId = dpnId;
297             this.flowId = flowId;
298             this.tableId = tableId;
299         }
300
301         @Override
302         public boolean equals(Object obj) {
303             if (this == obj) {
304                 return true;
305             }
306             if (obj == null || getClass() != obj.getClass()) {
307                 return false;
308             }
309             InternalFlowKey that = (InternalFlowKey) obj;
310             return tableId == that.tableId && Objects.equals(dpnId, that.dpnId) && Objects.equals(flowId, that.flowId);
311         }
312
313         @Override
314         public int hashCode() {
315             return Objects.hash(dpnId, flowId, tableId);
316         }
317     }
318
319     private static final class InternalGroupKey {
320         private final BigInteger dpnId;
321         private final long groupId;
322
323         private InternalGroupKey(BigInteger dpnId, long groupId) {
324             this.dpnId = dpnId;
325             this.groupId = groupId;
326         }
327
328         @Override
329         public boolean equals(Object obj) {
330             if (this == obj) {
331                 return true;
332             }
333             if (obj == null || getClass() != obj.getClass()) {
334                 return false;
335             }
336             InternalGroupKey that = (InternalGroupKey) obj;
337             return groupId == that.groupId && Objects.equals(dpnId, that.dpnId);
338         }
339
340         @Override
341         public int hashCode() {
342             return Objects.hash(dpnId, groupId);
343         }
344     }
345
346     private static final class InternalBucketKey {
347         private final BigInteger dpnId;
348         private final long groupId;
349         private final long bucketId;
350
351         private InternalBucketKey(BigInteger dpnId, long groupId, long bucketId) {
352             this.dpnId = dpnId;
353             this.groupId = groupId;
354             this.bucketId = bucketId;
355         }
356
357         @Override
358         public boolean equals(Object obj) {
359             if (this == obj) {
360                 return true;
361             }
362             if (obj == null || getClass() != obj.getClass()) {
363                 return false;
364             }
365             InternalBucketKey that = (InternalBucketKey) obj;
366             return groupId == that.groupId && bucketId == that.bucketId && Objects.equals(dpnId, that.dpnId);
367         }
368
369         @Override
370         public int hashCode() {
371             return Objects.hash(dpnId, groupId, bucketId);
372         }
373     }
374 }