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.easymock.EasyMock.eq; 24 import static org.easymock.EasyMock.expect; 25 import static org.easymock.EasyMock.isA; 26 27 import com.google.common.base.Equivalence; 28 import com.google.common.collect.MapMaker.RemovalListener; 29 import com.google.common.collect.MapMaker.RemovalNotification; 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 35 import junit.framework.Test; 36 import junit.framework.TestCase; 37 import junit.framework.TestSuite; 38 39 import org.easymock.EasyMock; 40 41 import java.util.Collections; 42 import java.util.Iterator; 43 import java.util.List; 44 import java.util.concurrent.ConcurrentMap; 45 import java.util.concurrent.ConcurrentSkipListMap; 46 import java.util.concurrent.TimeUnit; 47 import java.util.concurrent.atomic.AtomicInteger; 48 49 /** 50 * Test case for {@link ConcurrentHashMultiset}. 51 * 52 * @author Cliff L. Biffle 53 * @author mike nonemacher 54 */ 55 public class ConcurrentHashMultisetTest extends TestCase { 56 suite()57 public static Test suite() { 58 TestSuite suite = new TestSuite(); 59 suite.addTest(MultisetTestSuiteBuilder.using(concurrentHashMultisetGenerator()) 60 .withFeatures(CollectionSize.ANY, 61 CollectionFeature.GENERAL_PURPOSE, 62 CollectionFeature.SERIALIZABLE, 63 CollectionFeature.ALLOWS_NULL_QUERIES) 64 .named("ConcurrentHashMultiset") 65 .createTestSuite()); 66 suite.addTest(MultisetTestSuiteBuilder.using(concurrentSkipListMultisetGenerator()) 67 .withFeatures(CollectionSize.ANY, 68 CollectionFeature.KNOWN_ORDER, 69 CollectionFeature.GENERAL_PURPOSE, 70 CollectionFeature.SERIALIZABLE, 71 CollectionFeature.ALLOWS_NULL_QUERIES) 72 .named("ConcurrentSkipListMultiset") 73 .createTestSuite()); 74 suite.addTestSuite(ConcurrentHashMultisetTest.class); 75 return suite; 76 } 77 concurrentHashMultisetGenerator()78 private static TestStringMultisetGenerator concurrentHashMultisetGenerator() { 79 return new TestStringMultisetGenerator() { 80 @Override 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 protected Multiset<String> create(String[] elements) { 89 Multiset<String> multiset = new ConcurrentHashMultiset<String>( 90 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 protected void setUp() { 109 backingMap = EasyMock.createMock(ConcurrentMap.class); 110 expect(backingMap.isEmpty()).andReturn(true); 111 replay(); 112 113 multiset = new ConcurrentHashMultiset<String>(backingMap); 114 verify(); 115 reset(); 116 } 117 118 public void testCount_elementPresent() { 119 final int COUNT = 12; 120 expect(backingMap.get(KEY)).andReturn(new AtomicInteger(COUNT)); 121 replay(); 122 123 assertEquals(COUNT, multiset.count(KEY)); 124 verify(); 125 } 126 127 public void testCount_elementAbsent() { 128 expect(backingMap.get(KEY)).andReturn(null); 129 replay(); 130 131 assertEquals(0, multiset.count(KEY)); 132 verify(); 133 } 134 135 public void testAdd_zero() { 136 final int INITIAL_COUNT = 32; 137 138 expect(backingMap.get(KEY)).andReturn(new AtomicInteger(INITIAL_COUNT)); 139 replay(); 140 assertEquals(INITIAL_COUNT, multiset.add(KEY, 0)); 141 verify(); 142 } 143 144 public void testAdd_firstFewWithSuccess() { 145 final int COUNT = 400; 146 147 expect(backingMap.get(KEY)).andReturn(null); 148 expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(null); 149 replay(); 150 151 assertEquals(0, multiset.add(KEY, COUNT)); 152 verify(); 153 } 154 155 public void testAdd_laterFewWithSuccess() { 156 int INITIAL_COUNT = 32; 157 int COUNT_TO_ADD = 400; 158 159 AtomicInteger initial = new AtomicInteger(INITIAL_COUNT); 160 expect(backingMap.get(KEY)).andReturn(initial); 161 replay(); 162 163 assertEquals(INITIAL_COUNT, multiset.add(KEY, COUNT_TO_ADD)); 164 assertEquals(INITIAL_COUNT + COUNT_TO_ADD, initial.get()); 165 verify(); 166 } 167 168 public void testAdd_laterFewWithOverflow() { 169 final int INITIAL_COUNT = 92384930; 170 final int COUNT_TO_ADD = Integer.MAX_VALUE - INITIAL_COUNT + 1; 171 172 expect(backingMap.get(KEY)).andReturn(new AtomicInteger(INITIAL_COUNT)); 173 replay(); 174 175 try { 176 multiset.add(KEY, COUNT_TO_ADD); 177 fail("Must reject arguments that would cause counter overflow."); 178 } catch (IllegalArgumentException e) { 179 // Expected. 180 } 181 verify(); 182 } 183 184 /** 185 * Simulate some of the races that can happen on add. We can't easily simulate the race that 186 * happens when an {@link AtomicInteger#compareAndSet} fails, but we can simulate the case where 187 * the putIfAbsent returns a non-null value, and the case where the replace() of an observed 188 * zero fails. 189 */ 190 public void testAdd_withFailures() { 191 AtomicInteger existing = new AtomicInteger(12); 192 AtomicInteger existingZero = new AtomicInteger(0); 193 194 // initial map.get() 195 expect(backingMap.get(KEY)).andReturn(null); 196 // since get returned null, try a putIfAbsent; that fails due to a simulated race 197 expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(existingZero); 198 // since the putIfAbsent returned a zero, we'll try to replace... 199 expect(backingMap.replace(eq(KEY), eq(existingZero), isA(AtomicInteger.class))) 200 .andReturn(false); 201 // ...and then putIfAbsent. Simulate failure on both 202 expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(existing); 203 204 // next map.get() 205 expect(backingMap.get(KEY)).andReturn(existingZero); 206 // since get returned zero, try a replace; that fails due to a simulated race 207 expect(backingMap.replace(eq(KEY), eq(existingZero), isA(AtomicInteger.class))) 208 .andReturn(false); 209 expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(existing); 210 211 // another map.get() 212 expect(backingMap.get(KEY)).andReturn(existing); 213 // we shouldn't see any more map operations; CHM will now just update the AtomicInteger 214 215 replay(); 216 217 assertEquals(multiset.add(KEY, 3), 12); 218 assertEquals(15, existing.get()); 219 220 verify(); 221 } 222 223 public void testRemove_zeroFromSome() { 224 final int INITIAL_COUNT = 14; 225 expect(backingMap.get(KEY)).andReturn(new AtomicInteger(INITIAL_COUNT)); 226 replay(); 227 228 assertEquals(INITIAL_COUNT, multiset.remove(KEY, 0)); 229 verify(); 230 } 231 232 public void testRemove_zeroFromNone() { 233 expect(backingMap.get(KEY)).andReturn(null); 234 replay(); 235 236 assertEquals(0, multiset.remove(KEY, 0)); 237 verify(); 238 } 239 240 public void testRemove_nonePresent() { 241 expect(backingMap.get(KEY)).andReturn(null); 242 replay(); 243 244 assertEquals(0, multiset.remove(KEY, 400)); 245 verify(); 246 } 247 248 public void testRemove_someRemaining() { 249 int countToRemove = 30; 250 int countRemaining = 1; 251 AtomicInteger current = new AtomicInteger(countToRemove + countRemaining); 252 253 expect(backingMap.get(KEY)).andReturn(current); 254 replay(); 255 256 assertEquals(countToRemove + countRemaining, multiset.remove(KEY, countToRemove)); 257 assertEquals(countRemaining, current.get()); 258 verify(); 259 } 260 261 public void testRemove_noneRemaining() { 262 int countToRemove = 30; 263 AtomicInteger current = new AtomicInteger(countToRemove); 264 265 expect(backingMap.get(KEY)).andReturn(current); 266 // it's ok if removal fails: another thread may have done the remove 267 expect(backingMap.remove(KEY, current)).andReturn(false); 268 replay(); 269 270 assertEquals(countToRemove, multiset.remove(KEY, countToRemove)); 271 assertEquals(0, current.get()); 272 verify(); 273 } 274 275 public void testRemoveExactly() { 276 ConcurrentHashMultiset<String> cms = ConcurrentHashMultiset.create(); 277 cms.add("a", 2); 278 cms.add("b", 3); 279 280 try { 281 cms.removeExactly("a", -2); 282 } catch (IllegalArgumentException expected) {} 283 284 assertTrue(cms.removeExactly("a", 0)); 285 assertEquals(2, cms.count("a")); 286 assertTrue(cms.removeExactly("c", 0)); 287 assertEquals(0, cms.count("c")); 288 289 assertFalse(cms.removeExactly("a", 4)); 290 assertEquals(2, cms.count("a")); 291 assertTrue(cms.removeExactly("a", 2)); 292 assertEquals(0, cms.count("a")); 293 assertTrue(cms.removeExactly("b", 2)); 294 assertEquals(1, cms.count("b")); 295 } 296 297 public void testIteratorRemove_actualMap() { 298 // Override to avoid using mocks. 299 multiset = ConcurrentHashMultiset.create(); 300 301 multiset.add(KEY); 302 multiset.add(KEY + "_2"); 303 multiset.add(KEY); 304 305 int mutations = 0; 306 for (Iterator<String> it = multiset.iterator(); it.hasNext(); ) { 307 it.next(); 308 it.remove(); 309 mutations++; 310 } 311 assertTrue(multiset.isEmpty()); 312 assertEquals(3, mutations); 313 } 314 315 public void testSetCount_basic() { 316 int initialCount = 20; 317 int countToSet = 40; 318 AtomicInteger current = new AtomicInteger(initialCount); 319 320 expect(backingMap.get(KEY)).andReturn(current); 321 replay(); 322 323 assertEquals(initialCount, multiset.setCount(KEY, countToSet)); 324 assertEquals(countToSet, current.get()); 325 verify(); 326 } 327 328 public void testSetCount_asRemove() { 329 int countToRemove = 40; 330 AtomicInteger current = new AtomicInteger(countToRemove); 331 332 expect(backingMap.get(KEY)).andReturn(current); 333 expect(backingMap.remove(KEY, current)).andReturn(true); 334 replay(); 335 336 assertEquals(countToRemove, multiset.setCount(KEY, 0)); 337 assertEquals(0, current.get()); 338 verify(); 339 } 340 341 public void testSetCount_0_nonePresent() { 342 expect(backingMap.get(KEY)).andReturn(null); 343 replay(); 344 345 assertEquals(0, multiset.setCount(KEY, 0)); 346 verify(); 347 } 348 349 public void testCreate() { 350 ConcurrentHashMultiset<Integer> multiset = ConcurrentHashMultiset.create(); 351 assertTrue(multiset.isEmpty()); 352 reserializeAndAssert(multiset); 353 } 354 355 public void testCreateFromIterable() { 356 Iterable<Integer> iterable = asList(1, 2, 2, 3, 4); 357 ConcurrentHashMultiset<Integer> multiset 358 = ConcurrentHashMultiset.create(iterable); 359 assertEquals(2, multiset.count(2)); 360 reserializeAndAssert(multiset); 361 } 362 363 public void testIdentityKeyEquality_strongKeys() { 364 testIdentityKeyEquality(STRONG); 365 } 366 367 public void testIdentityKeyEquality_weakKeys() { 368 testIdentityKeyEquality(WEAK); 369 } 370 371 private void testIdentityKeyEquality( 372 MapMakerInternalMap.Strength keyStrength) { 373 374 MapMaker mapMaker = new MapMaker() 375 .setKeyStrength(keyStrength) 376 .keyEquivalence(Equivalence.identity()); 377 378 ConcurrentHashMultiset<String> multiset = 379 ConcurrentHashMultiset.create(mapMaker); 380 381 String s1 = new String("a"); 382 String s2 = new String("a"); 383 assertEquals(s1, s2); // Stating the obvious. 384 assertTrue(s1 != s2); // Stating the obvious. 385 386 multiset.add(s1); 387 assertTrue(multiset.contains(s1)); 388 assertFalse(multiset.contains(s2)); 389 assertEquals(1, multiset.count(s1)); 390 assertEquals(0, multiset.count(s2)); 391 392 multiset.add(s1); 393 multiset.add(s2, 3); 394 assertEquals(2, multiset.count(s1)); 395 assertEquals(3, multiset.count(s2)); 396 397 multiset.remove(s1); 398 assertEquals(1, multiset.count(s1)); 399 assertEquals(3, multiset.count(s2)); 400 } 401 402 public void testLogicalKeyEquality_strongKeys() { 403 testLogicalKeyEquality(STRONG); 404 } 405 406 public void testLogicalKeyEquality_weakKeys() { 407 testLogicalKeyEquality(WEAK); 408 } 409 410 private void testLogicalKeyEquality( 411 MapMakerInternalMap.Strength keyStrength) { 412 413 MapMaker mapMaker = new MapMaker() 414 .setKeyStrength(keyStrength) 415 .keyEquivalence(Equivalence.equals()); 416 417 ConcurrentHashMultiset<String> multiset = 418 ConcurrentHashMultiset.create(mapMaker); 419 420 String s1 = new String("a"); 421 String s2 = new String("a"); 422 assertEquals(s1, s2); // Stating the obvious. 423 424 multiset.add(s1); 425 assertTrue(multiset.contains(s1)); 426 assertTrue(multiset.contains(s2)); 427 assertEquals(1, multiset.count(s1)); 428 assertEquals(1, multiset.count(s2)); 429 430 multiset.add(s2, 3); 431 assertEquals(4, multiset.count(s1)); 432 assertEquals(4, multiset.count(s2)); 433 434 multiset.remove(s1); 435 assertEquals(3, multiset.count(s1)); 436 assertEquals(3, multiset.count(s2)); 437 } 438 439 public void testSerializationWithMapMaker1() { 440 MapMaker mapMaker = new MapMaker(); 441 multiset = ConcurrentHashMultiset.create(mapMaker); 442 reserializeAndAssert(multiset); 443 } 444 445 public void testSerializationWithMapMaker2() { 446 MapMaker mapMaker = new MapMaker(); 447 multiset = ConcurrentHashMultiset.create(mapMaker); 448 multiset.addAll(ImmutableList.of("a", "a", "b", "c", "d", "b")); 449 reserializeAndAssert(multiset); 450 } 451 452 public void testSerializationWithMapMaker3() { 453 MapMaker mapMaker = new MapMaker().expireAfterWrite(1, TimeUnit.SECONDS); 454 multiset = ConcurrentHashMultiset.create(mapMaker); 455 multiset.addAll(ImmutableList.of("a", "a", "b", "c", "d", "b")); 456 reserializeAndAssert(multiset); 457 } 458 459 public void testSerializationWithMapMaker_preservesIdentityKeyEquivalence() { 460 MapMaker mapMaker = new MapMaker() 461 .keyEquivalence(Equivalence.identity()); 462 463 ConcurrentHashMultiset<String> multiset = 464 ConcurrentHashMultiset.create(mapMaker); 465 multiset = reserializeAndAssert(multiset); 466 467 String s1 = new String("a"); 468 String s2 = new String("a"); 469 assertEquals(s1, s2); // Stating the obvious. 470 assertTrue(s1 != s2); // Stating the obvious. 471 472 multiset.add(s1); 473 assertTrue(multiset.contains(s1)); 474 assertFalse(multiset.contains(s2)); 475 assertEquals(1, multiset.count(s1)); 476 assertEquals(0, multiset.count(s2)); 477 } 478 479 // @Suppress(owner = "bmanes", detail = "Does not call the eviction listener") 480 // public void testWithMapMakerEvictionListener_BROKEN1() 481 // throws InterruptedException { 482 // MapEvictionListener<String, Number> evictionListener = 483 // mockEvictionListener(); 484 // evictionListener.onEviction("a", 5); 485 // EasyMock.replay(evictionListener); 486 // 487 // GenericMapMaker<String, Number> mapMaker = new MapMaker() 488 // .expireAfterWrite(100, TimeUnit.MILLISECONDS) 489 // .evictionListener(evictionListener); 490 // 491 // ConcurrentHashMultiset<String> multiset = 492 // ConcurrentHashMultiset.create(mapMaker); 493 // 494 // multiset.add("a", 5); 495 // 496 // assertTrue(multiset.contains("a")); 497 // assertEquals(5, multiset.count("a")); 498 // 499 // Thread.sleep(2000); 500 // 501 // EasyMock.verify(evictionListener); 502 // } 503 504 // @Suppress(owner = "bmanes", detail = "Does not call the eviction listener") 505 // public void testWithMapMakerEvictionListener_BROKEN2() 506 // throws InterruptedException { 507 // MapEvictionListener<String, Number> evictionListener = 508 // mockEvictionListener(); 509 // evictionListener.onEviction("a", 5); 510 // EasyMock.replay(evictionListener); 511 // 512 // GenericMapMaker<String, Number> mapMaker = new MapMaker() 513 // .expireAfterWrite(100, TimeUnit.MILLISECONDS) 514 // .evictionListener(evictionListener); 515 // 516 // ConcurrentHashMultiset<String> multiset = 517 // ConcurrentHashMultiset.create(mapMaker); 518 // 519 // multiset.add("a", 5); 520 // 521 // assertTrue(multiset.contains("a")); 522 // assertEquals(5, multiset.count("a")); 523 // 524 // Thread.sleep(2000); 525 // 526 // // This call should have the side-effect of calling the 527 // // eviction listener, but it does not. 528 // assertFalse(multiset.contains("a")); 529 // 530 // EasyMock.verify(evictionListener); 531 // } 532 533 public void testWithMapMakerEvictionListener() { 534 final List<RemovalNotification<String, Number>> notificationQueue = Lists.newArrayList(); 535 RemovalListener<String, Number> removalListener = 536 new RemovalListener<String, Number>() { 537 @Override public void onRemoval(RemovalNotification<String, Number> notification) { 538 notificationQueue.add(notification); 539 } 540 }; 541 542 @SuppressWarnings("deprecation") // TODO(kevinb): what to do? 543 MapMaker mapMaker = new MapMaker() 544 .concurrencyLevel(1) 545 .maximumSize(1); 546 /* 547 * Cleverly ignore the return type now that ConcurrentHashMultiset accepts only MapMaker and not 548 * the deprecated GenericMapMaker. We know that a RemovalListener<String, Number> is a type that 549 * will work with ConcurrentHashMultiset. 550 */ 551 mapMaker.removalListener(removalListener); 552 553 ConcurrentHashMultiset<String> multiset = ConcurrentHashMultiset.create(mapMaker); 554 555 multiset.add("a", 5); 556 assertTrue(multiset.contains("a")); 557 assertEquals(5, multiset.count("a")); 558 559 multiset.add("b", 3); 560 561 assertFalse(multiset.contains("a")); 562 assertTrue(multiset.contains("b")); 563 assertEquals(3, multiset.count("b")); 564 RemovalNotification<String, Number> notification = Iterables.getOnlyElement(notificationQueue); 565 assertEquals("a", notification.getKey()); 566 // The map evicted this entry, so CHM didn't have a chance to zero it. 567 assertEquals(5, notification.getValue().intValue()); 568 } 569 570 private void replay() { 571 EasyMock.replay(backingMap); 572 } 573 574 private void verify() { 575 EasyMock.verify(backingMap); 576 } 577 578 private void reset() { 579 EasyMock.reset(backingMap); 580 } 581 } 582