/* * Copyright (C) 2011 The Guava Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.common.collect; import static com.google.common.util.concurrent.Uninterruptibles.awaitUninterruptibly; import com.google.common.base.Function; import com.google.common.collect.MapMaker.RemovalNotification; import com.google.common.collect.MapMakerInternalMapTest.QueuingRemovalListener; import com.google.common.testing.NullPointerTester; import junit.framework.TestCase; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; /** * @author Charles Fry */ public class MapMakerTest extends TestCase { public void testNullParameters() throws Exception { NullPointerTester tester = new NullPointerTester(); tester.testAllPublicInstanceMethods(new MapMaker()); } public void testRemovalNotification_clear() throws InterruptedException { // If a clear() happens while a computation is pending, we should not get a removal // notification. final CountDownLatch computingLatch = new CountDownLatch(1); Function<String, String> computingFunction = new DelayingIdentityLoader(computingLatch); QueuingRemovalListener<String, String> listener = new QueuingRemovalListener<String, String>(); @SuppressWarnings("deprecation") // test of deprecated code final ConcurrentMap<String, String> map = new MapMaker() .concurrencyLevel(1) .removalListener(listener) .makeComputingMap(computingFunction); // seed the map, so its segment's count > 0 map.put("a", "a"); final CountDownLatch computationStarted = new CountDownLatch(1); final CountDownLatch computationComplete = new CountDownLatch(1); new Thread(new Runnable() { @Override public void run() { computationStarted.countDown(); map.get("b"); computationComplete.countDown(); } }).start(); // wait for the computingEntry to be created computationStarted.await(); map.clear(); // let the computation proceed computingLatch.countDown(); // don't check map.size() until we know the get("b") call is complete computationComplete.await(); // At this point, the listener should be holding the seed value (a -> a), and the map should // contain the computed value (b -> b), since the clear() happened before the computation // completed. assertEquals(1, listener.size()); RemovalNotification<String, String> notification = listener.remove(); assertEquals("a", notification.getKey()); assertEquals("a", notification.getValue()); assertEquals(1, map.size()); assertEquals("b", map.get("b")); } // "Basher tests", where we throw a bunch of stuff at a Cache and check basic invariants. /** * This is a less carefully-controlled version of {@link #testRemovalNotification_clear} - this is * a black-box test that tries to create lots of different thread-interleavings, and asserts that * each computation is affected by a call to {@code clear()} (and therefore gets passed to the * removal listener), or else is not affected by the {@code clear()} (and therefore exists in the * map afterward). */ public void testRemovalNotification_clear_basher() throws InterruptedException { // If a clear() happens close to the end of computation, one of two things should happen: // - computation ends first: the removal listener is called, and the map does not contain the // key/value pair // - clear() happens first: the removal listener is not called, and the map contains the pair CountDownLatch computationLatch = new CountDownLatch(1); QueuingRemovalListener<String, String> listener = new QueuingRemovalListener<String, String>(); @SuppressWarnings("deprecation") // test of deprecated code final Map<String, String> map = new MapMaker() .removalListener(listener) .concurrencyLevel(20) .makeComputingMap(new DelayingIdentityLoader<String>(computationLatch)); int nThreads = 100; int nTasks = 1000; int nSeededEntries = 100; Set<String> expectedKeys = Sets.newHashSetWithExpectedSize(nTasks + nSeededEntries); // seed the map, so its segments have a count>0; otherwise, clear() won't visit the in-progress // entries for (int i = 0; i < nSeededEntries; i++) { String s = "b" + i; map.put(s, s); expectedKeys.add(s); } final AtomicInteger computedCount = new AtomicInteger(); ExecutorService threadPool = Executors.newFixedThreadPool(nThreads); final CountDownLatch tasksFinished = new CountDownLatch(nTasks); for (int i = 0; i < nTasks; i++) { final String s = "a" + i; threadPool.submit(new Runnable() { @Override public void run() { map.get(s); computedCount.incrementAndGet(); tasksFinished.countDown(); } }); expectedKeys.add(s); } computationLatch.countDown(); // let some computations complete while (computedCount.get() < nThreads) { Thread.yield(); } map.clear(); tasksFinished.await(); // Check all of the removal notifications we received: they should have had correctly-associated // keys and values. (An earlier bug saw removal notifications for in-progress computations, // which had real keys with null values.) Map<String, String> removalNotifications = Maps.newHashMap(); for (RemovalNotification<String, String> notification : listener) { removalNotifications.put(notification.getKey(), notification.getValue()); assertEquals("Unexpected key/value pair passed to removalListener", notification.getKey(), notification.getValue()); } // All of the seed values should have been visible, so we should have gotten removal // notifications for all of them. for (int i = 0; i < nSeededEntries; i++) { assertEquals("b" + i, removalNotifications.get("b" + i)); } // Each of the values added to the map should either still be there, or have seen a removal // notification. assertEquals(expectedKeys, Sets.union(map.keySet(), removalNotifications.keySet())); assertTrue(Sets.intersection(map.keySet(), removalNotifications.keySet()).isEmpty()); } static final class DelayingIdentityLoader<T> implements Function<T, T> { private final CountDownLatch delayLatch; DelayingIdentityLoader(CountDownLatch delayLatch) { this.delayLatch = delayLatch; } @Override public T apply(T key) { awaitUninterruptibly(delayLatch); return key; } } }