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
17package com.google.common.collect;
18
19import static com.google.common.collect.MapMakerInternalMap.Strength.STRONG;
20import static com.google.common.collect.MapMakerInternalMap.Strength.WEAK;
21import static com.google.common.testing.SerializableTester.reserializeAndAssert;
22import static java.util.Arrays.asList;
23import static org.easymock.EasyMock.eq;
24import static org.easymock.EasyMock.expect;
25import static org.easymock.EasyMock.isA;
26
27import com.google.common.base.Equivalence;
28import com.google.common.collect.MapMaker.RemovalListener;
29import com.google.common.collect.MapMaker.RemovalNotification;
30import com.google.common.collect.testing.features.CollectionFeature;
31import com.google.common.collect.testing.features.CollectionSize;
32import com.google.common.collect.testing.google.MultisetTestSuiteBuilder;
33import com.google.common.collect.testing.google.TestStringMultisetGenerator;
34
35import junit.framework.Test;
36import junit.framework.TestCase;
37import junit.framework.TestSuite;
38
39import org.easymock.EasyMock;
40
41import java.util.Collections;
42import java.util.Iterator;
43import java.util.List;
44import java.util.concurrent.ConcurrentMap;
45import java.util.concurrent.ConcurrentSkipListMap;
46import java.util.concurrent.TimeUnit;
47import java.util.concurrent.atomic.AtomicInteger;
48
49/**
50 * Test case for {@link ConcurrentHashMultiset}.
51 *
52 * @author Cliff L. Biffle
53 * @author mike nonemacher
54 */
55public 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