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