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.ArrayList;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.List;
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;
47 * Fake IMdsalApiManager useful for tests.
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.
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.
57 * @author Michael Vorburger
60 public abstract class TestIMdsalApiManager implements IMdsalApiManager {
62 private static final Logger LOG = LoggerFactory.getLogger(TestIMdsalApiManager.class);
64 private Map<InternalFlowKey, FlowEntity> flows;
65 private Map<InternalGroupKey, Group> groups;
66 private Map<InternalBucketKey, Bucket> buckets;
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<>();
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
81 public List<FlowEntity> getFlows() {
82 return retrieveFlows();
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());
91 // TODO Support Iterable <-> List directly within XtendBeanGenerator
92 List<FlowEntity> expectedFlowsAsNewArrayList = Lists.newArrayList(expectedFlows);
93 assertEqualBeans(expectedFlowsAsNewArrayList, new ArrayList<>(nonNullFlows));
97 private void checkNonEmptyFlows(Iterable<FlowEntity> expectedFlows) {
98 if (!Iterables.isEmpty(expectedFlows)) {
99 assertTrue("No Flows created (bean wiring may be broken?)", !retrieveFlows().isEmpty());
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
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()),
116 List<FlowEntity> sortedExpectedFlows = sortFlows(keyedExpectedFlows.values());
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
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.
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));
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!!! :-(");
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
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())
184 private synchronized void storeFlow(FlowEntity flowEntity) {
185 flows.put(new InternalFlowKey(flowEntity.getDpnId(), flowEntity.getFlowId(), flowEntity.getTableId()),
189 private synchronized List<FlowEntity> retrieveFlows() {
190 return ImmutableList.copyOf(flows.values());
193 private synchronized void deleteFlow(BigInteger dpId, String flowId, short tableId) {
194 flows.remove(new InternalFlowKey(dpId, flowId, tableId));
197 private synchronized void storeGroup(BigInteger dpnId, Group group) {
198 groups.put(new InternalGroupKey(dpnId, group.key().getGroupId().getValue()), group);
201 private synchronized void deleteGroup(BigInteger dpnId, long groupId) {
202 groups.remove(new InternalGroupKey(dpnId, groupId));
205 private synchronized void storeBucket(BigInteger dpnId, long groupId, Bucket bucket) {
206 buckets.put(new InternalBucketKey(dpnId, groupId, bucket.getBucketId().getValue()), bucket);
209 private synchronized void deleteBucket(BigInteger dpnId, long groupId, long bucketId) {
210 buckets.remove(new InternalBucketKey(dpnId, groupId, bucketId));
214 public void addFlow(TypedWriteTransaction<Configuration> tx, FlowEntity flowEntity) {
215 storeFlow(flowEntity);
219 public void addFlow(TypedWriteTransaction<Configuration> tx, BigInteger dpId, Flow flow) {
220 throw new UnsupportedOperationException("addFlow(..., BigInteger, Flow) isn't supported yet");
224 public void removeFlow(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, Flow flow) {
225 removeFlow(tx, dpId, flow.key(), flow.getTableId());
229 public void removeFlow(TypedReadWriteTransaction<Configuration> tx, FlowEntity flowEntity) {
230 deleteFlow(flowEntity.getDpnId(), flowEntity.getFlowId(), flowEntity.getTableId());
234 public void removeFlow(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, FlowKey flowKey,
236 deleteFlow(dpId, flowKey.getId().getValue(), tableId);
240 public void removeFlow(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, String flowId,
242 deleteFlow(dpId, flowId, tableId);
246 public void addGroup(TypedWriteTransaction<Configuration> tx, GroupEntity groupEntity) {
247 storeGroup(groupEntity.getDpnId(), groupEntity.getGroupBuilder().build());
251 public void addGroup(TypedWriteTransaction<Configuration> tx, BigInteger dpId, Group group) {
252 storeGroup(dpId, group);
256 public void removeGroup(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, Group group) {
257 deleteGroup(dpId, group.getGroupId().getValue());
261 public void removeGroup(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, long groupId) {
262 deleteGroup(dpId, groupId);
266 public void addBucket(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, long groupId,
268 storeBucket(dpId, groupId, bucket);
272 public void removeBucket(TypedReadWriteTransaction<Configuration> tx, BigInteger dpId, long groupId,
274 deleteBucket(dpId, groupId, bucketId);
278 public CheckedFuture<Void, TransactionCommitFailedException> installFlow(FlowEntity flowEntity) {
279 storeFlow(flowEntity);
280 return Futures.immediateCheckedFuture(null);
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);
290 private static final class InternalFlowKey {
291 private final BigInteger dpnId;
292 private final String flowId;
293 private final short tableId;
295 private InternalFlowKey(BigInteger dpnId, String flowId, short tableId) {
297 this.flowId = flowId;
298 this.tableId = tableId;
302 public boolean equals(Object obj) {
306 if (obj == null || getClass() != obj.getClass()) {
309 InternalFlowKey that = (InternalFlowKey) obj;
310 return tableId == that.tableId && Objects.equals(dpnId, that.dpnId) && Objects.equals(flowId, that.flowId);
314 public int hashCode() {
315 return Objects.hash(dpnId, flowId, tableId);
319 private static final class InternalGroupKey {
320 private final BigInteger dpnId;
321 private final long groupId;
323 private InternalGroupKey(BigInteger dpnId, long groupId) {
325 this.groupId = groupId;
329 public boolean equals(Object obj) {
333 if (obj == null || getClass() != obj.getClass()) {
336 InternalGroupKey that = (InternalGroupKey) obj;
337 return groupId == that.groupId && Objects.equals(dpnId, that.dpnId);
341 public int hashCode() {
342 return Objects.hash(dpnId, groupId);
346 private static final class InternalBucketKey {
347 private final BigInteger dpnId;
348 private final long groupId;
349 private final long bucketId;
351 private InternalBucketKey(BigInteger dpnId, long groupId, long bucketId) {
353 this.groupId = groupId;
354 this.bucketId = bucketId;
358 public boolean equals(Object obj) {
362 if (obj == null || getClass() != obj.getClass()) {
365 InternalBucketKey that = (InternalBucketKey) obj;
366 return groupId == that.groupId && bucketId == that.bucketId && Objects.equals(dpnId, that.dpnId);
370 public int hashCode() {
371 return Objects.hash(dpnId, groupId, bucketId);