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