| /* |
| * Copyright (C) 2007 The Guava Authors |
| * |
| * 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 com.google.common.collect; |
| |
| import static com.google.common.collect.MapMakerInternalMap.Strength.STRONG; |
| import static com.google.common.collect.MapMakerInternalMap.Strength.WEAK; |
| import static com.google.common.testing.SerializableTester.reserializeAndAssert; |
| import static java.util.Arrays.asList; |
| import static org.mockito.Mockito.eq; |
| import static org.mockito.Mockito.isA; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.when; |
| |
| import com.google.common.base.Equivalence; |
| import com.google.common.collect.testing.features.CollectionFeature; |
| import com.google.common.collect.testing.features.CollectionSize; |
| import com.google.common.collect.testing.google.MultisetTestSuiteBuilder; |
| import com.google.common.collect.testing.google.TestStringMultisetGenerator; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.concurrent.ConcurrentSkipListMap; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import junit.framework.Test; |
| import junit.framework.TestCase; |
| import junit.framework.TestSuite; |
| |
| /** |
| * Test case for {@link ConcurrentHashMultiset}. |
| * |
| * @author Cliff L. Biffle |
| * @author mike nonemacher |
| */ |
| public class ConcurrentHashMultisetTest extends TestCase { |
| |
| public static Test suite() { |
| TestSuite suite = new TestSuite(); |
| suite.addTest( |
| MultisetTestSuiteBuilder.using(concurrentHashMultisetGenerator()) |
| .withFeatures( |
| CollectionSize.ANY, |
| CollectionFeature.GENERAL_PURPOSE, |
| CollectionFeature.SERIALIZABLE, |
| CollectionFeature.ALLOWS_NULL_QUERIES) |
| .named("ConcurrentHashMultiset") |
| .createTestSuite()); |
| suite.addTest( |
| MultisetTestSuiteBuilder.using(concurrentSkipListMultisetGenerator()) |
| .withFeatures( |
| CollectionSize.ANY, |
| CollectionFeature.KNOWN_ORDER, |
| CollectionFeature.GENERAL_PURPOSE, |
| CollectionFeature.SERIALIZABLE, |
| CollectionFeature.ALLOWS_NULL_QUERIES) |
| .named("ConcurrentSkipListMultiset") |
| .createTestSuite()); |
| suite.addTestSuite(ConcurrentHashMultisetTest.class); |
| return suite; |
| } |
| |
| private static TestStringMultisetGenerator concurrentHashMultisetGenerator() { |
| return new TestStringMultisetGenerator() { |
| @Override |
| protected Multiset<String> create(String[] elements) { |
| return ConcurrentHashMultiset.create(asList(elements)); |
| } |
| }; |
| } |
| |
| private static TestStringMultisetGenerator concurrentSkipListMultisetGenerator() { |
| return new TestStringMultisetGenerator() { |
| @Override |
| protected Multiset<String> create(String[] elements) { |
| Multiset<String> multiset = |
| new ConcurrentHashMultiset<>(new ConcurrentSkipListMap<String, AtomicInteger>()); |
| Collections.addAll(multiset, elements); |
| return multiset; |
| } |
| |
| @Override |
| public List<String> order(List<String> insertionOrder) { |
| return Ordering.natural().sortedCopy(insertionOrder); |
| } |
| }; |
| } |
| |
| private static final String KEY = "puppies"; |
| |
| ConcurrentMap<String, AtomicInteger> backingMap; |
| ConcurrentHashMultiset<String> multiset; |
| |
| @SuppressWarnings("unchecked") |
| @Override |
| protected void setUp() { |
| backingMap = mock(ConcurrentMap.class); |
| when(backingMap.isEmpty()).thenReturn(true); |
| |
| multiset = new ConcurrentHashMultiset<>(backingMap); |
| } |
| |
| public void testCount_elementPresent() { |
| final int COUNT = 12; |
| when(backingMap.get(KEY)).thenReturn(new AtomicInteger(COUNT)); |
| |
| assertEquals(COUNT, multiset.count(KEY)); |
| } |
| |
| public void testCount_elementAbsent() { |
| when(backingMap.get(KEY)).thenReturn(null); |
| |
| assertEquals(0, multiset.count(KEY)); |
| } |
| |
| public void testAdd_zero() { |
| final int INITIAL_COUNT = 32; |
| |
| when(backingMap.get(KEY)).thenReturn(new AtomicInteger(INITIAL_COUNT)); |
| assertEquals(INITIAL_COUNT, multiset.add(KEY, 0)); |
| } |
| |
| public void testAdd_firstFewWithSuccess() { |
| final int COUNT = 400; |
| |
| when(backingMap.get(KEY)).thenReturn(null); |
| when(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).thenReturn(null); |
| |
| assertEquals(0, multiset.add(KEY, COUNT)); |
| } |
| |
| public void testAdd_laterFewWithSuccess() { |
| int INITIAL_COUNT = 32; |
| int COUNT_TO_ADD = 400; |
| |
| AtomicInteger initial = new AtomicInteger(INITIAL_COUNT); |
| when(backingMap.get(KEY)).thenReturn(initial); |
| |
| assertEquals(INITIAL_COUNT, multiset.add(KEY, COUNT_TO_ADD)); |
| assertEquals(INITIAL_COUNT + COUNT_TO_ADD, initial.get()); |
| } |
| |
| public void testAdd_laterFewWithOverflow() { |
| final int INITIAL_COUNT = 92384930; |
| final int COUNT_TO_ADD = Integer.MAX_VALUE - INITIAL_COUNT + 1; |
| |
| when(backingMap.get(KEY)).thenReturn(new AtomicInteger(INITIAL_COUNT)); |
| |
| try { |
| multiset.add(KEY, COUNT_TO_ADD); |
| fail("Must reject arguments that would cause counter overflow."); |
| } catch (IllegalArgumentException expected) { |
| } |
| } |
| |
| /** |
| * Simulate some of the races that can happen on add. We can't easily simulate the race that |
| * happens when an {@link AtomicInteger#compareAndSet} fails, but we can simulate the case where |
| * the putIfAbsent returns a non-null value, and the case where the replace() of an observed zero |
| * fails. |
| */ |
| public void testAdd_withFailures() { |
| AtomicInteger existing = new AtomicInteger(12); |
| AtomicInteger existingZero = new AtomicInteger(0); |
| |
| // initial map.get() |
| when(backingMap.get(KEY)).thenReturn(null); |
| // since get returned null, try a putIfAbsent; that fails due to a simulated race |
| when(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).thenReturn(existingZero); |
| // since the putIfAbsent returned a zero, we'll try to replace... |
| when(backingMap.replace(eq(KEY), eq(existingZero), isA(AtomicInteger.class))).thenReturn(false); |
| // ...and then putIfAbsent. Simulate failure on both |
| when(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).thenReturn(existing); |
| |
| // next map.get() |
| when(backingMap.get(KEY)).thenReturn(existingZero); |
| // since get returned zero, try a replace; that fails due to a simulated race |
| when(backingMap.replace(eq(KEY), eq(existingZero), isA(AtomicInteger.class))).thenReturn(false); |
| when(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).thenReturn(existing); |
| |
| // another map.get() |
| when(backingMap.get(KEY)).thenReturn(existing); |
| // we shouldn't see any more map operations; CHM will now just update the AtomicInteger |
| |
| assertEquals(12, multiset.add(KEY, 3)); |
| assertEquals(15, existing.get()); |
| } |
| |
| public void testRemove_zeroFromSome() { |
| final int INITIAL_COUNT = 14; |
| when(backingMap.get(KEY)).thenReturn(new AtomicInteger(INITIAL_COUNT)); |
| |
| assertEquals(INITIAL_COUNT, multiset.remove(KEY, 0)); |
| } |
| |
| public void testRemove_zeroFromNone() { |
| when(backingMap.get(KEY)).thenReturn(null); |
| |
| assertEquals(0, multiset.remove(KEY, 0)); |
| } |
| |
| public void testRemove_nonePresent() { |
| when(backingMap.get(KEY)).thenReturn(null); |
| |
| assertEquals(0, multiset.remove(KEY, 400)); |
| } |
| |
| public void testRemove_someRemaining() { |
| int countToRemove = 30; |
| int countRemaining = 1; |
| AtomicInteger current = new AtomicInteger(countToRemove + countRemaining); |
| |
| when(backingMap.get(KEY)).thenReturn(current); |
| |
| assertEquals(countToRemove + countRemaining, multiset.remove(KEY, countToRemove)); |
| assertEquals(countRemaining, current.get()); |
| } |
| |
| public void testRemove_noneRemaining() { |
| int countToRemove = 30; |
| AtomicInteger current = new AtomicInteger(countToRemove); |
| |
| when(backingMap.get(KEY)).thenReturn(current); |
| // it's ok if removal fails: another thread may have done the remove |
| when(backingMap.remove(KEY, current)).thenReturn(false); |
| |
| assertEquals(countToRemove, multiset.remove(KEY, countToRemove)); |
| assertEquals(0, current.get()); |
| } |
| |
| public void testRemoveExactly() { |
| ConcurrentHashMultiset<String> cms = ConcurrentHashMultiset.create(); |
| cms.add("a", 2); |
| cms.add("b", 3); |
| |
| try { |
| cms.removeExactly("a", -2); |
| fail(); |
| } catch (IllegalArgumentException expected) { |
| } |
| |
| assertTrue(cms.removeExactly("a", 0)); |
| assertEquals(2, cms.count("a")); |
| assertTrue(cms.removeExactly("c", 0)); |
| assertEquals(0, cms.count("c")); |
| |
| assertFalse(cms.removeExactly("a", 4)); |
| assertEquals(2, cms.count("a")); |
| assertTrue(cms.removeExactly("a", 2)); |
| assertEquals(0, cms.count("a")); |
| assertTrue(cms.removeExactly("b", 2)); |
| assertEquals(1, cms.count("b")); |
| } |
| |
| public void testIteratorRemove_actualMap() { |
| // Override to avoid using mocks. |
| multiset = ConcurrentHashMultiset.create(); |
| |
| multiset.add(KEY); |
| multiset.add(KEY + "_2"); |
| multiset.add(KEY); |
| |
| int mutations = 0; |
| for (Iterator<String> it = multiset.iterator(); it.hasNext(); ) { |
| it.next(); |
| it.remove(); |
| mutations++; |
| } |
| assertTrue(multiset.isEmpty()); |
| assertEquals(3, mutations); |
| } |
| |
| public void testSetCount_basic() { |
| int initialCount = 20; |
| int countToSet = 40; |
| AtomicInteger current = new AtomicInteger(initialCount); |
| |
| when(backingMap.get(KEY)).thenReturn(current); |
| |
| assertEquals(initialCount, multiset.setCount(KEY, countToSet)); |
| assertEquals(countToSet, current.get()); |
| } |
| |
| public void testSetCount_asRemove() { |
| int countToRemove = 40; |
| AtomicInteger current = new AtomicInteger(countToRemove); |
| |
| when(backingMap.get(KEY)).thenReturn(current); |
| when(backingMap.remove(KEY, current)).thenReturn(true); |
| |
| assertEquals(countToRemove, multiset.setCount(KEY, 0)); |
| assertEquals(0, current.get()); |
| } |
| |
| public void testSetCount_0_nonePresent() { |
| when(backingMap.get(KEY)).thenReturn(null); |
| |
| assertEquals(0, multiset.setCount(KEY, 0)); |
| } |
| |
| public void testCreate() { |
| ConcurrentHashMultiset<Integer> multiset = ConcurrentHashMultiset.create(); |
| assertTrue(multiset.isEmpty()); |
| reserializeAndAssert(multiset); |
| } |
| |
| public void testCreateFromIterable() { |
| Iterable<Integer> iterable = asList(1, 2, 2, 3, 4); |
| ConcurrentHashMultiset<Integer> multiset = ConcurrentHashMultiset.create(iterable); |
| assertEquals(2, multiset.count(2)); |
| reserializeAndAssert(multiset); |
| } |
| |
| public void testIdentityKeyEquality_strongKeys() { |
| testIdentityKeyEquality(STRONG); |
| } |
| |
| public void testIdentityKeyEquality_weakKeys() { |
| testIdentityKeyEquality(WEAK); |
| } |
| |
| private void testIdentityKeyEquality(MapMakerInternalMap.Strength keyStrength) { |
| |
| ConcurrentMap<String, AtomicInteger> map = |
| new MapMaker().setKeyStrength(keyStrength).keyEquivalence(Equivalence.identity()).makeMap(); |
| |
| ConcurrentHashMultiset<String> multiset = ConcurrentHashMultiset.create(map); |
| |
| String s1 = new String("a"); |
| String s2 = new String("a"); |
| assertEquals(s1, s2); // Stating the obvious. |
| assertTrue(s1 != s2); // Stating the obvious. |
| |
| multiset.add(s1); |
| assertTrue(multiset.contains(s1)); |
| assertFalse(multiset.contains(s2)); |
| assertEquals(1, multiset.count(s1)); |
| assertEquals(0, multiset.count(s2)); |
| |
| multiset.add(s1); |
| multiset.add(s2, 3); |
| assertEquals(2, multiset.count(s1)); |
| assertEquals(3, multiset.count(s2)); |
| |
| multiset.remove(s1); |
| assertEquals(1, multiset.count(s1)); |
| assertEquals(3, multiset.count(s2)); |
| } |
| |
| public void testLogicalKeyEquality_strongKeys() { |
| testLogicalKeyEquality(STRONG); |
| } |
| |
| public void testLogicalKeyEquality_weakKeys() { |
| testLogicalKeyEquality(WEAK); |
| } |
| |
| private void testLogicalKeyEquality(MapMakerInternalMap.Strength keyStrength) { |
| |
| ConcurrentMap<String, AtomicInteger> map = |
| new MapMaker().setKeyStrength(keyStrength).keyEquivalence(Equivalence.equals()).makeMap(); |
| |
| ConcurrentHashMultiset<String> multiset = ConcurrentHashMultiset.create(map); |
| |
| String s1 = new String("a"); |
| String s2 = new String("a"); |
| assertEquals(s1, s2); // Stating the obvious. |
| |
| multiset.add(s1); |
| assertTrue(multiset.contains(s1)); |
| assertTrue(multiset.contains(s2)); |
| assertEquals(1, multiset.count(s1)); |
| assertEquals(1, multiset.count(s2)); |
| |
| multiset.add(s2, 3); |
| assertEquals(4, multiset.count(s1)); |
| assertEquals(4, multiset.count(s2)); |
| |
| multiset.remove(s1); |
| assertEquals(3, multiset.count(s1)); |
| assertEquals(3, multiset.count(s2)); |
| } |
| |
| public void testSerializationWithMapMaker1() { |
| ConcurrentMap<String, AtomicInteger> map = new MapMaker().makeMap(); |
| multiset = ConcurrentHashMultiset.create(map); |
| reserializeAndAssert(multiset); |
| } |
| |
| public void testSerializationWithMapMaker2() { |
| ConcurrentMap<String, AtomicInteger> map = new MapMaker().makeMap(); |
| multiset = ConcurrentHashMultiset.create(map); |
| multiset.addAll(ImmutableList.of("a", "a", "b", "c", "d", "b")); |
| reserializeAndAssert(multiset); |
| } |
| |
| public void testSerializationWithMapMaker3() { |
| ConcurrentMap<String, AtomicInteger> map = new MapMaker().makeMap(); |
| multiset = ConcurrentHashMultiset.create(map); |
| multiset.addAll(ImmutableList.of("a", "a", "b", "c", "d", "b")); |
| reserializeAndAssert(multiset); |
| } |
| |
| public void testSerializationWithMapMaker_preservesIdentityKeyEquivalence() { |
| ConcurrentMap<String, AtomicInteger> map = |
| new MapMaker().keyEquivalence(Equivalence.identity()).makeMap(); |
| |
| ConcurrentHashMultiset<String> multiset = ConcurrentHashMultiset.create(map); |
| multiset = reserializeAndAssert(multiset); |
| |
| String s1 = new String("a"); |
| String s2 = new String("a"); |
| assertEquals(s1, s2); // Stating the obvious. |
| assertTrue(s1 != s2); // Stating the obvious. |
| |
| multiset.add(s1); |
| assertTrue(multiset.contains(s1)); |
| assertFalse(multiset.contains(s2)); |
| assertEquals(1, multiset.count(s1)); |
| assertEquals(0, multiset.count(s2)); |
| } |
| } |