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