| /* |
| * Copyright 2016 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 com.google.common.io.Files; |
| |
| import io.atomix.catalyst.transport.Address; |
| import io.atomix.catalyst.transport.local.LocalTransport; |
| import io.atomix.copycat.server.CopycatServer; |
| import io.atomix.copycat.server.storage.Storage; |
| import io.atomix.copycat.server.storage.StorageLevel; |
| import io.atomix.manager.internal.ResourceManagerState; |
| import io.atomix.resource.ResourceType; |
| |
| import org.apache.commons.collections.keyvalue.DefaultMapEntry; |
| import org.junit.Ignore; |
| import org.junit.Test; |
| import org.onlab.util.Tools; |
| |
| import java.io.File; |
| import java.time.Duration; |
| 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 AsyncConsistentSetMultimap}. |
| */ |
| public class AsyncConsistentSetMultimapTest extends AtomixTestBase { |
| private final File testDir = Files.createTempDir(); |
| 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); |
| |
| @Override |
| protected ResourceType resourceType() { |
| return new ResourceType(AsyncConsistentSetMultimap.class); |
| } |
| |
| /** |
| * Test that size behaves correctly (This includes testing of the empty |
| * check). |
| */ |
| @Ignore |
| @Test |
| public void testSize() throws Throwable { |
| clearTests(); |
| AsyncConsistentSetMultimap map = createResource(3); |
| //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(); |
| clearTests(); |
| } |
| |
| /** |
| * Contains tests for value, key and entry. |
| */ |
| @Ignore |
| @Test |
| public void containsTest() throws Throwable { |
| clearTests(); |
| AsyncConsistentSetMultimap map = createResource(3); |
| |
| //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(); |
| clearTests(); |
| } |
| |
| /** |
| * Contains tests for put, putAll, remove, removeAll and replace. |
| * @throws Exception |
| */ |
| @Ignore |
| @Test |
| public void addAndRemoveTest() throws Exception { |
| clearTests(); |
| AsyncConsistentSetMultimap map = createResource(3); |
| |
| //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(); |
| clearTests(); |
| } |
| |
| /** |
| * Tests the get, keySet, keys, values, and entries implementations as well |
| * as a trivial test of the asMap functionality (throws error). |
| * @throws Exception |
| */ |
| @Ignore |
| @Test |
| public void testAccessors() throws Exception { |
| clearTests(); |
| AsyncConsistentSetMultimap map = createResource(3); |
| |
| //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(); |
| clearTests(); |
| } |
| |
| |
| private AsyncConsistentSetMultimap createResource(int clusterSize) { |
| try { |
| createCopycatServers(clusterSize); |
| AsyncConsistentSetMultimap map = createAtomixClient(). |
| getResource("testMap", AsyncConsistentSetMultimap.class) |
| .join(); |
| return map; |
| } catch (Throwable e) { |
| throw new RuntimeException(e.toString()); |
| } |
| } |
| |
| @Override |
| protected CopycatServer createCopycatServer(Address address) { |
| CopycatServer server = CopycatServer.builder(address) |
| .withTransport(new LocalTransport(registry)) |
| .withStorage(Storage.builder() |
| .withStorageLevel(StorageLevel.MEMORY) |
| .withDirectory(testDir + "/" + address.port()) |
| .build()) |
| .withStateMachine(ResourceManagerState::new) |
| .withSerializer(serializer.clone()) |
| .withHeartbeatInterval(Duration.ofMillis(25)) |
| .withElectionTimeout(Duration.ofMillis(50)) |
| .withSessionTimeout(Duration.ofMillis(100)) |
| .build(); |
| copycatServers.add(server); |
| return server; |
| } |
| |
| /** |
| * 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()); |
| } |
| } |
| } |
| } |