• 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.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.junit.Assert.assertThrows;
24 import static org.mockito.ArgumentMatchers.isA;
25 import static org.mockito.Mockito.eq;
26 import static org.mockito.Mockito.mock;
27 import static org.mockito.Mockito.when;
28 
29 import com.google.common.base.Equivalence;
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 import java.util.Collections;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.concurrent.ConcurrentMap;
38 import java.util.concurrent.ConcurrentSkipListMap;
39 import java.util.concurrent.atomic.AtomicInteger;
40 import junit.framework.Test;
41 import junit.framework.TestCase;
42 import junit.framework.TestSuite;
43 
44 /**
45  * Test case for {@link ConcurrentHashMultiset}.
46  *
47  * @author Cliff L. Biffle
48  * @author mike nonemacher
49  */
50 public class ConcurrentHashMultisetTest extends TestCase {
51 
suite()52   public static Test suite() {
53     TestSuite suite = new TestSuite();
54     suite.addTest(
55         MultisetTestSuiteBuilder.using(concurrentHashMultisetGenerator())
56             .withFeatures(
57                 CollectionSize.ANY,
58                 CollectionFeature.GENERAL_PURPOSE,
59                 CollectionFeature.SERIALIZABLE,
60                 CollectionFeature.ALLOWS_NULL_QUERIES)
61             .named("ConcurrentHashMultiset")
62             .createTestSuite());
63     suite.addTest(
64         MultisetTestSuiteBuilder.using(concurrentSkipListMultisetGenerator())
65             .withFeatures(
66                 CollectionSize.ANY,
67                 CollectionFeature.KNOWN_ORDER,
68                 CollectionFeature.GENERAL_PURPOSE,
69                 CollectionFeature.SERIALIZABLE,
70                 CollectionFeature.ALLOWS_NULL_QUERIES)
71             .named("ConcurrentSkipListMultiset")
72             .createTestSuite());
73     suite.addTestSuite(ConcurrentHashMultisetTest.class);
74     return suite;
75   }
76 
concurrentHashMultisetGenerator()77   private static TestStringMultisetGenerator concurrentHashMultisetGenerator() {
78     return new TestStringMultisetGenerator() {
79       @Override
80       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
89       protected Multiset<String> create(String[] elements) {
90         Multiset<String> multiset =
91             new ConcurrentHashMultiset<>(new ConcurrentSkipListMap<String, AtomicInteger>());
92         Collections.addAll(multiset, elements);
93         return multiset;
94       }
95 
96       @Override
97       public List<String> order(List<String> insertionOrder) {
98         return Ordering.natural().sortedCopy(insertionOrder);
99       }
100     };
101   }
102 
103   private static final String KEY = "puppies";
104 
105   ConcurrentMap<String, AtomicInteger> backingMap;
106   ConcurrentHashMultiset<String> multiset;
107 
108   @SuppressWarnings("unchecked")
109   @Override
110   protected void setUp() {
111     backingMap = mock(ConcurrentMap.class);
112     when(backingMap.isEmpty()).thenReturn(true);
113 
114     multiset = new ConcurrentHashMultiset<>(backingMap);
115   }
116 
117   public void testCount_elementPresent() {
118     final int COUNT = 12;
119     when(backingMap.get(KEY)).thenReturn(new AtomicInteger(COUNT));
120 
121     assertEquals(COUNT, multiset.count(KEY));
122   }
123 
124   public void testCount_elementAbsent() {
125     when(backingMap.get(KEY)).thenReturn(null);
126 
127     assertEquals(0, multiset.count(KEY));
128   }
129 
130   public void testAdd_zero() {
131     final int INITIAL_COUNT = 32;
132 
133     when(backingMap.get(KEY)).thenReturn(new AtomicInteger(INITIAL_COUNT));
134     assertEquals(INITIAL_COUNT, multiset.add(KEY, 0));
135   }
136 
137   public void testAdd_firstFewWithSuccess() {
138     final int COUNT = 400;
139 
140     when(backingMap.get(KEY)).thenReturn(null);
141     when(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).thenReturn(null);
142 
143     assertEquals(0, multiset.add(KEY, COUNT));
144   }
145 
146   public void testAdd_laterFewWithSuccess() {
147     int INITIAL_COUNT = 32;
148     int COUNT_TO_ADD = 400;
149 
150     AtomicInteger initial = new AtomicInteger(INITIAL_COUNT);
151     when(backingMap.get(KEY)).thenReturn(initial);
152 
153     assertEquals(INITIAL_COUNT, multiset.add(KEY, COUNT_TO_ADD));
154     assertEquals(INITIAL_COUNT + COUNT_TO_ADD, initial.get());
155   }
156 
157   public void testAdd_laterFewWithOverflow() {
158     final int INITIAL_COUNT = 92384930;
159     final int COUNT_TO_ADD = Integer.MAX_VALUE - INITIAL_COUNT + 1;
160 
161     when(backingMap.get(KEY)).thenReturn(new AtomicInteger(INITIAL_COUNT));
162 
163     assertThrows(IllegalArgumentException.class, () -> multiset.add(KEY, COUNT_TO_ADD));
164   }
165 
166   /**
167    * Simulate some of the races that can happen on add. We can't easily simulate the race that
168    * happens when an {@link AtomicInteger#compareAndSet} fails, but we can simulate the case where
169    * the putIfAbsent returns a non-null value, and the case where the replace() of an observed zero
170    * fails.
171    */
172   public void testAdd_withFailures() {
173     AtomicInteger existing = new AtomicInteger(12);
174     AtomicInteger existingZero = new AtomicInteger(0);
175 
176     // initial map.get()
177     when(backingMap.get(KEY)).thenReturn(null);
178     // since get returned null, try a putIfAbsent; that fails due to a simulated race
179     when(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).thenReturn(existingZero);
180     // since the putIfAbsent returned a zero, we'll try to replace...
181     when(backingMap.replace(eq(KEY), eq(existingZero), isA(AtomicInteger.class))).thenReturn(false);
182     // ...and then putIfAbsent. Simulate failure on both
183     when(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).thenReturn(existing);
184 
185     // next map.get()
186     when(backingMap.get(KEY)).thenReturn(existingZero);
187     // since get returned zero, try a replace; that fails due to a simulated race
188     when(backingMap.replace(eq(KEY), eq(existingZero), isA(AtomicInteger.class))).thenReturn(false);
189     when(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).thenReturn(existing);
190 
191     // another map.get()
192     when(backingMap.get(KEY)).thenReturn(existing);
193     // we shouldn't see any more map operations; CHM will now just update the AtomicInteger
194 
195     assertEquals(12, multiset.add(KEY, 3));
196     assertEquals(15, existing.get());
197   }
198 
199   public void testRemove_zeroFromSome() {
200     final int INITIAL_COUNT = 14;
201     when(backingMap.get(KEY)).thenReturn(new AtomicInteger(INITIAL_COUNT));
202 
203     assertEquals(INITIAL_COUNT, multiset.remove(KEY, 0));
204   }
205 
206   public void testRemove_zeroFromNone() {
207     when(backingMap.get(KEY)).thenReturn(null);
208 
209     assertEquals(0, multiset.remove(KEY, 0));
210   }
211 
212   public void testRemove_nonePresent() {
213     when(backingMap.get(KEY)).thenReturn(null);
214 
215     assertEquals(0, multiset.remove(KEY, 400));
216   }
217 
218   public void testRemove_someRemaining() {
219     int countToRemove = 30;
220     int countRemaining = 1;
221     AtomicInteger current = new AtomicInteger(countToRemove + countRemaining);
222 
223     when(backingMap.get(KEY)).thenReturn(current);
224 
225     assertEquals(countToRemove + countRemaining, multiset.remove(KEY, countToRemove));
226     assertEquals(countRemaining, current.get());
227   }
228 
229   public void testRemove_noneRemaining() {
230     int countToRemove = 30;
231     AtomicInteger current = new AtomicInteger(countToRemove);
232 
233     when(backingMap.get(KEY)).thenReturn(current);
234     // it's ok if removal fails: another thread may have done the remove
235     when(backingMap.remove(KEY, current)).thenReturn(false);
236 
237     assertEquals(countToRemove, multiset.remove(KEY, countToRemove));
238     assertEquals(0, current.get());
239   }
240 
241   public void testRemoveExactly() {
242     ConcurrentHashMultiset<String> cms = ConcurrentHashMultiset.create();
243     cms.add("a", 2);
244     cms.add("b", 3);
245 
246     assertThrows(IllegalArgumentException.class, () -> cms.removeExactly("a", -2));
247 
248     assertTrue(cms.removeExactly("a", 0));
249     assertEquals(2, cms.count("a"));
250     assertTrue(cms.removeExactly("c", 0));
251     assertEquals(0, cms.count("c"));
252 
253     assertFalse(cms.removeExactly("a", 4));
254     assertEquals(2, cms.count("a"));
255     assertTrue(cms.removeExactly("a", 2));
256     assertEquals(0, cms.count("a"));
257     assertTrue(cms.removeExactly("b", 2));
258     assertEquals(1, cms.count("b"));
259   }
260 
261   public void testIteratorRemove_actualMap() {
262     // Override to avoid using mocks.
263     multiset = ConcurrentHashMultiset.create();
264 
265     multiset.add(KEY);
266     multiset.add(KEY + "_2");
267     multiset.add(KEY);
268 
269     int mutations = 0;
270     for (Iterator<String> it = multiset.iterator(); it.hasNext(); ) {
271       it.next();
272       it.remove();
273       mutations++;
274     }
275     assertTrue(multiset.isEmpty());
276     assertEquals(3, mutations);
277   }
278 
279   public void testSetCount_basic() {
280     int initialCount = 20;
281     int countToSet = 40;
282     AtomicInteger current = new AtomicInteger(initialCount);
283 
284     when(backingMap.get(KEY)).thenReturn(current);
285 
286     assertEquals(initialCount, multiset.setCount(KEY, countToSet));
287     assertEquals(countToSet, current.get());
288   }
289 
290   public void testSetCount_asRemove() {
291     int countToRemove = 40;
292     AtomicInteger current = new AtomicInteger(countToRemove);
293 
294     when(backingMap.get(KEY)).thenReturn(current);
295     when(backingMap.remove(KEY, current)).thenReturn(true);
296 
297     assertEquals(countToRemove, multiset.setCount(KEY, 0));
298     assertEquals(0, current.get());
299   }
300 
301   public void testSetCount_0_nonePresent() {
302     when(backingMap.get(KEY)).thenReturn(null);
303 
304     assertEquals(0, multiset.setCount(KEY, 0));
305   }
306 
307   public void testCreate() {
308     ConcurrentHashMultiset<Integer> multiset = ConcurrentHashMultiset.create();
309     assertTrue(multiset.isEmpty());
310     reserializeAndAssert(multiset);
311   }
312 
313   public void testCreateFromIterable() {
314     Iterable<Integer> iterable = asList(1, 2, 2, 3, 4);
315     ConcurrentHashMultiset<Integer> multiset = ConcurrentHashMultiset.create(iterable);
316     assertEquals(2, multiset.count(2));
317     reserializeAndAssert(multiset);
318   }
319 
320   public void testIdentityKeyEquality_strongKeys() {
321     testIdentityKeyEquality(STRONG);
322   }
323 
324   public void testIdentityKeyEquality_weakKeys() {
325     testIdentityKeyEquality(WEAK);
326   }
327 
328   private void testIdentityKeyEquality(MapMakerInternalMap.Strength keyStrength) {
329 
330     ConcurrentMap<String, AtomicInteger> map =
331         new MapMaker().setKeyStrength(keyStrength).keyEquivalence(Equivalence.identity()).makeMap();
332 
333     ConcurrentHashMultiset<String> multiset = ConcurrentHashMultiset.create(map);
334 
335     String s1 = new String("a");
336     String s2 = new String("a");
337     assertEquals(s1, s2); // Stating the obvious.
338     assertTrue(s1 != s2); // Stating the obvious.
339 
340     multiset.add(s1);
341     assertTrue(multiset.contains(s1));
342     assertFalse(multiset.contains(s2));
343     assertEquals(1, multiset.count(s1));
344     assertEquals(0, multiset.count(s2));
345 
346     multiset.add(s1);
347     multiset.add(s2, 3);
348     assertEquals(2, multiset.count(s1));
349     assertEquals(3, multiset.count(s2));
350 
351     multiset.remove(s1);
352     assertEquals(1, multiset.count(s1));
353     assertEquals(3, multiset.count(s2));
354   }
355 
356   public void testLogicalKeyEquality_strongKeys() {
357     testLogicalKeyEquality(STRONG);
358   }
359 
360   public void testLogicalKeyEquality_weakKeys() {
361     testLogicalKeyEquality(WEAK);
362   }
363 
364   private void testLogicalKeyEquality(MapMakerInternalMap.Strength keyStrength) {
365 
366     ConcurrentMap<String, AtomicInteger> map =
367         new MapMaker().setKeyStrength(keyStrength).keyEquivalence(Equivalence.equals()).makeMap();
368 
369     ConcurrentHashMultiset<String> multiset = ConcurrentHashMultiset.create(map);
370 
371     String s1 = new String("a");
372     String s2 = new String("a");
373     assertEquals(s1, s2); // Stating the obvious.
374 
375     multiset.add(s1);
376     assertTrue(multiset.contains(s1));
377     assertTrue(multiset.contains(s2));
378     assertEquals(1, multiset.count(s1));
379     assertEquals(1, multiset.count(s2));
380 
381     multiset.add(s2, 3);
382     assertEquals(4, multiset.count(s1));
383     assertEquals(4, multiset.count(s2));
384 
385     multiset.remove(s1);
386     assertEquals(3, multiset.count(s1));
387     assertEquals(3, multiset.count(s2));
388   }
389 
390   public void testSerializationWithMapMaker1() {
391     ConcurrentMap<String, AtomicInteger> map = new MapMaker().makeMap();
392     multiset = ConcurrentHashMultiset.create(map);
393     reserializeAndAssert(multiset);
394   }
395 
396   public void testSerializationWithMapMaker2() {
397     ConcurrentMap<String, AtomicInteger> map = new MapMaker().makeMap();
398     multiset = ConcurrentHashMultiset.create(map);
399     multiset.addAll(ImmutableList.of("a", "a", "b", "c", "d", "b"));
400     reserializeAndAssert(multiset);
401   }
402 
403   public void testSerializationWithMapMaker3() {
404     ConcurrentMap<String, AtomicInteger> map = new MapMaker().makeMap();
405     multiset = ConcurrentHashMultiset.create(map);
406     multiset.addAll(ImmutableList.of("a", "a", "b", "c", "d", "b"));
407     reserializeAndAssert(multiset);
408   }
409 
410   public void testSerializationWithMapMaker_preservesIdentityKeyEquivalence() {
411     ConcurrentMap<String, AtomicInteger> map =
412         new MapMaker().keyEquivalence(Equivalence.identity()).makeMap();
413 
414     ConcurrentHashMultiset<String> multiset = ConcurrentHashMultiset.create(map);
415     multiset = reserializeAndAssert(multiset);
416 
417     String s1 = new String("a");
418     String s2 = new String("a");
419     assertEquals(s1, s2); // Stating the obvious.
420     assertTrue(s1 != s2); // Stating the obvious.
421 
422     multiset.add(s1);
423     assertTrue(multiset.contains(s1));
424     assertFalse(multiset.contains(s2));
425     assertEquals(1, multiset.count(s1));
426     assertEquals(0, multiset.count(s2));
427   }
428 }
429