Home | History | Annotate | Download | only in collect
      1 /*
      2  * Copyright (C) 2007 The Guava Authors
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.google.common.collect;
     18 
     19 import static com.google.common.collect.MapMakerInternalMap.Strength.STRONG;
     20 import static com.google.common.collect.MapMakerInternalMap.Strength.WEAK;
     21 import static com.google.common.testing.SerializableTester.reserializeAndAssert;
     22 import static java.util.Arrays.asList;
     23 import static org.easymock.EasyMock.eq;
     24 import static org.easymock.EasyMock.expect;
     25 import static org.easymock.EasyMock.isA;
     26 
     27 import com.google.common.base.Equivalence;
     28 import com.google.common.collect.MapMaker.RemovalListener;
     29 import com.google.common.collect.MapMaker.RemovalNotification;
     30 import com.google.common.collect.testing.features.CollectionFeature;
     31 import com.google.common.collect.testing.features.CollectionSize;
     32 import com.google.common.collect.testing.google.MultisetTestSuiteBuilder;
     33 import com.google.common.collect.testing.google.TestStringMultisetGenerator;
     34 
     35 import junit.framework.Test;
     36 import junit.framework.TestCase;
     37 import junit.framework.TestSuite;
     38 
     39 import org.easymock.EasyMock;
     40 
     41 import java.util.Collections;
     42 import java.util.Iterator;
     43 import java.util.List;
     44 import java.util.concurrent.ConcurrentMap;
     45 import java.util.concurrent.TimeUnit;
     46 import java.util.concurrent.atomic.AtomicInteger;
     47 
     48 /**
     49  * Test case for {@link ConcurrentHashMultiset}.
     50  *
     51  * @author Cliff L. Biffle
     52  * @author mike nonemacher
     53  */
     54 public class ConcurrentHashMultisetTest extends TestCase {
     55 
     56   public static Test suite() {
     57     TestSuite suite = new TestSuite();
     58     suite.addTest(MultisetTestSuiteBuilder.using(concurrentHashMultisetGenerator())
     59         .withFeatures(CollectionSize.ANY,
     60             CollectionFeature.GENERAL_PURPOSE,
     61             CollectionFeature.SERIALIZABLE,
     62             CollectionFeature.ALLOWS_NULL_QUERIES)
     63         .named("ConcurrentHashMultiset")
     64         .createTestSuite());
     65     suite.addTestSuite(ConcurrentHashMultisetTest.class);
     66     return suite;
     67   }
     68 
     69   private static TestStringMultisetGenerator concurrentHashMultisetGenerator() {
     70     return new TestStringMultisetGenerator() {
     71       @Override protected Multiset<String> create(String[] elements) {
     72         return ConcurrentHashMultiset.create(asList(elements));
     73       }
     74     };
     75   }
     76 
     77   private static final String KEY = "puppies";
     78 
     79   ConcurrentMap<String, AtomicInteger> backingMap;
     80   ConcurrentHashMultiset<String> multiset;
     81 
     82   @SuppressWarnings("unchecked")
     83   @Override protected void setUp() {
     84     backingMap = EasyMock.createMock(ConcurrentMap.class);
     85     expect(backingMap.isEmpty()).andReturn(true);
     86     replay();
     87 
     88     multiset = new ConcurrentHashMultiset<String>(backingMap);
     89     verify();
     90     reset();
     91   }
     92 
     93   public void testCount_elementPresent() {
     94     final int COUNT = 12;
     95     expect(backingMap.get(KEY)).andReturn(new AtomicInteger(COUNT));
     96     replay();
     97 
     98     assertEquals(COUNT, multiset.count(KEY));
     99     verify();
    100   }
    101 
    102   public void testCount_elementAbsent() {
    103     expect(backingMap.get(KEY)).andReturn(null);
    104     replay();
    105 
    106     assertEquals(0, multiset.count(KEY));
    107     verify();
    108   }
    109 
    110   public void testAdd_zero() {
    111     final int INITIAL_COUNT = 32;
    112 
    113     expect(backingMap.get(KEY)).andReturn(new AtomicInteger(INITIAL_COUNT));
    114     replay();
    115     assertEquals(INITIAL_COUNT, multiset.add(KEY, 0));
    116     verify();
    117   }
    118 
    119   public void testAdd_firstFewWithSuccess() {
    120     final int COUNT = 400;
    121 
    122     expect(backingMap.get(KEY)).andReturn(null);
    123     expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(null);
    124     replay();
    125 
    126     assertEquals(0, multiset.add(KEY, COUNT));
    127     verify();
    128   }
    129 
    130   public void testAdd_laterFewWithSuccess() {
    131     int INITIAL_COUNT = 32;
    132     int COUNT_TO_ADD = 400;
    133 
    134     AtomicInteger initial = new AtomicInteger(INITIAL_COUNT);
    135     expect(backingMap.get(KEY)).andReturn(initial);
    136     replay();
    137 
    138     assertEquals(INITIAL_COUNT, multiset.add(KEY, COUNT_TO_ADD));
    139     assertEquals(INITIAL_COUNT + COUNT_TO_ADD, initial.get());
    140     verify();
    141   }
    142 
    143   public void testAdd_laterFewWithOverflow() {
    144     final int INITIAL_COUNT = 92384930;
    145     final int COUNT_TO_ADD = Integer.MAX_VALUE - INITIAL_COUNT + 1;
    146 
    147     expect(backingMap.get(KEY)).andReturn(new AtomicInteger(INITIAL_COUNT));
    148     replay();
    149 
    150     try {
    151       multiset.add(KEY, COUNT_TO_ADD);
    152       fail("Must reject arguments that would cause counter overflow.");
    153     } catch (IllegalArgumentException e) {
    154       // Expected.
    155     }
    156     verify();
    157   }
    158 
    159   /**
    160    * Simulate some of the races that can happen on add. We can't easily simulate the race that
    161    * happens when an {@link AtomicInteger#compareAndSet} fails, but we can simulate the case where
    162    * the putIfAbsent returns a non-null value, and the case where the replace() of an observed
    163    * zero fails.
    164    */
    165   public void testAdd_withFailures() {
    166     AtomicInteger existing = new AtomicInteger(12);
    167     AtomicInteger existingZero = new AtomicInteger(0);
    168 
    169     // initial map.get()
    170     expect(backingMap.get(KEY)).andReturn(null);
    171     // since get returned null, try a putIfAbsent; that fails due to a simulated race
    172     expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(existingZero);
    173     // since the putIfAbsent returned a zero, we'll try to replace...
    174     expect(backingMap.replace(eq(KEY), eq(existingZero), isA(AtomicInteger.class)))
    175         .andReturn(false);
    176     // ...and then putIfAbsent. Simulate failure on both
    177     expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(existing);
    178 
    179     // next map.get()
    180     expect(backingMap.get(KEY)).andReturn(existingZero);
    181     // since get returned zero, try a replace; that fails due to a simulated race
    182     expect(backingMap.replace(eq(KEY), eq(existingZero), isA(AtomicInteger.class)))
    183         .andReturn(false);
    184     expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(existing);
    185 
    186     // another map.get()
    187     expect(backingMap.get(KEY)).andReturn(existing);
    188     // we shouldn't see any more map operations; CHM will now just update the AtomicInteger
    189 
    190     replay();
    191 
    192     assertEquals(multiset.add(KEY, 3), 12);
    193     assertEquals(15, existing.get());
    194 
    195     verify();
    196   }
    197 
    198   public void testRemove_zeroFromSome() {
    199     final int INITIAL_COUNT = 14;
    200     expect(backingMap.get(KEY)).andReturn(new AtomicInteger(INITIAL_COUNT));
    201     replay();
    202 
    203     assertEquals(INITIAL_COUNT, multiset.remove(KEY, 0));
    204     verify();
    205   }
    206 
    207   public void testRemove_zeroFromNone() {
    208     expect(backingMap.get(KEY)).andReturn(null);
    209     replay();
    210 
    211     assertEquals(0, multiset.remove(KEY, 0));
    212     verify();
    213   }
    214 
    215   public void testRemove_nonePresent() {
    216     expect(backingMap.get(KEY)).andReturn(null);
    217     replay();
    218 
    219     assertEquals(0, multiset.remove(KEY, 400));
    220     verify();
    221   }
    222 
    223   public void testRemove_someRemaining() {
    224     int countToRemove = 30;
    225     int countRemaining = 1;
    226     AtomicInteger current = new AtomicInteger(countToRemove + countRemaining);
    227 
    228     expect(backingMap.get(KEY)).andReturn(current);
    229     replay();
    230 
    231     assertEquals(countToRemove + countRemaining, multiset.remove(KEY, countToRemove));
    232     assertEquals(countRemaining, current.get());
    233     verify();
    234   }
    235 
    236   public void testRemove_noneRemaining() {
    237     int countToRemove = 30;
    238     AtomicInteger current = new AtomicInteger(countToRemove);
    239 
    240     expect(backingMap.get(KEY)).andReturn(current);
    241     // it's ok if removal fails: another thread may have done the remove
    242     expect(backingMap.remove(KEY, current)).andReturn(false);
    243     replay();
    244 
    245     assertEquals(countToRemove, multiset.remove(KEY, countToRemove));
    246     assertEquals(0, current.get());
    247     verify();
    248   }
    249 
    250   public void testRemoveExactly() {
    251     ConcurrentHashMultiset<String> cms = ConcurrentHashMultiset.create();
    252     cms.add("a", 2);
    253     cms.add("b", 3);
    254 
    255     try {
    256       cms.removeExactly("a", -2);
    257     } catch (IllegalArgumentException expected) {}
    258 
    259     assertTrue(cms.removeExactly("a", 0));
    260     assertEquals(2, cms.count("a"));
    261     assertTrue(cms.removeExactly("c", 0));
    262     assertEquals(0, cms.count("c"));
    263 
    264     assertFalse(cms.removeExactly("a", 4));
    265     assertEquals(2, cms.count("a"));
    266     assertTrue(cms.removeExactly("a", 2));
    267     assertEquals(0, cms.count("a"));
    268     assertTrue(cms.removeExactly("b", 2));
    269     assertEquals(1, cms.count("b"));
    270   }
    271 
    272   public void testIteratorRemove_actualMap() {
    273     // Override to avoid using mocks.
    274     multiset = ConcurrentHashMultiset.create();
    275 
    276     multiset.add(KEY);
    277     multiset.add(KEY + "_2");
    278     multiset.add(KEY);
    279 
    280     int mutations = 0;
    281     for (Iterator<String> it = multiset.iterator(); it.hasNext(); ) {
    282       it.next();
    283       it.remove();
    284       mutations++;
    285     }
    286     assertTrue(multiset.isEmpty());
    287     assertEquals(3, mutations);
    288   }
    289 
    290   public void testSetCount_basic() {
    291     int initialCount = 20;
    292     int countToSet = 40;
    293     AtomicInteger current = new AtomicInteger(initialCount);
    294 
    295     expect(backingMap.get(KEY)).andReturn(current);
    296     replay();
    297 
    298     assertEquals(initialCount, multiset.setCount(KEY, countToSet));
    299     assertEquals(countToSet, current.get());
    300     verify();
    301   }
    302 
    303   public void testSetCount_asRemove() {
    304     int countToRemove = 40;
    305     AtomicInteger current = new AtomicInteger(countToRemove);
    306 
    307     expect(backingMap.get(KEY)).andReturn(current);
    308     expect(backingMap.remove(KEY, current)).andReturn(true);
    309     replay();
    310 
    311     assertEquals(countToRemove, multiset.setCount(KEY, 0));
    312     assertEquals(0, current.get());
    313     verify();
    314   }
    315 
    316   public void testSetCount_0_nonePresent() {
    317     expect(backingMap.get(KEY)).andReturn(null);
    318     replay();
    319 
    320     assertEquals(0, multiset.setCount(KEY, 0));
    321     verify();
    322   }
    323 
    324   public void testCreate() {
    325     ConcurrentHashMultiset<Integer> multiset = ConcurrentHashMultiset.create();
    326     assertTrue(multiset.isEmpty());
    327     reserializeAndAssert(multiset);
    328   }
    329 
    330   public void testCreateFromIterable() {
    331     Iterable<Integer> iterable = asList(1, 2, 2, 3, 4);
    332     ConcurrentHashMultiset<Integer> multiset
    333         = ConcurrentHashMultiset.create(iterable);
    334     assertEquals(2, multiset.count(2));
    335     reserializeAndAssert(multiset);
    336   }
    337 
    338   public void testIdentityKeyEquality_strongKeys() {
    339     testIdentityKeyEquality(STRONG);
    340   }
    341 
    342   public void testIdentityKeyEquality_weakKeys() {
    343     testIdentityKeyEquality(WEAK);
    344   }
    345 
    346   private void testIdentityKeyEquality(
    347       MapMakerInternalMap.Strength keyStrength) {
    348 
    349     MapMaker mapMaker = new MapMaker()
    350         .setKeyStrength(keyStrength)
    351         .keyEquivalence(Equivalence.identity());
    352 
    353     ConcurrentHashMultiset<String> multiset =
    354         ConcurrentHashMultiset.create(mapMaker);
    355 
    356     String s1 = new String("a");
    357     String s2 = new String("a");
    358     assertEquals(s1, s2); // Stating the obvious.
    359     assertTrue(s1 != s2); // Stating the obvious.
    360 
    361     multiset.add(s1);
    362     assertTrue(multiset.contains(s1));
    363     assertFalse(multiset.contains(s2));
    364     assertEquals(1, multiset.count(s1));
    365     assertEquals(0, multiset.count(s2));
    366 
    367     multiset.add(s1);
    368     multiset.add(s2, 3);
    369     assertEquals(2, multiset.count(s1));
    370     assertEquals(3, multiset.count(s2));
    371 
    372     multiset.remove(s1);
    373     assertEquals(1, multiset.count(s1));
    374     assertEquals(3, multiset.count(s2));
    375   }
    376 
    377   public void testLogicalKeyEquality_strongKeys() {
    378     testLogicalKeyEquality(STRONG);
    379   }
    380 
    381   public void testLogicalKeyEquality_weakKeys() {
    382     testLogicalKeyEquality(WEAK);
    383   }
    384 
    385   private void testLogicalKeyEquality(
    386       MapMakerInternalMap.Strength keyStrength) {
    387 
    388     MapMaker mapMaker = new MapMaker()
    389         .setKeyStrength(keyStrength)
    390         .keyEquivalence(Equivalence.equals());
    391 
    392     ConcurrentHashMultiset<String> multiset =
    393         ConcurrentHashMultiset.create(mapMaker);
    394 
    395     String s1 = new String("a");
    396     String s2 = new String("a");
    397     assertEquals(s1, s2); // Stating the obvious.
    398 
    399     multiset.add(s1);
    400     assertTrue(multiset.contains(s1));
    401     assertTrue(multiset.contains(s2));
    402     assertEquals(1, multiset.count(s1));
    403     assertEquals(1, multiset.count(s2));
    404 
    405     multiset.add(s2, 3);
    406     assertEquals(4, multiset.count(s1));
    407     assertEquals(4, multiset.count(s2));
    408 
    409     multiset.remove(s1);
    410     assertEquals(3, multiset.count(s1));
    411     assertEquals(3, multiset.count(s2));
    412   }
    413 
    414   public void testSerializationWithMapMaker1() {
    415     MapMaker mapMaker = new MapMaker();
    416     multiset = ConcurrentHashMultiset.create(mapMaker);
    417     reserializeAndAssert(multiset);
    418   }
    419 
    420   public void testSerializationWithMapMaker2() {
    421     MapMaker mapMaker = new MapMaker();
    422     multiset = ConcurrentHashMultiset.create(mapMaker);
    423     multiset.addAll(ImmutableList.of("a", "a", "b", "c", "d", "b"));
    424     reserializeAndAssert(multiset);
    425   }
    426 
    427   public void testSerializationWithMapMaker3() {
    428     MapMaker mapMaker = new MapMaker().expireAfterWrite(1, TimeUnit.SECONDS);
    429     multiset = ConcurrentHashMultiset.create(mapMaker);
    430     multiset.addAll(ImmutableList.of("a", "a", "b", "c", "d", "b"));
    431     reserializeAndAssert(multiset);
    432   }
    433 
    434   public void testSerializationWithMapMaker_preservesIdentityKeyEquivalence() {
    435     MapMaker mapMaker = new MapMaker()
    436         .keyEquivalence(Equivalence.identity());
    437 
    438     ConcurrentHashMultiset<String> multiset =
    439         ConcurrentHashMultiset.create(mapMaker);
    440     multiset = reserializeAndAssert(multiset);
    441 
    442     String s1 = new String("a");
    443     String s2 = new String("a");
    444     assertEquals(s1, s2); // Stating the obvious.
    445     assertTrue(s1 != s2); // Stating the obvious.
    446 
    447     multiset.add(s1);
    448     assertTrue(multiset.contains(s1));
    449     assertFalse(multiset.contains(s2));
    450     assertEquals(1, multiset.count(s1));
    451     assertEquals(0, multiset.count(s2));
    452   }
    453 
    454 //  @Suppress(owner = "bmanes", detail = "Does not call the eviction listener")
    455 //  public void testWithMapMakerEvictionListener_BROKEN1()
    456 //      throws InterruptedException {
    457 //    MapEvictionListener<String, Number> evictionListener =
    458 //        mockEvictionListener();
    459 //    evictionListener.onEviction("a", 5);
    460 //    EasyMock.replay(evictionListener);
    461 //
    462 //    GenericMapMaker<String, Number> mapMaker = new MapMaker()
    463 //        .expireAfterWrite(100, TimeUnit.MILLISECONDS)
    464 //        .evictionListener(evictionListener);
    465 //
    466 //    ConcurrentHashMultiset<String> multiset =
    467 //        ConcurrentHashMultiset.create(mapMaker);
    468 //
    469 //    multiset.add("a", 5);
    470 //
    471 //    assertTrue(multiset.contains("a"));
    472 //    assertEquals(5, multiset.count("a"));
    473 //
    474 //    Thread.sleep(2000);
    475 //
    476 //    EasyMock.verify(evictionListener);
    477 //  }
    478 
    479 //  @Suppress(owner = "bmanes", detail = "Does not call the eviction listener")
    480 //  public void testWithMapMakerEvictionListener_BROKEN2()
    481 //      throws InterruptedException {
    482 //    MapEvictionListener<String, Number> evictionListener =
    483 //        mockEvictionListener();
    484 //    evictionListener.onEviction("a", 5);
    485 //    EasyMock.replay(evictionListener);
    486 //
    487 //    GenericMapMaker<String, Number> mapMaker = new MapMaker()
    488 //        .expireAfterWrite(100, TimeUnit.MILLISECONDS)
    489 //        .evictionListener(evictionListener);
    490 //
    491 //    ConcurrentHashMultiset<String> multiset =
    492 //        ConcurrentHashMultiset.create(mapMaker);
    493 //
    494 //    multiset.add("a", 5);
    495 //
    496 //    assertTrue(multiset.contains("a"));
    497 //    assertEquals(5, multiset.count("a"));
    498 //
    499 //    Thread.sleep(2000);
    500 //
    501 //    // This call should have the side-effect of calling the
    502 //    // eviction listener, but it does not.
    503 //    assertFalse(multiset.contains("a"));
    504 //
    505 //    EasyMock.verify(evictionListener);
    506 //  }
    507 
    508   public void testWithMapMakerEvictionListener() {
    509     final List<RemovalNotification<String, Number>> notificationQueue = Lists.newArrayList();
    510     RemovalListener<String, Number> removalListener =
    511         new RemovalListener<String, Number>() {
    512           @Override public void onRemoval(RemovalNotification<String, Number> notification) {
    513             notificationQueue.add(notification);
    514           }
    515         };
    516 
    517     @SuppressWarnings("deprecation") // TODO(kevinb): what to do?
    518     MapMaker mapMaker = new MapMaker()
    519         .concurrencyLevel(1)
    520         .maximumSize(1);
    521     /*
    522      * Cleverly ignore the return type now that ConcurrentHashMultiset accepts only MapMaker and not
    523      * the deprecated GenericMapMaker. We know that a RemovalListener<String, Number> is a type that
    524      * will work with ConcurrentHashMultiset.
    525      */
    526     mapMaker.removalListener(removalListener);
    527 
    528     ConcurrentHashMultiset<String> multiset = ConcurrentHashMultiset.create(mapMaker);
    529 
    530     multiset.add("a", 5);
    531     assertTrue(multiset.contains("a"));
    532     assertEquals(5, multiset.count("a"));
    533 
    534     multiset.add("b", 3);
    535 
    536     assertFalse(multiset.contains("a"));
    537     assertTrue(multiset.contains("b"));
    538     assertEquals(3, multiset.count("b"));
    539     RemovalNotification<String, Number> notification = Iterables.getOnlyElement(notificationQueue);
    540     assertEquals("a", notification.getKey());
    541     // The map evicted this entry, so CHM didn't have a chance to zero it.
    542     assertEquals(5, notification.getValue().intValue());
    543   }
    544 
    545   private void replay() {
    546     EasyMock.replay(backingMap);
    547   }
    548 
    549   private void verify() {
    550     EasyMock.verify(backingMap);
    551   }
    552 
    553   private void reset() {
    554     EasyMock.reset(backingMap);
    555   }
    556 }
    557