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