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