• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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")
setUp()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 
testCount_elementPresent()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 
testCount_elementAbsent()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 
testAdd_zero()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 
testAdd_firstFewWithSuccess()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 
testAdd_laterFewWithSuccess()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 
testAdd_laterFewWithOverflow()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    */
testAdd_withFailures()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 
testRemove_zeroFromSome()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 
testRemove_zeroFromNone()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 
testRemove_nonePresent()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 
testRemove_someRemaining()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 
testRemove_noneRemaining()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 
testIteratorRemove_actualMap()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 
testIterator()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 
testEntryIterator()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 
testSetCount_basic()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 
testSetCount_asRemove()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 
testSetCount_0_nonePresent()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 
testCreate()329   public void testCreate() {
330     ConcurrentHashMultiset<Integer> multiset = ConcurrentHashMultiset.create();
331     assertTrue(multiset.isEmpty());
332     reserializeAndAssert(multiset);
333   }
334 
testCreateFromIterable()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 
testIdentityKeyEquality_strongKeys()343   public void testIdentityKeyEquality_strongKeys() {
344     testIdentityKeyEquality(STRONG);
345   }
346 
testIdentityKeyEquality_softKeys()347   public void testIdentityKeyEquality_softKeys() {
348     testIdentityKeyEquality(SOFT);
349   }
350 
testIdentityKeyEquality_weakKeys()351   public void testIdentityKeyEquality_weakKeys() {
352     testIdentityKeyEquality(WEAK);
353   }
354 
testIdentityKeyEquality( MapMakerInternalMap.Strength keyStrength)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 
testLogicalKeyEquality_strongKeys()386   public void testLogicalKeyEquality_strongKeys() {
387     testLogicalKeyEquality(STRONG);
388   }
389 
testLogicalKeyEquality_softKeys()390   public void testLogicalKeyEquality_softKeys() {
391     testLogicalKeyEquality(SOFT);
392   }
393 
testLogicalKeyEquality_weakKeys()394   public void testLogicalKeyEquality_weakKeys() {
395     testLogicalKeyEquality(WEAK);
396   }
397 
testLogicalKeyEquality( MapMakerInternalMap.Strength keyStrength)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 
testSerializationWithMapMaker1()427   public void testSerializationWithMapMaker1() {
428     MapMaker mapMaker = new MapMaker();
429     multiset = ConcurrentHashMultiset.create(mapMaker);
430     reserializeAndAssert(multiset);
431   }
432 
testSerializationWithMapMaker2()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 
testSerializationWithMapMaker3()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 
testSerializationWithMapMaker_preservesIdentityKeyEquivalence()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 
testWithMapMakerEvictionListener()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 
replay()553   private void replay() {
554     EasyMock.replay(backingMap);
555   }
556 
verify()557   private void verify() {
558     EasyMock.verify(backingMap);
559   }
560 
reset()561   private void reset() {
562     EasyMock.reset(backingMap);
563   }
564 }
565