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.FluentFuture;
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.HashMap;
25 import java.util.List;
27 import java.util.Objects;
28 import org.junit.ComparisonFailure;
29 import org.mockito.Mockito;
30 import org.opendaylight.genius.infra.Datastore.Configuration;
31 import org.opendaylight.genius.infra.TypedReadWriteTransaction;
32 import org.opendaylight.genius.infra.TypedWriteTransaction;
33 import org.opendaylight.genius.mdsalutil.FlowEntity;
34 import org.opendaylight.genius.mdsalutil.GroupEntity;
35 import org.opendaylight.genius.mdsalutil.interfaces.IMdsalApiManager;
36 import org.opendaylight.yang.gen.v1.urn.opendaylight.flow.inventory.rev130819.tables.table.Flow;
37 import org.opendaylight.yang.gen.v1.urn.opendaylight.flow.inventory.rev130819.tables.table.FlowKey;
38 import org.opendaylight.yang.gen.v1.urn.opendaylight.group.types.rev131018.group.buckets.Bucket;
39 import org.opendaylight.yang.gen.v1.urn.opendaylight.group.types.rev131018.groups.Group;
40 import org.opendaylight.yangtools.util.concurrent.FluentFutures;
41 import org.opendaylight.yangtools.yang.common.Uint64;
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());
69 instance.flows = new HashMap<>();
70 instance.groups = new HashMap<>();
71 instance.buckets = new HashMap<>();
76 * Get list of installed flows.
77 * Prefer the {@link #assertFlows(Iterable)} instead of using this and checking yourself.
78 * @return immutable copy of list of flows
80 public List<FlowEntity> getFlows() {
81 return retrieveFlows();
84 public void assertFlows(Iterable<FlowEntity> expectedFlows) {
85 checkNonEmptyFlows(expectedFlows);
86 Collection<FlowEntity> nonNullFlows = retrieveFlows();
87 if (!Iterables.isEmpty(expectedFlows)) {
88 assertTrue("No Flows created (bean wiring may be broken?)", !nonNullFlows.isEmpty());
90 // TODO Support Iterable <-> List directly within XtendBeanGenerator
91 List<FlowEntity> expectedFlowsAsNewArrayList = Lists.newArrayList(expectedFlows);
92 assertEqualBeans(expectedFlowsAsNewArrayList, new ArrayList<>(nonNullFlows));
96 private void checkNonEmptyFlows(Iterable<FlowEntity> expectedFlows) {
97 if (!Iterables.isEmpty(expectedFlows)) {
98 assertTrue("No Flows created (bean wiring may be broken?)", !retrieveFlows().isEmpty());
102 // ComparisonException doesn’t allow us to keep the cause (which we don’t care about anyway)
103 @SuppressWarnings("checkstyle:AvoidHidingCauseException")
104 public void assertFlowsInAnyOrder(Iterable<FlowEntity> expectedFlows) {
105 checkNonEmptyFlows(expectedFlows);
106 // TODO Support Iterable <-> List directly within XtendBeanGenerator
108 List<FlowEntity> sortedFlows = sortFlows(retrieveFlows());
109 Map<InternalFlowKey, FlowEntity> keyedExpectedFlows = new HashMap<>();
110 for (FlowEntity expectedFlow : expectedFlows) {
111 keyedExpectedFlows.put(
112 new InternalFlowKey(expectedFlow.getDpnId(), expectedFlow.getFlowId(), expectedFlow.getTableId()),
115 List<FlowEntity> sortedExpectedFlows = sortFlows(keyedExpectedFlows.values());
117 // FYI: This containsExactlyElementsIn() assumes that FlowEntity, and everything in it,
118 // has correctly working equals() implementations. assertEqualBeans() does not assume
119 // that, and would work even without equals, because it only uses property reflection.
120 // Normally this will lead to the same result, but if one day it doesn't (because of
121 // a bug in an equals() implementation somewhere), then it's worth to keep this diff
124 // FTR: This use of G Truth and then catch AssertionError and using assertEqualBeans iff NOK
125 // (thus discarding the message from G Truth) is a bit of a hack, but it works well...
126 // If you're tempted to improve this, please remember that correctly re-implementing
127 // containsExactlyElementsIn (or Hamcrest's similar containsInAnyOrder) isn't a 1 line
128 // trivia... e.g. a.containsAll(b) && b.containsAll(a) isn't sufficient, because it
129 // won't work for duplicates (which we frequently have here); and ordering before is
130 // not viable because FlowEntity is not Comparable, and Comparator based on hashCode
131 // is not a good idea (different instances can have same hashCode), and e.g. on
132 // System#identityHashCode even less so.
134 assertThat(sortedFlows).containsExactlyElementsIn(sortedExpectedFlows);
135 } catch (AssertionError e) {
136 // We LOG the AssertionError just for clarity why containsExactlyElementsIn() failed
137 LOG.warn("assert containsExactlyElementsIn() failed", e);
138 // We LOG the expected and actual flow in case of a failed assertion
139 // because, even though that is typically just a HUGE String that's
140 // hard to read (the diff printed subsequently by assertEqualBeans
141 // is, much, more readable), there are cases when looking more closely
142 // at the full toString() output of the flows is still useful, so:
143 // TIP: Use e.g. 'wdiff -n expected.txt actual.txt | colordiff' to compare these!
144 LOG.warn("assert failed [order ignored!]; expected flows ({}): {}", sortedExpectedFlows.size(),
145 sortedExpectedFlows);
146 LOG.warn("assert failed [order ignored!]; actual flows ({}): {}", sortedFlows.size(), sortedFlows);
147 for (int i = 0; i < sortedExpectedFlows.size() && i < sortedFlows.size(); i++) {
148 if (!sortedExpectedFlows.get(i).equals(sortedFlows.get(i))) {
149 LOG.warn("First mismatch at index {};", i);
150 LOG.warn(" expected {}", sortedExpectedFlows.get(i));
151 LOG.warn(" got {}", sortedFlows.get(i));
155 // The point of now also doing assertEqualBeans() is just that its output,
156 // in case of a comparison failure, is *A LOT* more clearly readable
157 // than what G Truth (or Hamcrest) can do based on toString.
158 assertEqualBeans(sortedExpectedFlows, sortedFlows);
159 if (sortedExpectedFlows.toString().equals(sortedFlows.toString())
160 && !sortedExpectedFlows.equals(sortedFlows)) {
161 fail("Suspected toString, missing getter, equals (hashCode) bug in FlowEntity related class!!! :-(");
163 throw new ComparisonFailure(
164 "assertEqualBeans() MUST fail - given that the assertThat.containsExactlyElementsIn() just failed!"
165 // Beware, we're using XtendBeanGenerator instead of XtendYangBeanGenerator like in
166 // AssertDataObjects, but for FlowEntity it's the same... it only makes a difference for DataObjects
167 + " What is missing in: " + new XtendBeanGenerator().getExpression(sortedFlows),
168 sortedExpectedFlows.toString(), sortedFlows.toString());
169 // If this ^^^ occurs, then there is probably a bug in ch.vorburger.xtendbeans
173 private static List<FlowEntity> sortFlows(Iterable<FlowEntity> flowsToSort) {
174 List<FlowEntity> sortedFlows = Lists.newArrayList(flowsToSort);
175 sortedFlows.sort((flow1, flow2) -> ComparisonChain.start()
176 .compare(flow1.getTableId(), flow2.getTableId())
177 .compare(flow1.getPriority(), flow2.getPriority())
178 .compare(flow1.getFlowId(), flow2.getFlowId())
183 private synchronized void storeFlow(FlowEntity flowEntity) {
184 flows.put(new InternalFlowKey(flowEntity.getDpnId(), flowEntity.getFlowId(), flowEntity.getTableId()),
188 private synchronized List<FlowEntity> retrieveFlows() {
189 return ImmutableList.copyOf(flows.values());
192 private synchronized void deleteFlow(Uint64 dpId, String flowId, short tableId) {
193 flows.remove(new InternalFlowKey(dpId, flowId, tableId));
196 private synchronized void storeGroup(Uint64 dpnId, Group group) {
197 groups.put(new InternalGroupKey(dpnId, group.key().getGroupId().getValue().toJava()), group);
200 private synchronized void deleteGroup(Uint64 dpnId, long groupId) {
201 groups.remove(new InternalGroupKey(dpnId, groupId));
204 private synchronized void storeBucket(Uint64 dpnId, long groupId, Bucket bucket) {
205 buckets.put(new InternalBucketKey(dpnId, groupId, bucket.getBucketId().getValue().toJava()), bucket);
208 private synchronized void deleteBucket(Uint64 dpnId, long groupId, long bucketId) {
209 buckets.remove(new InternalBucketKey(dpnId, groupId, bucketId));
213 public void addFlow(TypedWriteTransaction<Configuration> tx, FlowEntity flowEntity) {
214 storeFlow(flowEntity);
218 public void addFlow(TypedWriteTransaction<Configuration> tx, Uint64 dpId, Flow flow) {
219 throw new UnsupportedOperationException("addFlow(..., BigInteger, Flow) isn't supported yet");
223 public void removeFlow(TypedReadWriteTransaction<Configuration> tx, Uint64 dpId, Flow flow) {
224 removeFlow(tx, dpId, flow.key(), flow.getTableId().toJava());
228 public void removeFlow(TypedReadWriteTransaction<Configuration> tx, FlowEntity flowEntity) {
229 deleteFlow(flowEntity.getDpnId(), flowEntity.getFlowId(), flowEntity.getTableId());
233 public void removeFlow(TypedReadWriteTransaction<Configuration> tx, Uint64 dpId, FlowKey flowKey,
235 deleteFlow(dpId, flowKey.getId().getValue(), tableId);
239 public void removeFlow(TypedReadWriteTransaction<Configuration> tx, Uint64 dpId, String flowId,
241 deleteFlow(dpId, flowId, tableId);
245 public void addGroup(TypedWriteTransaction<Configuration> tx, GroupEntity groupEntity) {
246 storeGroup(groupEntity.getDpnId(), groupEntity.getGroupBuilder().build());
250 public void addGroup(TypedWriteTransaction<Configuration> tx, Uint64 dpId, Group group) {
251 storeGroup(dpId, group);
255 public void removeGroup(TypedReadWriteTransaction<Configuration> tx, Uint64 dpId, Group group) {
256 deleteGroup(dpId, group.getGroupId().getValue().toJava());
260 public void removeGroup(TypedReadWriteTransaction<Configuration> tx, Uint64 dpId, long groupId) {
261 deleteGroup(dpId, groupId);
265 public void addBucket(TypedReadWriteTransaction<Configuration> tx, Uint64 dpId, long groupId,
267 storeBucket(dpId, groupId, bucket);
271 public void removeBucket(TypedReadWriteTransaction<Configuration> tx, Uint64 dpId, long groupId,
273 deleteBucket(dpId, groupId, bucketId);
277 public FluentFuture<Void> installFlow(FlowEntity flowEntity) {
278 storeFlow(flowEntity);
279 return FluentFutures.immediateNullFluentFuture();
283 public FluentFuture<Void> installFlow(Uint64 dpId, FlowEntity flowEntity) {
284 // TODO should dpId be considered here? how? Copy clone FlowEntity and change its dpId?
285 return installFlow(flowEntity);
288 private static final class InternalFlowKey {
289 private final Uint64 dpnId;
290 private final String flowId;
291 private final short tableId;
293 private InternalFlowKey(Uint64 dpnId, String flowId, short tableId) {
295 this.flowId = flowId;
296 this.tableId = tableId;
300 public boolean equals(Object obj) {
304 if (obj == null || getClass() != obj.getClass()) {
307 InternalFlowKey that = (InternalFlowKey) obj;
308 return tableId == that.tableId && Objects.equals(dpnId, that.dpnId) && Objects.equals(flowId, that.flowId);
312 public int hashCode() {
313 return Objects.hash(dpnId, flowId, tableId);
317 private static final class InternalGroupKey {
318 private final Uint64 dpnId;
319 private final long groupId;
321 private InternalGroupKey(Uint64 dpnId, long groupId) {
323 this.groupId = groupId;
327 public boolean equals(Object obj) {
331 if (obj == null || getClass() != obj.getClass()) {
334 InternalGroupKey that = (InternalGroupKey) obj;
335 return groupId == that.groupId && Objects.equals(dpnId, that.dpnId);
339 public int hashCode() {
340 return Objects.hash(dpnId, groupId);
344 private static final class InternalBucketKey {
345 private final Uint64 dpnId;
346 private final long groupId;
347 private final long bucketId;
349 private InternalBucketKey(Uint64 dpnId, long groupId, long bucketId) {
351 this.groupId = groupId;
352 this.bucketId = bucketId;
356 public boolean equals(Object obj) {
360 if (obj == null || getClass() != obj.getClass()) {
363 InternalBucketKey that = (InternalBucketKey) obj;
364 return groupId == that.groupId && bucketId == that.bucketId && Objects.equals(dpnId, that.dpnId);
368 public int hashCode() {
369 return Objects.hash(dpnId, groupId, bucketId);