/* * Copyright 2014 The Netty Project * * The Netty Project licenses this file to you 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 io.netty.util.collection; import org.junit.Before; import org.junit.Test; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Random; import java.util.Set; import static org.junit.Assert.*; /** * Tests for {@link IntObjectHashMap}. */ public class IntObjectHashMapTest { private static class Value { private final String name; Value(String name) { this.name = name; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (name == null ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Value other = (Value) obj; if (name == null) { if (other.name != null) { return false; } } else if (!name.equals(other.name)) { return false; } return true; } } private IntObjectHashMap<Value> map; @Before public void setup() { map = new IntObjectHashMap<Value>(); } @Test public void putNewMappingShouldSucceed() { Value v = new Value("v"); assertNull(map.put(1, v)); assertEquals(1, map.size()); assertTrue(map.containsKey(1)); assertTrue(map.containsValue(v)); assertEquals(v, map.get(1)); } @Test public void putShouldReplaceValue() { Value v1 = new Value("v1"); assertNull(map.put(1, v1)); // Replace the value. Value v2 = new Value("v2"); assertSame(v1, map.put(1, v2)); assertEquals(1, map.size()); assertTrue(map.containsKey(1)); assertTrue(map.containsValue(v2)); assertEquals(v2, map.get(1)); } @Test public void putShouldGrowMap() { for (int i = 0; i < 10000; ++i) { Value v = new Value(Integer.toString(i)); assertNull(map.put(i, v)); assertEquals(i + 1, map.size()); assertTrue(map.containsKey(i)); assertTrue(map.containsValue(v)); assertEquals(v, map.get(i)); } } @Test public void negativeKeyShouldSucceed() { Value v = new Value("v"); map.put(-3, v); assertEquals(1, map.size()); assertEquals(v, map.get(-3)); } @Test public void removeMissingValueShouldReturnNull() { assertNull(map.remove(1)); assertEquals(0, map.size()); } @Test public void removeShouldReturnPreviousValue() { Value v = new Value("v"); map.put(1, v); assertSame(v, map.remove(1)); } /** * This test is a bit internal-centric. We're just forcing a rehash to occur based on no longer * having any FREE slots available. We do this by adding and then removing several keys up to * the capacity, so that no rehash is done. We then add one more, which will cause the rehash * due to a lack of free slots and verify that everything is still behaving properly */ @Test public void noFreeSlotsShouldRehash() { for (int i = 0; i < 10; ++i) { map.put(i, new Value(Integer.toString(i))); // Now mark it as REMOVED so that size won't cause the rehash. map.remove(i); assertEquals(0, map.size()); } // Now add an entry to force the rehash since no FREE slots are available in the map. Value v = new Value("v"); map.put(1, v); assertEquals(1, map.size()); assertSame(v, map.get(1)); } @Test public void putAllShouldSucceed() { Value v1 = new Value("v1"); Value v2 = new Value("v2"); Value v3 = new Value("v3"); map.put(1, v1); map.put(2, v2); map.put(3, v3); IntObjectHashMap<Value> map2 = new IntObjectHashMap<Value>(); map2.putAll(map); assertEquals(3, map2.size()); assertSame(v1, map2.get(1)); assertSame(v2, map2.get(2)); assertSame(v3, map2.get(3)); } @Test public void clearShouldSucceed() { Value v1 = new Value("v1"); Value v2 = new Value("v2"); Value v3 = new Value("v3"); map.put(1, v1); map.put(2, v2); map.put(3, v3); map.clear(); assertEquals(0, map.size()); assertTrue(map.isEmpty()); } @Test public void containsValueShouldFindNull() { map.put(1, new Value("v1")); map.put(2, null); map.put(3, new Value("v2")); assertTrue(map.containsValue(null)); } @Test public void containsValueShouldFindInstance() { Value v = new Value("v1"); map.put(1, new Value("v2")); map.put(2, new Value("v3")); map.put(3, v); assertTrue(map.containsValue(v)); } @Test public void containsValueShouldFindEquivalentValue() { map.put(1, new Value("v1")); map.put(2, new Value("v2")); map.put(3, new Value("v3")); assertTrue(map.containsValue(new Value("v2"))); } @Test public void containsValueNotFindMissingValue() { map.put(1, new Value("v1")); map.put(2, new Value("v2")); map.put(3, new Value("v3")); assertFalse(map.containsValue(new Value("v4"))); } @Test public void iteratorShouldTraverseEntries() { map.put(1, new Value("v1")); map.put(2, new Value("v2")); map.put(3, new Value("v3")); // Add and then immediately remove another entry. map.put(4, new Value("v4")); map.remove(4); Set<Integer> found = new HashSet<Integer>(); for (IntObjectMap.Entry<Value> entry : map.entries()) { assertTrue(found.add(entry.key())); } assertEquals(3, found.size()); assertTrue(found.contains(1)); assertTrue(found.contains(2)); assertTrue(found.contains(3)); } @Test public void keysShouldBeReturned() { map.put(1, new Value("v1")); map.put(2, new Value("v2")); map.put(3, new Value("v3")); // Add and then immediately remove another entry. map.put(4, new Value("v4")); map.remove(4); int[] keys = map.keys(); assertEquals(3, keys.length); Set<Integer> expected = new HashSet<Integer>(); expected.add(1); expected.add(2); expected.add(3); Set<Integer> found = new HashSet<Integer>(); for (int key : keys) { assertTrue(found.add(key)); } assertEquals(expected, found); } @Test public void valuesShouldBeReturned() { Value v1 = new Value("v1"); Value v2 = new Value("v2"); Value v3 = new Value("v3"); map.put(1, v1); map.put(2, v2); map.put(3, v3); // Add and then immediately remove another entry. map.put(4, new Value("v4")); map.remove(4); // Ensure values() return all values. Set<Value> expected = new HashSet<Value>(); Set<Value> actual = new HashSet<Value>(); expected.add(v1); expected.add(v2); expected.add(v3); Value[] valueArray = map.values(Value.class); assertEquals(3, valueArray.length); for (Value value : valueArray) { assertTrue(actual.add(value)); } assertEquals(expected, actual); actual.clear(); Collection<Value> valueCollection = map.values(); assertEquals(3, valueCollection.size()); for (Value value : valueCollection) { assertTrue(actual.add(value)); } assertEquals(expected, actual); } @Test public void mapShouldSupportHashingConflicts() { for (int mod = 0; mod < 10; ++mod) { for (int sz = 1; sz <= 101; sz += 2) { IntObjectHashMap<String> map = new IntObjectHashMap<String>(sz); for (int i = 0; i < 100; ++i) { map.put(i * mod, ""); } } } } @Test public void hashcodeEqualsTest() { IntObjectHashMap<Integer> map1 = new IntObjectHashMap<Integer>(); IntObjectHashMap<Integer> map2 = new IntObjectHashMap<Integer>(); Random rnd = new Random(0); while (map1.size() < 100) { int key = rnd.nextInt(100); map1.put(key, key); map2.put(key, key); } assertEquals(map1.hashCode(), map2.hashCode()); assertEquals(map1, map2); // Remove one "middle" element, maps should now be non-equals. int[] keys = map1.keys(); map2.remove(keys[50]); assertFalse(map1.equals(map2)); // Put it back; will likely be in a different position, but maps will be equal again. map2.put(keys[50], map1.keys()[50]); assertEquals(map1, map2); assertEquals(map1.hashCode(), map2.hashCode()); // Make map2 have one extra element, will be non-equal. map2.put(1000, 1000); assertFalse(map1.equals(map2)); // Rebuild map2 with elements in a different order, again the maps should be equal. // (These tests with same elements in different order also show that the hashCode // function does not depend on the internal ordering of entries.) map2.clear(); Arrays.sort(keys); for (int key : keys) { map2.put(key, key); } assertEquals(map1.hashCode(), map2.hashCode()); assertEquals(map1, map2); } @Test public void fuzzTest() { // This test is so extremely internals-dependent that I'm not even trying to // minimize that. Any internal changes will not fail the test (so it's not flaky per se) // but will possibly make it less effective (not test interesting scenarios anymore). // The RNG algorithm is specified and stable, so this will cause the same exact dataset // to be used in every run and every JVM implementation. Random rnd = new Random(0); int baseSize = 1000; // Empirically-determined size to expand the capacity exactly once, and before // the step that creates the long conflict chain. We need to test rehash(), // but also control when rehash happens because it cleans up the REMOVED entries. // This size is also chosen so after the single rehash, the map will be densely // populated, getting close to a second rehash but not triggering it. int startTableSize = 1105; IntObjectHashMap<Integer> map = new IntObjectHashMap<Integer>(startTableSize); // Reference map which implementation we trust to be correct, will mirror all operations. HashMap<Integer, Integer> goodMap = new HashMap<Integer, Integer>(); // Add initial population. for (int i = 0; i < baseSize / 4; ++i) { int key = rnd.nextInt(baseSize); assertEquals(goodMap.put(key, key), map.put(key, key)); // 50% elements are multiple of a divisor of startTableSize => more conflicts. key = rnd.nextInt(baseSize) * 17; assertEquals(goodMap.put(key, key), map.put(key, key)); } // Now do some mixed adds and removes for further fuzzing // Rehash will happen here, but only once, and the final size will be closer to max. for (int i = 0; i < baseSize * 1000; ++i) { int key = rnd.nextInt(baseSize); if (rnd.nextDouble() >= 0.2) { assertEquals(goodMap.put(key, key), map.put(key, key)); } else { assertEquals(goodMap.remove(key), map.remove(key)); } } // Final batch of fuzzing, only searches and removes. int removeSize = map.size() / 2; while (removeSize > 0) { int key = rnd.nextInt(baseSize); boolean found = goodMap.containsKey(key); assertEquals(found, map.containsKey(key)); assertEquals(goodMap.remove(key), map.remove(key)); if (found) { --removeSize; } } // Now gotta write some code to compare the final maps, as equals() won't work. assertEquals(goodMap.size(), map.size()); Integer[] goodKeys = goodMap.keySet().toArray(new Integer[goodMap.size()]); Arrays.sort(goodKeys); int [] keys = map.keys(); Arrays.sort(keys); for (int i = 0; i < goodKeys.length; ++i) { assertEquals((int) goodKeys[i], keys[i]); } // Finally drain the map. for (int key : map.keys()) { assertEquals(goodMap.remove(key), map.remove(key)); } assertTrue(map.isEmpty()); } }