2 * Copyright (c) 2016, 2017 Red Hat, Inc. and others. All rights reserved.
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
8 package org.opendaylight.genius.mdsalutil.interfaces.testutils;
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;
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.Collection;
25 import java.util.HashMap;
26 import java.util.List;
28 import java.util.Objects;
29 import org.junit.ComparisonFailure;
30 import org.mockito.Mockito;
31 import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
32 import org.opendaylight.genius.infra.Datastore.Configuration;
33 import org.opendaylight.genius.infra.TypedReadWriteTransaction;
34 import org.opendaylight.genius.infra.TypedWriteTransaction;
35 import org.opendaylight.genius.mdsalutil.FlowEntity;
36 import org.opendaylight.genius.mdsalutil.GroupEntity;
37 import org.opendaylight.genius.mdsalutil.interfaces.IMdsalApiManager;
38 import org.opendaylight.yang.gen.v1.urn.opendaylight.flow.inventory.rev130819.tables.table.Flow;
39 import org.opendaylight.yang.gen.v1.urn.opendaylight.flow.inventory.rev130819.tables.table.FlowKey;
40 import org.opendaylight.yang.gen.v1.urn.opendaylight.group.types.rev131018.group.buckets.Bucket;
41 import org.opendaylight.yang.gen.v1.urn.opendaylight.group.types.rev131018.groups.Group;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
46 * Fake IMdsalApiManager useful for tests.
49 * http://googletesting.blogspot.ch/2013/07/testing-on-toilet-know-your-test-doubles.html
50 * and http://martinfowler.com/articles/mocksArentStubs.html for more background.
52 * <p>This class is abstract just to save reading lines and typing keystrokes to
53 * manually implement a bunch of methods we're not yet interested in. Create instances
54 * of it using it's static {@link #newInstance()} method.
56 * @author Michael Vorburger
59 public abstract class TestIMdsalApiManager implements IMdsalApiManager {
61 private static final Logger LOG = LoggerFactory.getLogger(TestIMdsalApiManager.class);
63 private Map<InternalFlowKey, FlowEntity> flows;
64 private Map<InternalGroupKey, Group> groups;
65 private Map<InternalBucketKey, Bucket> buckets;
67 public static TestIMdsalApiManager newInstance() {
68 TestIMdsalApiManager instance = Mockito.mock(TestIMdsalApiManager.class, realOrException());
74 this.flows = new HashMap<>();
75 this.groups = new HashMap<>();
76 this.buckets = new HashMap<>();
80 * Get list of installed flows.
81 * Prefer the {@link #assertFlows(Iterable)} instead of using this and checking yourself.
82 * @return immutable copy of list of flows
84 public synchronized List<FlowEntity> getFlows() {
85 return ImmutableList.copyOf(retrieveFlows());
88 public synchronized void assertFlows(Iterable<FlowEntity> expectedFlows) {
89 checkNonEmptyFlows(expectedFlows);
90 Collection<FlowEntity> nonNullFlows = retrieveFlows();
91 if (!Iterables.isEmpty(expectedFlows)) {
92 assertTrue("No Flows created (bean wiring may be broken?)", !nonNullFlows.isEmpty());
94 // TODO Support Iterable <-> List directly within XtendBeanGenerator
95 List<FlowEntity> expectedFlowsAsNewArrayList = Lists.newArrayList(expectedFlows);
96 assertEqualBeans(expectedFlowsAsNewArrayList, nonNullFlows);
100 private synchronized void checkNonEmptyFlows(Iterable<FlowEntity> expectedFlows) {
101 if (!Iterables.isEmpty(expectedFlows)) {
102 assertTrue("No Flows created (bean wiring may be broken?)", !retrieveFlows().isEmpty());
106 // ComparisonException doesn’t allow us to keep the cause (which we don’t care about anyway)
107 @SuppressWarnings("checkstyle:AvoidHidingCauseException")
108 public synchronized void assertFlowsInAnyOrder(Iterable<FlowEntity> expectedFlows) {
109 checkNonEmptyFlows(expectedFlows);
110 // TODO Support Iterable <-> List directly within XtendBeanGenerator
112 List<FlowEntity> sortedFlows = sortFlows(retrieveFlows());
113 Map<InternalFlowKey, FlowEntity> keyedExpectedFlows = new HashMap<>();
114 for (FlowEntity expectedFlow : expectedFlows) {
115 keyedExpectedFlows.put(
116 new InternalFlowKey(expectedFlow.getDpnId(), expectedFlow.getFlowId(), expectedFlow.getTableId()),
119 List<FlowEntity> sortedExpectedFlows = sortFlows(keyedExpectedFlows.values());
121 // FYI: This containsExactlyElementsIn() assumes that FlowEntity, and everything in it,
122 // has correctly working equals() implementations. assertEqualBeans() does not assume
123 // that, and would work even without equals, because it only uses property reflection.
124 // Normally this will lead to the same result, but if one day it doesn't (because of
125 // a bug in an equals() implementation somewhere), then it's worth to keep this diff
128 // FTR: This use of G Truth and then catch AssertionError and using assertEqualBeans iff NOK
129 // (thus discarding the message from G Truth) is a bit of a hack, but it works well...
130 // If you're tempted to improve this, please remember that correctly re-implementing
131 // containsExactlyElementsIn (or Hamcrest's similar containsInAnyOrder) isn't a 1 line
132 // trivia... e.g. a.containsAll(b) && b.containsAll(a) isn't sufficient, because it
133 // won't work for duplicates (which we frequently have here); and ordering before is
134 // not viable because FlowEntity is not Comparable, and Comparator based on hashCode
135 // is not a good idea (different instances can have same hashCode), and e.g. on
136 // System#identityHashCode even less so.
138 assertThat(sortedFlows).containsExactlyElementsIn(sortedExpectedFlows);
139 } catch (AssertionError e) {
140 // We LOG the AssertionError just for clarity why containsExactlyElementsIn() failed
141 LOG.warn("assert containsExactlyElementsIn() failed", e);
142 // We LOG the expected and actual flow in case of a failed assertion
143 // because, even though that is typically just a HUGE String that's
144 // hard to read (the diff printed subsequently by assertEqualBeans
145 // is, much, more readable), there are cases when looking more closely
146 // at the full toString() output of the flows is still useful, so:
147 // TIP: Use e.g. 'wdiff -n expected.txt actual.txt | colordiff' to compare these!
148 LOG.warn("assert failed [order ignored!]; expected flows ({}): {}", sortedExpectedFlows.size(),
149 sortedExpectedFlows);
150 LOG.warn("assert failed [order ignored!]; actual flows ({}): {}", sortedFlows.size(), sortedFlows);
151 for (int i = 0; i < sortedExpectedFlows.size() && i < sortedFlows.size(); i++) {
152 if (!sortedExpectedFlows.get(i).equals(sortedFlows.get(i))) {
153 LOG.warn("First mismatch at index {};", i);
154 LOG.warn(" expected {}", sortedExpectedFlows.get(i));
155 LOG.warn(" got {}", sortedFlows.get(i));
159 // The point of now also doing assertEqualBeans() is just that its output,
160 // in case of a comparison failure, is *A LOT* more clearly readable
161 // than what G Truth (or Hamcrest) can do based on toString.
162 assertEqualBeans(sortedExpectedFlows, sortedFlows);
163 if (sortedExpectedFlows.toString().equals(sortedFlows.toString())
164 && !sortedExpectedFlows.equals(sortedFlows)) {
165 fail("Suspected toString, missing getter, equals (hashCode) bug in FlowEntity related class!!! :-(");
167 throw new ComparisonFailure(
168 "assertEqualBeans() MUST fail - given that the assertThat.containsExactlyElementsIn() just failed!"
169 // Beware, we're using XtendBeanGenerator instead of XtendYangBeanGenerator like in
170 // AssertDataObjects, but for FlowEntity it's the same... it only makes a difference for DataObjects
171 + " What is missing in: " + new XtendBeanGenerator().getExpression(sortedFlows),
172 sortedExpectedFlows.toString(), sortedFlows.toString());
173 // If this ^^^ occurs, then there is probably a bug in ch.vorburger.xtendbeans
177 private List<FlowEntity> sortFlows(Iterable<FlowEntity> flowsToSort) {
178 List<FlowEntity> sortedFlows = Lists.newArrayList(flowsToSort);
179 sortedFlows.sort((flow1, flow2) -> ComparisonChain.start()
180 .compare(flow1.getTableId(), flow2.getTableId())
181 .compare(flow1.getPriority(), flow2.getPriority())
182 .compare(flow1.getFlowId(), flow2.getFlowId())
187 private void storeFlow(FlowEntity flowEntity) {
188 flows.put(new InternalFlowKey(flowEntity.getDpnId(), flowEntity.getFlowId(), flowEntity.getTableId()),
192 private Collection<FlowEntity> retrieveFlows() {
193 return flows.values();
196 private void deleteFlow(BigInteger dpId, String flowId, short tableId) {
197 flows.remove(new InternalFlowKey(dpId, flowId, tableId));
200 private void storeGroup(BigInteger dpnId, Group group) {
201 groups.put(new InternalGroupKey(dpnId, group.key().getGroupId().getValue()), group);
204 private Collection<Group> retrieveGroups() {
205 return groups.values();
208 private void deleteGroup(BigInteger dpnId, long groupId) {
209 groups.remove(new InternalGroupKey(dpnId, groupId));
212 private void storeBucket(BigInteger dpnId, long groupId, Bucket bucket) {
213 buckets.put(new InternalBucketKey(dpnId, groupId, bucket.getBucketId().getValue()), bucket);
216 private Collection<Bucket> retrieveBuckets() {
217 return buckets.values();
220 private void deleteBucket(BigInteger dpnId, long groupId, long bucketId) {
221 buckets.remove(new InternalBucketKey(dpnId, groupId, bucketId));
225 public void addFlow(TypedWriteTransaction<Configuration> tx, FlowEntity flowEntity) {
226 storeFlow(flowEntity);
230 public void addFlow(TypedWriteTransaction<Configuration> tx, BigInteger dpId, Flow flow) {
231 throw new UnsupportedOperationException("addFlow(..., BigInteger, Flow) isn't supported yet");
235 public void removeFlow(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, Flow flow) {
236 removeFlow(tx, dpId, flow.key(), flow.getTableId());
240 public void removeFlow(TypedReadWriteTransaction<Configuration> tx, FlowEntity flowEntity) {
241 deleteFlow(flowEntity.getDpnId(), flowEntity.getFlowId(), flowEntity.getTableId());
245 public void removeFlow(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, FlowKey flowKey,
247 deleteFlow(dpId, flowKey.getId().getValue(), tableId);
251 public void removeFlow(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, String flowId,
253 deleteFlow(dpId, flowId, tableId);
257 public synchronized CheckedFuture<Void, TransactionCommitFailedException> removeFlow(BigInteger dpnId,
258 FlowEntity flowEntity) {
259 deleteFlow(dpnId, flowEntity.getFlowId(), flowEntity.getTableId());
260 return Futures.immediateCheckedFuture(null);
264 public void addGroup(TypedWriteTransaction<Configuration> tx, GroupEntity groupEntity) {
265 storeGroup(groupEntity.getDpnId(), groupEntity.getGroupBuilder().build());
269 public void addGroup(TypedWriteTransaction<Configuration> tx, BigInteger dpId, Group group) {
270 storeGroup(dpId, group);
274 public void removeGroup(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, Group group) {
275 deleteGroup(dpId, group.getGroupId().getValue());
279 public void removeGroup(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, long groupId) {
280 deleteGroup(dpId, groupId);
284 public void addBucket(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, long groupId,
286 storeBucket(dpId, groupId, bucket);
290 public void removeBucket(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, long groupId,
292 deleteBucket(dpId, groupId, bucketId);
296 public synchronized CheckedFuture<Void, TransactionCommitFailedException> installFlow(FlowEntity flowEntity) {
297 storeFlow(flowEntity);
298 return Futures.immediateCheckedFuture(null);
302 public synchronized CheckedFuture<Void, TransactionCommitFailedException> installFlow(BigInteger dpId,
303 FlowEntity flowEntity) {
304 // TODO should dpId be considered here? how? Copy clone FlowEntity and change its dpId?
305 return installFlow(flowEntity);
309 public synchronized void batchedAddFlow(BigInteger dpId, FlowEntity flowEntity) {
310 storeFlow(flowEntity);
314 public synchronized void batchedRemoveFlow(BigInteger dpId, FlowEntity flowEntity) {
315 deleteFlow(dpId, flowEntity.getFlowId(), flowEntity.getTableId());
319 public void syncInstallGroup(BigInteger dpId, Group group, long delayTime) {
320 storeGroup(dpId, group);
324 public void syncInstallGroup(BigInteger dpId, Group group) {
325 storeGroup(dpId, group);
329 public void syncRemoveGroup(BigInteger dpId, Group groupEntity) {
330 deleteGroup(dpId, groupEntity.getGroupId().getValue());
333 private final class InternalFlowKey {
334 private final BigInteger dpnId;
335 private final String flowId;
336 private final short tableId;
338 private InternalFlowKey(BigInteger dpnId, String flowId, short tableId) {
340 this.flowId = flowId;
341 this.tableId = tableId;
345 public boolean equals(Object obj) {
349 if (obj == null || getClass() != obj.getClass()) {
352 InternalFlowKey that = (InternalFlowKey) obj;
353 return tableId == that.tableId && Objects.equals(dpnId, that.dpnId) && Objects.equals(flowId, that.flowId);
357 public int hashCode() {
358 return Objects.hash(dpnId, flowId, tableId);
362 private final class InternalGroupKey {
363 private final BigInteger dpnId;
364 private final long groupId;
366 private InternalGroupKey(BigInteger dpnId, long groupId) {
368 this.groupId = groupId;
372 public boolean equals(Object obj) {
376 if (obj == null || getClass() != obj.getClass()) {
379 InternalGroupKey that = (InternalGroupKey) obj;
380 return groupId == that.groupId && Objects.equals(dpnId, that.dpnId);
384 public int hashCode() {
385 return Objects.hash(dpnId, groupId);
389 private final class InternalBucketKey {
390 private final BigInteger dpnId;
391 private final long groupId;
392 private final long bucketId;
394 private InternalBucketKey(BigInteger dpnId, long groupId, long bucketId) {
396 this.groupId = groupId;
397 this.bucketId = bucketId;
401 public boolean equals(Object obj) {
405 if (obj == null || getClass() != obj.getClass()) {
408 InternalBucketKey that = (InternalBucketKey) obj;
409 return groupId == that.groupId && bucketId == that.bucketId && Objects.equals(dpnId, that.dpnId);
413 public int hashCode() {
414 return Objects.hash(dpnId, groupId, bucketId);