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