/* * Copyright 2016-present Open Networking Laboratory * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.onosproject.store.primitives.resources.impl; import com.google.common.collect.Lists; import com.google.common.collect.Multiset; import com.google.common.collect.TreeMultiset; import io.atomix.resource.ResourceType; import org.apache.commons.collections.keyvalue.DefaultMapEntry; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.onlab.util.Tools; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** * Tests the {@link AtomixConsistentSetMultimap}. */ public class AtomixConsistentSetMultimapTest extends AtomixTestBase { private final String keyOne = "hello"; private final String keyTwo = "goodbye"; private final String keyThree = "foo"; private final String keyFour = "bar"; private final byte[] valueOne = Tools.getBytesUtf8(keyOne); private final byte[] valueTwo = Tools.getBytesUtf8(keyTwo); private final byte[] valueThree = Tools.getBytesUtf8(keyThree); private final byte[] valueFour = Tools.getBytesUtf8(keyFour); private final List<String> allKeys = Lists.newArrayList(keyOne, keyTwo, keyThree, keyFour); private final List<byte[]> allValues = Lists.newArrayList(valueOne, valueTwo, valueThree, valueFour); @BeforeClass public static void preTestSetup() throws Throwable { createCopycatServers(3); } @AfterClass public static void postTestCleanup() throws Exception { clearTests(); } @Override protected ResourceType resourceType() { return new ResourceType(AtomixConsistentSetMultimap.class); } /** * Test that size behaves correctly (This includes testing of the empty * check). */ @Test public void testSize() throws Throwable { AtomixConsistentSetMultimap map = createResource("testOneMap"); //Simplest operation case map.isEmpty().thenAccept(result -> assertTrue(result)); map.put(keyOne, valueOne). thenAccept(result -> assertTrue(result)).join(); map.isEmpty().thenAccept(result -> assertFalse(result)); map.size().thenAccept(result -> assertEquals(1, (int) result)) .join(); //Make sure sizing is dependent on values not keys map.put(keyOne, valueTwo). thenAccept(result -> assertTrue(result)).join(); map.size().thenAccept(result -> assertEquals(2, (int) result)) .join(); //Ensure that double adding has no effect map.put(keyOne, valueOne). thenAccept(result -> assertFalse(result)).join(); map.size().thenAccept(result -> assertEquals(2, (int) result)) .join(); //Check handling for multiple keys map.put(keyTwo, valueOne) .thenAccept(result -> assertTrue(result)).join(); map.put(keyTwo, valueTwo) .thenAccept(result -> assertTrue(result)).join(); map.size().thenAccept(result -> assertEquals(4, (int) result)) .join(); //Check size with removal map.remove(keyOne, valueOne). thenAccept(result -> assertTrue(result)).join(); map.size().thenAccept(result -> assertEquals(3, (int) result)) .join(); //Check behavior under remove of non-existant key map.remove(keyOne, valueOne). thenAccept(result -> assertFalse(result)).join(); map.size().thenAccept(result -> assertEquals(3, (int) result)) .join(); //Check clearing the entirety of the map map.clear().join(); map.size().thenAccept(result -> assertEquals(0, (int) result)) .join(); map.isEmpty().thenAccept(result -> assertTrue(result)); map.destroy().join(); } /** * Contains tests for value, key and entry. */ @Test public void containsTest() throws Throwable { AtomixConsistentSetMultimap map = createResource("testTwoMap"); //Populate the maps allKeys.forEach(key -> { map.putAll(key, allValues) .thenAccept(result -> assertTrue(result)).join(); }); map.size().thenAccept(result -> assertEquals(16, (int) result)).join(); //Test key contains positive results allKeys.forEach(key -> { map.containsKey(key) .thenAccept(result -> assertTrue(result)).join(); }); //Test value contains positive results allValues.forEach(value -> { map.containsValue(value) .thenAccept(result -> assertTrue(result)).join(); }); //Test contains entry for all possible entries allKeys.forEach(key -> { allValues.forEach(value -> { map.containsEntry(key, value) .thenAccept(result -> assertTrue(result)).join(); }); }); //Test behavior after removals allValues.forEach(value -> { final String[] removedKey = new String[1]; allKeys.forEach(key -> { map.remove(key, value) .thenAccept(result -> assertTrue(result)).join(); map.containsEntry(key, value) .thenAccept(result -> assertFalse(result)).join(); removedKey[0] = key; }); //Check that contains key works properly for removed keys map.containsKey(removedKey[0]) .thenAccept(result -> assertFalse(result)); }); //Check that contains value works correctly for removed values allValues.forEach(value -> { map.containsValue(value) .thenAccept(result -> assertFalse(result)).join(); }); map.destroy().join(); } /** * Contains tests for put, putAll, remove, removeAll and replace. * @throws Exception */ @Test public void addAndRemoveTest() throws Exception { AtomixConsistentSetMultimap map = createResource("testThreeMap"); //Test single put allKeys.forEach(key -> { //Value should actually be added here allValues.forEach(value -> { map.put(key, value) .thenAccept(result -> assertTrue(result)).join(); //Duplicate values should be ignored here map.put(key, value) .thenAccept(result -> assertFalse(result)).join(); }); }); //Test single remove allKeys.forEach(key -> { //Value should actually be added here allValues.forEach(value -> { map.remove(key, value) .thenAccept(result -> assertTrue(result)).join(); //Duplicate values should be ignored here map.remove(key, value) .thenAccept(result -> assertFalse(result)).join(); }); }); map.isEmpty().thenAccept(result -> assertTrue(result)).join(); //Test multi put allKeys.forEach(key -> { map.putAll(key, Lists.newArrayList(allValues.subList(0, 2))) .thenAccept(result -> assertTrue(result)).join(); map.putAll(key, Lists.newArrayList(allValues.subList(0, 2))) .thenAccept(result -> assertFalse(result)).join(); map.putAll(key, Lists.newArrayList(allValues.subList(2, 4))) .thenAccept(result -> assertTrue(result)).join(); map.putAll(key, Lists.newArrayList(allValues.subList(2, 4))) .thenAccept(result -> assertFalse(result)).join(); }); //Test multi remove allKeys.forEach(key -> { //Split the lists to test how multiRemove can work piecewise map.removeAll(key, Lists.newArrayList(allValues.subList(0, 2))) .thenAccept(result -> assertTrue(result)).join(); map.removeAll(key, Lists.newArrayList(allValues.subList(0, 2))) .thenAccept(result -> assertFalse(result)).join(); map.removeAll(key, Lists.newArrayList(allValues.subList(2, 4))) .thenAccept(result -> assertTrue(result)).join(); map.removeAll(key, Lists.newArrayList(allValues.subList(2, 4))) .thenAccept(result -> assertFalse(result)).join(); }); map.isEmpty().thenAccept(result -> assertTrue(result)).join(); //Repopulate for next test allKeys.forEach(key -> { map.putAll(key, allValues) .thenAccept(result -> assertTrue(result)).join(); }); map.size().thenAccept(result -> assertEquals(16, (int) result)).join(); //Test removeAll of entire entry allKeys.forEach(key -> { map.removeAll(key).thenAccept(result -> { assertTrue( byteArrayCollectionIsEqual(allValues, result.value())); }).join(); map.removeAll(key).thenAccept(result -> { assertFalse( byteArrayCollectionIsEqual(allValues, result.value())); }).join(); }); map.isEmpty().thenAccept(result -> assertTrue(result)).join(); //Repopulate for next test allKeys.forEach(key -> { map.putAll(key, allValues) .thenAccept(result -> assertTrue(result)).join(); }); map.size().thenAccept(result -> assertEquals(16, (int) result)).join(); allKeys.forEach(key -> { map.replaceValues(key, allValues) .thenAccept(result -> assertTrue(byteArrayCollectionIsEqual(allValues, result.value()))) .join(); map.replaceValues(key, Lists.newArrayList()) .thenAccept(result -> assertTrue(byteArrayCollectionIsEqual(allValues, result.value()))) .join(); map.replaceValues(key, allValues) .thenAccept(result -> assertTrue(result.value().isEmpty())) .join(); }); //Test replacements of partial sets map.size().thenAccept(result -> assertEquals(16, (int) result)).join(); allKeys.forEach(key -> { map.remove(key, valueOne) .thenAccept(result -> assertTrue(result)).join(); map.replaceValues(key, Lists.newArrayList()) .thenAccept(result -> assertTrue(byteArrayCollectionIsEqual( Lists.newArrayList(valueTwo, valueThree, valueFour), result.value()))) .join(); map.replaceValues(key, allValues) .thenAccept(result -> assertTrue(result.value().isEmpty())) .join(); }); map.destroy().join(); } /** * Tests the get, keySet, keys, values, and entries implementations as well * as a trivial test of the asMap functionality (throws error). * @throws Exception */ @Test public void testAccessors() throws Exception { AtomixConsistentSetMultimap map = createResource("testFourMap"); //Populate for full map behavior tests allKeys.forEach(key -> { map.putAll(key, allValues) .thenAccept(result -> assertTrue(result)).join(); }); map.size().thenAccept(result -> assertEquals(16, (int) result)).join(); allKeys.forEach(key -> { map.get(key).thenAccept(result -> { assertTrue(byteArrayCollectionIsEqual(allValues, result.value())); }).join(); }); //Test that the key set is correct map.keySet() .thenAccept(result -> assertTrue(stringArrayCollectionIsEqual(allKeys, result))) .join(); //Test that the correct set and occurrence of values are found in the //values result map.values().thenAccept(result -> { final Multiset<byte[]> set = TreeMultiset.create( new ByteArrayComparator()); for (int i = 0; i < 4; i++) { set.addAll(allValues); } assertEquals(16, result.size()); result.forEach(value -> assertTrue(set.remove(value))); assertTrue(set.isEmpty()); }).join(); //Test that keys returns the right result including the correct number //of each item map.keys().thenAccept(result -> { final Multiset<String> set = TreeMultiset.create(); for (int i = 0; i < 4; i++) { set.addAll(allKeys); } assertEquals(16, result.size()); result.forEach(value -> assertTrue(set.remove(value))); assertTrue(set.isEmpty()); }).join(); //Test that the right combination of key, value pairs are present map.entries().thenAccept(result -> { final Multiset<Map.Entry<String, byte[]>> set = TreeMultiset.create(new EntryComparator()); allKeys.forEach(key -> { allValues.forEach(value -> { set.add(new DefaultMapEntry(key, value)); }); }); assertEquals(16, result.size()); result.forEach(entry -> assertTrue(set.remove(entry))); assertTrue(set.isEmpty()); }).join(); //Testing for empty map behavior map.clear().join(); allKeys.forEach(key -> { map.get(key).thenAccept(result -> { assertTrue(result.value().isEmpty()); }).join(); }); map.keySet().thenAccept(result -> assertTrue(result.isEmpty())).join(); map.values().thenAccept(result -> assertTrue(result.isEmpty())).join(); map.keys().thenAccept(result -> assertTrue(result.isEmpty())).join(); map.entries() .thenAccept(result -> assertTrue(result.isEmpty())).join(); map.destroy().join(); } private AtomixConsistentSetMultimap createResource(String mapName) { try { AtomixConsistentSetMultimap map = createAtomixClient(). getResource(mapName, AtomixConsistentSetMultimap.class) .join(); return map; } catch (Throwable e) { throw new RuntimeException(e.toString()); } } /** * Returns two arrays contain the same set of elements, * regardless of order. * @param o1 first collection * @param o2 second collection * @return true if they contain the same elements */ private boolean byteArrayCollectionIsEqual( Collection<? extends byte[]> o1, Collection<? extends byte[]> o2) { if (o1 == null || o2 == null || o1.size() != o2.size()) { return false; } for (byte[] array1 : o1) { boolean matched = false; for (byte[] array2 : o2) { if (Arrays.equals(array1, array2)) { matched = true; break; } } if (!matched) { return false; } } return true; } /** * Compares two collections of strings returns true if they contain the * same strings, false otherwise. * @param s1 string collection one * @param s2 string collection two * @return true if the two sets contain the same strings */ private boolean stringArrayCollectionIsEqual( Collection<? extends String> s1, Collection<? extends String> s2) { if (s1 == null || s2 == null || s1.size() != s2.size()) { return false; } for (String string1 : s1) { boolean matched = false; for (String string2 : s2) { if (string1.equals(string2)) { matched = true; break; } } if (!matched) { return false; } } return true; } /** * Byte array comparator implementation. */ private class ByteArrayComparator implements Comparator<byte[]> { @Override public int compare(byte[] o1, byte[] o2) { if (Arrays.equals(o1, o2)) { return 0; } else { for (int i = 0; i < o1.length && i < o2.length; i++) { if (o1[i] < o2[i]) { return -1; } else if (o1[i] > o2[i]) { return 1; } } return o1.length > o2.length ? 1 : -1; } } } /** * Entry comparator, uses both key and value to determine equality, * for comparison falls back to the default string comparator. */ private class EntryComparator implements Comparator<Map.Entry<String, byte[]>> { @Override public int compare(Map.Entry<String, byte[]> o1, Map.Entry<String, byte[]> o2) { if (o1 == null || o1.getKey() == null || o2 == null || o2.getKey() == null) { throw new IllegalArgumentException(); } if (o1.getKey().equals(o2.getKey()) && Arrays.equals(o1.getValue(), o2.getValue())) { return 0; } else { return o1.getKey().compareTo(o2.getKey()); } } } }