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