/* * 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.SOFT; import static com.google.common.collect.MapMakerInternalMap.Strength.STRONG; import static com.google.common.collect.MapMakerInternalMap.Strength.WEAK; import static com.google.common.collect.testing.IteratorFeature.SUPPORTS_REMOVE; import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static java.util.Arrays.asList; import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.isA; import com.google.common.base.Equivalence; import com.google.common.collect.MapMaker.RemovalListener; import com.google.common.collect.MapMaker.RemovalNotification; import com.google.common.collect.Multiset.Entry; import com.google.common.collect.testing.IteratorTester; import junit.framework.TestCase; import org.easymock.EasyMock; import java.util.Iterator; import java.util.List; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * Test case for {@link ConcurrentHashMultiset}. * * @author Cliff L. Biffle * @author mike nonemacher */ public class ConcurrentHashMultisetTest extends TestCase { private static final String KEY = "puppies"; ConcurrentMap<String, AtomicInteger> backingMap; ConcurrentHashMultiset<String> multiset; @SuppressWarnings("unchecked") @Override protected void setUp() { backingMap = EasyMock.createMock(ConcurrentMap.class); expect(backingMap.isEmpty()).andReturn(true); replay(); multiset = new ConcurrentHashMultiset<String>(backingMap); verify(); reset(); } public void testCount_elementPresent() { final int COUNT = 12; expect(backingMap.get(KEY)).andReturn(new AtomicInteger(COUNT)); replay(); assertEquals(COUNT, multiset.count(KEY)); verify(); } public void testCount_elementAbsent() { expect(backingMap.get(KEY)).andReturn(null); replay(); assertEquals(0, multiset.count(KEY)); verify(); } public void testAdd_zero() { final int INITIAL_COUNT = 32; expect(backingMap.get(KEY)).andReturn(new AtomicInteger(INITIAL_COUNT)); replay(); assertEquals(INITIAL_COUNT, multiset.add(KEY, 0)); verify(); } public void testAdd_firstFewWithSuccess() { final int COUNT = 400; expect(backingMap.get(KEY)).andReturn(null); expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(null); replay(); assertEquals(0, multiset.add(KEY, COUNT)); verify(); } public void testAdd_laterFewWithSuccess() { int INITIAL_COUNT = 32; int COUNT_TO_ADD = 400; AtomicInteger initial = new AtomicInteger(INITIAL_COUNT); expect(backingMap.get(KEY)).andReturn(initial); replay(); assertEquals(INITIAL_COUNT, multiset.add(KEY, COUNT_TO_ADD)); assertEquals(INITIAL_COUNT + COUNT_TO_ADD, initial.get()); verify(); } public void testAdd_laterFewWithOverflow() { final int INITIAL_COUNT = 92384930; final int COUNT_TO_ADD = Integer.MAX_VALUE - INITIAL_COUNT + 1; expect(backingMap.get(KEY)).andReturn(new AtomicInteger(INITIAL_COUNT)); replay(); try { multiset.add(KEY, COUNT_TO_ADD); fail("Must reject arguments that would cause counter overflow."); } catch (IllegalArgumentException e) { // Expected. } verify(); } /** * 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() expect(backingMap.get(KEY)).andReturn(null); // since get returned null, try a putIfAbsent; that fails due to a simulated race expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(existingZero); // since the putIfAbsent returned a zero, we'll try to replace... expect(backingMap.replace(eq(KEY), eq(existingZero), isA(AtomicInteger.class))) .andReturn(false); // ...and then putIfAbsent. Simulate failure on both expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(existing); // next map.get() expect(backingMap.get(KEY)).andReturn(existingZero); // since get returned zero, try a replace; that fails due to a simulated race expect(backingMap.replace(eq(KEY), eq(existingZero), isA(AtomicInteger.class))) .andReturn(false); expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(existing); // another map.get() expect(backingMap.get(KEY)).andReturn(existing); // we shouldn't see any more map operations; CHM will now just update the AtomicInteger replay(); assertEquals(multiset.add(KEY, 3), 12); assertEquals(15, existing.get()); verify(); } public void testRemove_zeroFromSome() { final int INITIAL_COUNT = 14; expect(backingMap.get(KEY)).andReturn(new AtomicInteger(INITIAL_COUNT)); replay(); assertEquals(INITIAL_COUNT, multiset.remove(KEY, 0)); verify(); } public void testRemove_zeroFromNone() { expect(backingMap.get(KEY)).andReturn(null); replay(); assertEquals(0, multiset.remove(KEY, 0)); verify(); } public void testRemove_nonePresent() { expect(backingMap.get(KEY)).andReturn(null); replay(); assertEquals(0, multiset.remove(KEY, 400)); verify(); } public void testRemove_someRemaining() { int countToRemove = 30; int countRemaining = 1; AtomicInteger current = new AtomicInteger(countToRemove + countRemaining); expect(backingMap.get(KEY)).andReturn(current); replay(); assertEquals(countToRemove + countRemaining, multiset.remove(KEY, countToRemove)); assertEquals(countRemaining, current.get()); verify(); } public void testRemove_noneRemaining() { int countToRemove = 30; AtomicInteger current = new AtomicInteger(countToRemove); expect(backingMap.get(KEY)).andReturn(current); // it's ok if removal fails: another thread may have done the remove expect(backingMap.remove(KEY, current)).andReturn(false); replay(); assertEquals(countToRemove, multiset.remove(KEY, countToRemove)); assertEquals(0, current.get()); verify(); } 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 testIterator() { // multiset.iterator List<String> expected = asList("a", "a", "b", "b", "b"); new IteratorTester<String>( 5, asList(SUPPORTS_REMOVE), expected, IteratorTester.KnownOrder.UNKNOWN_ORDER) { ConcurrentHashMultiset<String> multiset; @Override protected Iterator<String> newTargetIterator() { multiset = ConcurrentHashMultiset.create(); multiset.add("a", 2); multiset.add("b", 3); return multiset.iterator(); } @Override protected void verify(List<String> elements) { super.verify(elements); assertEquals(ImmutableMultiset.copyOf(elements), multiset); } }.test(); } public void testEntryIterator() { // multiset.entryIterator List<Entry<String>> expected = asList( Multisets.immutableEntry("a", 1), Multisets.immutableEntry("b", 2), Multisets.immutableEntry("c", 3), Multisets.immutableEntry("d", 4), Multisets.immutableEntry("e", 5)); new IteratorTester<Entry<String>>( 5, asList(SUPPORTS_REMOVE), expected, IteratorTester.KnownOrder.UNKNOWN_ORDER) { ConcurrentHashMultiset<String> multiset; @Override protected Iterator<Entry<String>> newTargetIterator() { multiset = ConcurrentHashMultiset.create(); multiset.add("a", 1); multiset.add("b", 2); multiset.add("c", 3); multiset.add("d", 4); multiset.add("e", 5); return multiset.entryIterator(); } @Override protected void verify(List<Entry<String>> elements) { super.verify(elements); assertEquals(ImmutableSet.copyOf(elements), ImmutableSet.copyOf(multiset.entryIterator())); } }.test(); } public void testSetCount_basic() { int initialCount = 20; int countToSet = 40; AtomicInteger current = new AtomicInteger(initialCount); expect(backingMap.get(KEY)).andReturn(current); replay(); assertEquals(initialCount, multiset.setCount(KEY, countToSet)); assertEquals(countToSet, current.get()); verify(); } public void testSetCount_asRemove() { int countToRemove = 40; AtomicInteger current = new AtomicInteger(countToRemove); expect(backingMap.get(KEY)).andReturn(current); expect(backingMap.remove(KEY, current)).andReturn(true); replay(); assertEquals(countToRemove, multiset.setCount(KEY, 0)); assertEquals(0, current.get()); verify(); } public void testSetCount_0_nonePresent() { expect(backingMap.get(KEY)).andReturn(null); replay(); assertEquals(0, multiset.setCount(KEY, 0)); verify(); } 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_softKeys() { testIdentityKeyEquality(SOFT); } public void testIdentityKeyEquality_weakKeys() { testIdentityKeyEquality(WEAK); } private void testIdentityKeyEquality( MapMakerInternalMap.Strength keyStrength) { MapMaker mapMaker = new MapMaker() .setKeyStrength(keyStrength) .keyEquivalence(Equivalence.identity()); ConcurrentHashMultiset<String> multiset = ConcurrentHashMultiset.create(mapMaker); 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_softKeys() { testLogicalKeyEquality(SOFT); } public void testLogicalKeyEquality_weakKeys() { testLogicalKeyEquality(WEAK); } private void testLogicalKeyEquality( MapMakerInternalMap.Strength keyStrength) { MapMaker mapMaker = new MapMaker() .setKeyStrength(keyStrength) .keyEquivalence(Equivalence.equals()); ConcurrentHashMultiset<String> multiset = ConcurrentHashMultiset.create(mapMaker); 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() { MapMaker mapMaker = new MapMaker(); multiset = ConcurrentHashMultiset.create(mapMaker); reserializeAndAssert(multiset); } public void testSerializationWithMapMaker2() { MapMaker mapMaker = new MapMaker(); multiset = ConcurrentHashMultiset.create(mapMaker); multiset.addAll(ImmutableList.of("a", "a", "b", "c", "d", "b")); reserializeAndAssert(multiset); } public void testSerializationWithMapMaker3() { MapMaker mapMaker = new MapMaker().expireAfterWrite(1, TimeUnit.SECONDS); multiset = ConcurrentHashMultiset.create(mapMaker); multiset.addAll(ImmutableList.of("a", "a", "b", "c", "d", "b")); reserializeAndAssert(multiset); } public void testSerializationWithMapMaker_preservesIdentityKeyEquivalence() { MapMaker mapMaker = new MapMaker() .keyEquivalence(Equivalence.identity()); ConcurrentHashMultiset<String> multiset = ConcurrentHashMultiset.create(mapMaker); 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)); } // @Suppress(owner = "bmanes", detail = "Does not call the eviction listener") // public void testWithMapMakerEvictionListener_BROKEN1() // throws InterruptedException { // MapEvictionListener<String, Number> evictionListener = // mockEvictionListener(); // evictionListener.onEviction("a", 5); // EasyMock.replay(evictionListener); // // GenericMapMaker<String, Number> mapMaker = new MapMaker() // .expireAfterWrite(100, TimeUnit.MILLISECONDS) // .evictionListener(evictionListener); // // ConcurrentHashMultiset<String> multiset = // ConcurrentHashMultiset.create(mapMaker); // // multiset.add("a", 5); // // assertTrue(multiset.contains("a")); // assertEquals(5, multiset.count("a")); // // Thread.sleep(2000); // // EasyMock.verify(evictionListener); // } // @Suppress(owner = "bmanes", detail = "Does not call the eviction listener") // public void testWithMapMakerEvictionListener_BROKEN2() // throws InterruptedException { // MapEvictionListener<String, Number> evictionListener = // mockEvictionListener(); // evictionListener.onEviction("a", 5); // EasyMock.replay(evictionListener); // // GenericMapMaker<String, Number> mapMaker = new MapMaker() // .expireAfterWrite(100, TimeUnit.MILLISECONDS) // .evictionListener(evictionListener); // // ConcurrentHashMultiset<String> multiset = // ConcurrentHashMultiset.create(mapMaker); // // multiset.add("a", 5); // // assertTrue(multiset.contains("a")); // assertEquals(5, multiset.count("a")); // // Thread.sleep(2000); // // // This call should have the side-effect of calling the // // eviction listener, but it does not. // assertFalse(multiset.contains("a")); // // EasyMock.verify(evictionListener); // } public void testWithMapMakerEvictionListener() { final List<RemovalNotification<String, Number>> notificationQueue = Lists.newArrayList(); RemovalListener<String, Number> removalListener = new RemovalListener<String, Number>() { @Override public void onRemoval(RemovalNotification<String, Number> notification) { notificationQueue.add(notification); } }; @SuppressWarnings("deprecation") // TODO(kevinb): what to do? GenericMapMaker<String, Number> mapMaker = new MapMaker() .concurrencyLevel(1) .maximumSize(1) .removalListener(removalListener); ConcurrentHashMultiset<String> multiset = ConcurrentHashMultiset.create(mapMaker); multiset.add("a", 5); assertTrue(multiset.contains("a")); assertEquals(5, multiset.count("a")); multiset.add("b", 3); assertFalse(multiset.contains("a")); assertTrue(multiset.contains("b")); assertEquals(3, multiset.count("b")); RemovalNotification<String, Number> notification = Iterables.getOnlyElement(notificationQueue); assertEquals("a", notification.getKey()); // The map evicted this entry, so CHM didn't have a chance to zero it. assertEquals(5, notification.getValue().intValue()); } private void replay() { EasyMock.replay(backingMap); } private void verify() { EasyMock.verify(backingMap); } private void reset() { EasyMock.reset(backingMap); } }