/* * 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.base.Throwables; import com.google.common.collect.Lists; import io.atomix.resource.ResourceType; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.onlab.util.Tools; import org.onosproject.store.service.MapEvent; import org.onosproject.store.service.MapEventListener; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.stream.Collectors; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** * Unit tests for {@link AtomixConsistentTreeMap}. */ public class AtomixConsistentTreeMapTest extends AtomixTestBase { private final String keyFour = "hello"; private final String keyThree = "goodbye"; private final String keyTwo = "foo"; private final String keyOne = "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 byte[] spareValue = Tools.getBytesUtf8("spareValue"); 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 Throwable { clearTests(); } @Override protected ResourceType resourceType() { return new ResourceType(AtomixConsistentTreeMap.class); } /** * Tests of the functionality associated with the * {@link org.onosproject.store.service.AsyncConsistentMap} interface * except transactions and listeners. */ @Test public void testBasicMapOperations() throws Throwable { //Throughout the test there are isEmpty queries, these are intended to //make sure that the previous section has been cleaned up, they serve //the secondary purpose of testing isEmpty but that is not their //primary purpose. AtomixConsistentTreeMap map = createResource("basicTestMap"); //test size map.size().thenAccept(result -> assertEquals(0, (int) result)).join(); map.isEmpty().thenAccept(result -> assertTrue(result)).join(); //test contains key allKeys.forEach(key -> map.containsKey(key). thenAccept(result -> assertFalse(result)).join()); //test contains value allValues.forEach(value -> map.containsValue(value) .thenAccept(result -> assertFalse(result)).join()); //test get allKeys.forEach(key -> map.get(key). thenAccept(result -> assertNull(result)).join()); //test getOrDefault allKeys.forEach(key -> map.getOrDefault(key, null).thenAccept(result -> { assertEquals(0, result.version()); assertNull(result.value()); }).join()); allKeys.forEach(key -> map.getOrDefault(key, "bar".getBytes()).thenAccept(result -> { assertEquals(0, result.version()); assertArrayEquals("bar".getBytes(), result.value()); }).join()); //populate and redo prior three tests allKeys.forEach(key -> map.put(key, allValues.get(allKeys.indexOf(key))) .thenAccept(result -> assertNull(result)).join()); //test contains key allKeys.forEach(key -> map.containsKey(key) .thenAccept(result -> assertTrue(result)).join()); //test contains value allValues.forEach(value -> map.containsValue(value) .thenAccept(result -> assertTrue(result)).join()); //test get allKeys.forEach(key -> map.get(key).thenAccept(result -> { assertArrayEquals(allValues.get(allKeys.indexOf(key)), result.value()); }).join()); allKeys.forEach(key -> map.getOrDefault(key, null).thenAccept(result -> { assertNotEquals(0, result.version()); assertArrayEquals(allValues.get(allKeys.indexOf(key)), result.value()); }).join()); //test all compute methods in this section allKeys.forEach(key -> map.computeIfAbsent(key, v -> allValues.get(allKeys.indexOf(key))) .thenAccept(result -> { assertArrayEquals(allValues.get(allKeys.indexOf(key)), result.value()); }).join()); map.size().thenAccept(result -> assertEquals(4, (int) result)).join(); map.isEmpty().thenAccept(result -> assertFalse(result)).join(); allKeys.forEach(key -> map.computeIfPresent(key, (k, v) -> null). thenAccept(result -> assertNull(result)).join()); map.isEmpty().thenAccept(result -> assertTrue(result)).join(); allKeys.forEach(key -> map.compute(key, (k, v) -> allValues.get(allKeys.indexOf(key))) .thenAccept(result -> assertArrayEquals(allValues.get(allKeys.indexOf(key)), result.value())).join()); map.size().thenAccept(result -> assertEquals(4, (int) result)).join(); map.isEmpty().thenAccept(result -> assertFalse(result)).join(); allKeys.forEach(key -> map.computeIf(key, (k) -> allKeys.indexOf(key) < 2, (k, v) -> null).thenAccept(result -> { if (allKeys.indexOf(key) < 2) { assertNull(result); } else { assertArrayEquals(allValues.get(allKeys.indexOf(key)), result.value()); } }).join()); map.size().thenAccept(result -> assertEquals(2, (int) result)).join(); map.isEmpty().thenAccept(result -> assertFalse(result)).join(); //test simple put allKeys.forEach(key -> map.put(key, allValues.get(allKeys.indexOf(key))).thenAccept(result -> { if (allKeys.indexOf(key) < 2) { assertNull(result); } else { assertArrayEquals(allValues.get(allKeys.indexOf(key)), result.value()); } }).join()); map.size().thenAccept(result -> assertEquals(4, (int) result)).join(); map.isEmpty().thenAccept(result -> assertFalse(result)).join(); //test put and get for version retrieval allKeys.forEach(key -> map.putAndGet(key, allValues.get(allKeys.indexOf(key))).thenAccept(firstResult -> { map.putAndGet(key, allValues.get(allKeys.indexOf(key))).thenAccept(secondResult -> { assertArrayEquals(allValues.get(allKeys.indexOf(key)), firstResult.value()); assertArrayEquals(allValues.get(allKeys.indexOf(key)), secondResult.value()); assertTrue((firstResult.version() + 1) == secondResult.version()); }); }).join()); //test removal allKeys.forEach(key -> map.remove(key).thenAccept( result -> assertArrayEquals( allValues.get(allKeys.indexOf(key)), result.value())) .join()); map.isEmpty().thenAccept(result -> assertTrue(result)); //repopulating, this is not mainly for testing allKeys.forEach(key -> map.put(key, allValues.get(allKeys.indexOf(key))).thenAccept(result -> { assertNull(result); }).join()); //Test various collections of keys, values and entries map.keySet().thenAccept(keys -> assertTrue(stringArrayCollectionIsEqual(keys, allKeys))).join(); map.values().thenAccept(values -> assertTrue( byteArrayCollectionIsEqual(values.stream().map(v -> v.value()) .collect(Collectors.toSet()), allValues))).join(); map.entrySet().thenAccept(entrySet -> { entrySet.forEach(entry -> { assertTrue(allKeys.contains(entry.getKey())); assertTrue(Arrays.equals(entry.getValue().value(), allValues.get(allKeys.indexOf(entry.getKey())))); }); }).join(); map.clear().join(); map.isEmpty().thenAccept(result -> assertTrue(result)).join(); //test conditional put allKeys.forEach(key -> map.putIfAbsent(key, allValues.get(allKeys.indexOf(key))) .thenAccept(result -> assertNull(result)).join()); allKeys.forEach(key -> map.putIfAbsent(key, null).thenAccept(result -> assertArrayEquals(result.value(), allValues.get(allKeys.indexOf(key))) ).join()); // test alternate removes that specify value or version allKeys.forEach(key -> map.remove(key, spareValue).thenAccept(result -> assertFalse(result)).join()); allKeys.forEach(key -> map.remove(key, allValues.get(allKeys.indexOf(key))) .thenAccept(result -> assertTrue(result)).join()); map.isEmpty().thenAccept(result -> assertTrue(result)).join(); List<Long> versions = Lists.newArrayList(); //repopulating set for version based removal allKeys.forEach(key -> map.putAndGet(key, allValues.get(allKeys.indexOf(key))) .thenAccept(result -> versions.add(result.version())).join()); allKeys.forEach(key -> map.remove(key, versions.get(0)).thenAccept(result -> { assertTrue(result); versions.remove(0); }).join()); map.isEmpty().thenAccept(result -> assertTrue(result)).join(); //Testing all replace both simple (k, v), and complex that consider // previous mapping or version. allKeys.forEach(key -> map.put(key, allValues.get(allKeys.indexOf(key))) .thenAccept(result -> assertNull(result)).join()); allKeys.forEach(key -> map.replace(key, allValues.get(3 - allKeys.indexOf(key))) .thenAccept(result -> assertArrayEquals(allValues.get(allKeys.indexOf(key)), result.value())) .join()); allKeys.forEach(key -> map.replace(key, spareValue, allValues.get(allKeys.indexOf(key))) .thenAccept(result -> assertFalse(result)).join()); allKeys.forEach(key -> map.replace(key, allValues.get(3 - allKeys.indexOf(key)), allValues.get(allKeys.indexOf(key))).thenAccept(result -> assertTrue(result)).join()); map.clear().join(); map.isEmpty().thenAccept(result -> assertTrue(result)).join(); versions.clear(); //populate for version based replacement allKeys.forEach(key -> map.putAndGet(key, allValues.get(3 - allKeys.indexOf(key))) .thenAccept(result -> versions.add(result.version())).join()); allKeys.forEach(key -> map.replace(key, 0, allValues.get(allKeys.indexOf(key))) .thenAccept(result -> assertFalse(result)).join()); allKeys.forEach(key -> map.replace(key, versions.get(0), allValues.get(allKeys.indexOf(key))) .thenAccept(result -> { assertTrue(result); versions.remove(0); }).join()); } @Test public void mapListenerTests() throws Throwable { final byte[] value1 = Tools.getBytesUtf8("value1"); final byte[] value2 = Tools.getBytesUtf8("value2"); final byte[] value3 = Tools.getBytesUtf8("value3"); AtomixConsistentTreeMap map = createResource("treeMapListenerTestMap"); TestMapEventListener listener = new TestMapEventListener(); // add listener; insert new value into map and verify an INSERT event // is received. map.addListener(listener).thenCompose(v -> map.put("foo", value1)) .join(); MapEvent<String, byte[]> event = listener.event(); assertNotNull(event); assertEquals(MapEvent.Type.INSERT, event.type()); assertTrue(Arrays.equals(value1, event.newValue().value())); // remove listener and verify listener is not notified. map.removeListener(listener).thenCompose(v -> map.put("foo", value2)) .join(); assertFalse(listener.eventReceived()); // add the listener back and verify UPDATE events are received // correctly map.addListener(listener).thenCompose(v -> map.put("foo", value3)) .join(); event = listener.event(); assertNotNull(event); assertEquals(MapEvent.Type.UPDATE, event.type()); assertTrue(Arrays.equals(value3, event.newValue().value())); // perform a non-state changing operation and verify no events are // received. map.putIfAbsent("foo", value1).join(); assertFalse(listener.eventReceived()); // verify REMOVE events are received correctly. map.remove("foo").join(); event = listener.event(); assertNotNull(event); assertEquals(MapEvent.Type.REMOVE, event.type()); assertTrue(Arrays.equals(value3, event.oldValue().value())); // verify compute methods also generate events. map.computeIf("foo", v -> v == null, (k, v) -> value1).join(); event = listener.event(); assertNotNull(event); assertEquals(MapEvent.Type.INSERT, event.type()); assertTrue(Arrays.equals(value1, event.newValue().value())); map.compute("foo", (k, v) -> value2).join(); event = listener.event(); assertNotNull(event); assertEquals(MapEvent.Type.UPDATE, event.type()); assertTrue(Arrays.equals(value2, event.newValue().value())); map.computeIf( "foo", v -> Arrays.equals(v, value2), (k, v) -> null).join(); event = listener.event(); assertNotNull(event); assertEquals(MapEvent.Type.REMOVE, event.type()); assertTrue(Arrays.equals(value2, event.oldValue().value())); map.removeListener(listener).join(); } /** * Tests functionality specified in the {@link AtomixConsistentTreeMap} * interface, beyond the functionality provided in * {@link org.onosproject.store.service.AsyncConsistentMap}. */ @Test public void treeMapFunctionsTest() { AtomixConsistentTreeMap map = createResource("treeMapFunctionTestMap"); //Tests on empty map map.firstKey().thenAccept(result -> assertNull(result)).join(); map.lastKey().thenAccept(result -> assertNull(result)).join(); map.ceilingEntry(keyOne).thenAccept(result -> assertNull(result)) .join(); map.floorEntry(keyOne).thenAccept(result -> assertNull(result)).join(); map.higherEntry(keyOne).thenAccept(result -> assertNull(result)) .join(); map.lowerEntry(keyOne).thenAccept(result -> assertNull(result)).join(); map.firstEntry().thenAccept(result -> assertNull(result)).join(); map.lastEntry().thenAccept(result -> assertNull(result)).join(); map.pollFirstEntry().thenAccept(result -> assertNull(result)).join(); map.pollLastEntry().thenAccept(result -> assertNull(result)).join(); map.lowerKey(keyOne).thenAccept(result -> assertNull(result)).join(); map.floorKey(keyOne).thenAccept(result -> assertNull(result)).join(); map.ceilingKey(keyOne).thenAccept(result -> assertNull(result)) .join(); map.higherKey(keyOne).thenAccept(result -> assertNull(result)).join(); map.delete().join(); allKeys.forEach(key -> map.put( key, allValues.get(allKeys.indexOf(key))) .thenAccept(result -> assertNull(result)).join()); //Note ordering keys are in their proper ordering in ascending order //both in naming and in the allKeys list. map.firstKey().thenAccept(result -> assertEquals(keyOne, result)) .join(); map.lastKey().thenAccept(result -> assertEquals(keyFour, result)) .join(); map.ceilingEntry(keyOne) .thenAccept(result -> { assertEquals(keyOne, result.getKey()); assertArrayEquals(valueOne, result.getValue().value()); }) .join(); //adding an additional letter to make keyOne an unacceptable response map.ceilingEntry(keyOne + "a") .thenAccept(result -> { assertEquals(keyTwo, result.getKey()); assertArrayEquals(valueTwo, result.getValue().value()); }) .join(); map.ceilingEntry(keyFour + "a") .thenAccept(result -> { assertNull(result); }) .join(); map.floorEntry(keyTwo).thenAccept(result -> { assertEquals(keyTwo, result.getKey()); assertArrayEquals(valueTwo, result.getValue().value()); }) .join(); //shorten the key so it itself is not an acceptable reply map.floorEntry(keyTwo.substring(0, 2)).thenAccept(result -> { assertEquals(keyOne, result.getKey()); assertArrayEquals(valueOne, result.getValue().value()); }) .join(); // shorten least key so no acceptable response exists map.floorEntry(keyOne.substring(0, 1)).thenAccept( result -> assertNull(result)) .join(); map.higherEntry(keyTwo).thenAccept(result -> { assertEquals(keyThree, result.getKey()); assertArrayEquals(valueThree, result.getValue().value()); }) .join(); map.higherEntry(keyFour).thenAccept(result -> assertNull(result)) .join(); map.lowerEntry(keyFour).thenAccept(result -> { assertEquals(keyThree, result.getKey()); assertArrayEquals(valueThree, result.getValue().value()); }) .join(); map.lowerEntry(keyOne).thenAccept(result -> assertNull(result)) .join(); map.firstEntry().thenAccept(result -> { assertEquals(keyOne, result.getKey()); assertArrayEquals(valueOne, result.getValue().value()); }) .join(); map.lastEntry().thenAccept(result -> { assertEquals(keyFour, result.getKey()); assertArrayEquals(valueFour, result.getValue().value()); }) .join(); map.pollFirstEntry().thenAccept(result -> { assertEquals(keyOne, result.getKey()); assertArrayEquals(valueOne, result.getValue().value()); }); map.containsKey(keyOne).thenAccept(result -> assertFalse(result)) .join(); map.size().thenAccept(result -> assertEquals(3, (int) result)).join(); map.pollLastEntry().thenAccept(result -> { assertEquals(keyFour, result.getKey()); assertArrayEquals(valueFour, result.getValue().value()); }); map.containsKey(keyFour).thenAccept(result -> assertFalse(result)) .join(); map.size().thenAccept(result -> assertEquals(2, (int) result)).join(); //repopulate the missing entries allKeys.forEach(key -> map.put( key, allValues.get(allKeys.indexOf(key))) .thenAccept(result -> { if (key.equals(keyOne) || key.equals(keyFour)) { assertNull(result); } else { assertArrayEquals(allValues.get(allKeys.indexOf(key)), result.value()); } }) .join()); map.lowerKey(keyOne).thenAccept(result -> assertNull(result)).join(); map.lowerKey(keyThree).thenAccept( result -> assertEquals(keyTwo, result)) .join(); map.floorKey(keyThree).thenAccept( result -> assertEquals(keyThree, result)) .join(); //shortening the key so there is no acceptable response map.floorKey(keyOne.substring(0, 1)).thenAccept( result -> assertNull(result)) .join(); map.ceilingKey(keyTwo).thenAccept( result -> assertEquals(keyTwo, result)) .join(); //adding to highest key so there is no acceptable response map.ceilingKey(keyFour + "a") .thenAccept(reslt -> assertNull(reslt)) .join(); map.higherKey(keyThree).thenAccept( result -> assertEquals(keyFour, result)) .join(); map.higherKey(keyFour).thenAccept( result -> assertNull(result)) .join(); map.delete().join(); } private AtomixConsistentTreeMap createResource(String mapName) { try { AtomixConsistentTreeMap map = createAtomixClient(). getResource(mapName, AtomixConsistentTreeMap.class) .join(); return map; } catch (Throwable e) { throw new RuntimeException(e.toString()); } } private static class TestMapEventListener implements MapEventListener<String, byte[]> { private final BlockingQueue<MapEvent<String, byte[]>> queue = new ArrayBlockingQueue<>(1); @Override public void event(MapEvent<String, byte[]> event) { try { queue.put(event); } catch (InterruptedException e) { Throwables.propagate(e); } } public boolean eventReceived() { return !queue.isEmpty(); } public MapEvent<String, byte[]> event() throws InterruptedException { return queue.take(); } } /** * 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; } /** * Inner entry type for testing. * @param <K> * @param <V> */ private class InnerEntry<K, V> implements Map.Entry<K, V> { private K key; private V value; public InnerEntry(K key, V value) { this.key = key; this.value = value; } @Override public K getKey() { return key; } @Override public V getValue() { return value; } @Override public V setValue(V value) { V temp = this.value; this.value = value; return temp; } @Override public boolean equals(Object o) { if (!(o instanceof InnerEntry)) { return false; } InnerEntry other = (InnerEntry) o; boolean keysEqual = false; boolean valuesEqual = false; if (this.key instanceof byte[]) { if (other.getKey() instanceof byte[]) { keysEqual = Arrays.equals((byte[]) this.key, (byte[]) other.getKey()); } else { return false; } } else { keysEqual = this.getKey().equals(other.getKey()); } if (keysEqual) { if (this.value instanceof byte[]) { if (other.getValue() instanceof byte[]) { return Arrays.equals((byte[]) this.value, (byte[]) other.getValue()); } else { return false; } } else { return this.key.equals(other.getKey()); } } return false; } @Override public int hashCode() { return 0; } } }