1 /*
2  * Copyright (C) 2011 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.util.concurrent.Uninterruptibles.awaitUninterruptibly;
20 import static java.util.concurrent.TimeUnit.HOURS;
21 
22 import com.google.common.annotations.GwtCompatible;
23 import com.google.common.annotations.GwtIncompatible;
24 import com.google.common.base.Function;
25 import com.google.common.collect.MapMaker.RemovalNotification;
26 import com.google.common.collect.MapMakerInternalMapTest.QueuingRemovalListener;
27 import com.google.common.testing.NullPointerTester;
28 
29 import junit.framework.TestCase;
30 
31 import java.util.Map;
32 import java.util.Set;
33 import java.util.concurrent.ConcurrentHashMap;
34 import java.util.concurrent.ConcurrentMap;
35 import java.util.concurrent.CountDownLatch;
36 import java.util.concurrent.ExecutorService;
37 import java.util.concurrent.Executors;
38 import java.util.concurrent.atomic.AtomicInteger;
39 
40 /**
41  * @author Charles Fry
42  */
43 @GwtCompatible(emulated = true)
44 public class MapMakerTest extends TestCase {
45 
46   @GwtIncompatible("NullPointerTester")
47   public void testNullParameters() throws Exception {
48     NullPointerTester tester = new NullPointerTester();
49     tester.testAllPublicInstanceMethods(new MapMaker());
50   }
51 
52   @GwtIncompatible("threads")
53 
54   public void testRemovalNotification_clear() throws InterruptedException {
55     // If a clear() happens while a computation is pending, we should not get a removal
56     // notification.
57 
58     final CountDownLatch computingLatch = new CountDownLatch(1);
59     Function<String, String> computingFunction = new DelayingIdentityLoader<String>(computingLatch);
60     QueuingRemovalListener<String, String> listener = new QueuingRemovalListener<String, String>();
61 
62     @SuppressWarnings("deprecation") // test of deprecated code
63     final ConcurrentMap<String, String> map = new MapMaker()
64         .concurrencyLevel(1)
65         .removalListener(listener)
66         .makeComputingMap(computingFunction);
67 
68     // seed the map, so its segment's count > 0
69     map.put("a", "a");
70 
71     final CountDownLatch computationStarted = new CountDownLatch(1);
72     final CountDownLatch computationComplete = new CountDownLatch(1);
73     new Thread(new Runnable() {
74       @Override public void run() {
75         computationStarted.countDown();
76         map.get("b");
77         computationComplete.countDown();
78       }
79     }).start();
80 
81     // wait for the computingEntry to be created
82     computationStarted.await();
83     map.clear();
84     // let the computation proceed
85     computingLatch.countDown();
86     // don't check map.size() until we know the get("b") call is complete
87     computationComplete.await();
88 
89     // At this point, the listener should be holding the seed value (a -> a), and the map should
90     // contain the computed value (b -> b), since the clear() happened before the computation
91     // completed.
92     assertEquals(1, listener.size());
93     RemovalNotification<String, String> notification = listener.remove();
94     assertEquals("a", notification.getKey());
95     assertEquals("a", notification.getValue());
96     assertEquals(1, map.size());
97     assertEquals("b", map.get("b"));
98   }
99 
100   // "Basher tests", where we throw a bunch of stuff at a Cache and check basic invariants.
101 
102   /**
103    * This is a less carefully-controlled version of {@link #testRemovalNotification_clear} - this is
104    * a black-box test that tries to create lots of different thread-interleavings, and asserts that
105    * each computation is affected by a call to {@code clear()} (and therefore gets passed to the
106    * removal listener), or else is not affected by the {@code clear()} (and therefore exists in the
107    * map afterward).
108    */
109   @GwtIncompatible("threads")
110 
111   public void testRemovalNotification_clear_basher() throws InterruptedException {
112     // If a clear() happens close to the end of computation, one of two things should happen:
113     // - computation ends first: the removal listener is called, and the map does not contain the
114     //   key/value pair
115     // - clear() happens first: the removal listener is not called, and the map contains the pair
116     CountDownLatch computationLatch = new CountDownLatch(1);
117     QueuingRemovalListener<String, String> listener = new QueuingRemovalListener<String, String>();
118 
119     @SuppressWarnings("deprecation") // test of deprecated code
120     final Map<String, String> map = new MapMaker()
121         .removalListener(listener)
122         .concurrencyLevel(20)
123         .makeComputingMap(new DelayingIdentityLoader<String>(computationLatch));
124 
125     int nThreads = 100;
126     int nTasks = 1000;
127     int nSeededEntries = 100;
128     Set<String> expectedKeys = Sets.newHashSetWithExpectedSize(nTasks + nSeededEntries);
129     // seed the map, so its segments have a count>0; otherwise, clear() won't visit the in-progress
130     // entries
131     for (int i = 0; i < nSeededEntries; i++) {
132       String s = "b" + i;
133       map.put(s, s);
134       expectedKeys.add(s);
135     }
136 
137     final AtomicInteger computedCount = new AtomicInteger();
138     ExecutorService threadPool = Executors.newFixedThreadPool(nThreads);
139     final CountDownLatch tasksFinished = new CountDownLatch(nTasks);
140     for (int i = 0; i < nTasks; i++) {
141       final String s = "a" + i;
142       threadPool.submit(new Runnable() {
143         @Override public void run() {
144           map.get(s);
145           computedCount.incrementAndGet();
146           tasksFinished.countDown();
147         }
148       });
149       expectedKeys.add(s);
150     }
151 
152     computationLatch.countDown();
153     // let some computations complete
154     while (computedCount.get() < nThreads) {
155       Thread.yield();
156     }
157     map.clear();
158     tasksFinished.await();
159 
160     // Check all of the removal notifications we received: they should have had correctly-associated
161     // keys and values. (An earlier bug saw removal notifications for in-progress computations,
162     // which had real keys with null values.)
163     Map<String, String> removalNotifications = Maps.newHashMap();
164     for (RemovalNotification<String, String> notification : listener) {
165       removalNotifications.put(notification.getKey(), notification.getValue());
166       assertEquals("Unexpected key/value pair passed to removalListener",
167           notification.getKey(), notification.getValue());
168     }
169 
170     // All of the seed values should have been visible, so we should have gotten removal
171     // notifications for all of them.
172     for (int i = 0; i < nSeededEntries; i++) {
173       assertEquals("b" + i, removalNotifications.get("b" + i));
174     }
175 
176     // Each of the values added to the map should either still be there, or have seen a removal
177     // notification.
178     assertEquals(expectedKeys, Sets.union(map.keySet(), removalNotifications.keySet()));
179     assertTrue(Sets.intersection(map.keySet(), removalNotifications.keySet()).isEmpty());
180   }
181 
182   @GwtIncompatible("threads")
183   static final class DelayingIdentityLoader<T> implements Function<T, T> {
184     private final CountDownLatch delayLatch;
185 
186     DelayingIdentityLoader(CountDownLatch delayLatch) {
187       this.delayLatch = delayLatch;
188     }
189 
190     @Override public T apply(T key) {
191       awaitUninterruptibly(delayLatch);
192       return key;
193     }
194   }
195 
196   /*
197    * TODO(cpovirk): eliminate duplication between these tests and those in LegacyMapMakerTests and
198    * anywhere else
199    */
200 
201   /** Tests for the builder. */
202   public static class MakerTest extends TestCase {
203     public void testInitialCapacity_negative() {
204       MapMaker maker = new MapMaker();
205       try {
206         maker.initialCapacity(-1);
207         fail();
208       } catch (IllegalArgumentException expected) {
209       }
210     }
211 
212     // TODO(cpovirk): enable when ready
213     public void xtestInitialCapacity_setTwice() {
214       MapMaker maker = new MapMaker().initialCapacity(16);
215       try {
216         // even to the same value is not allowed
217         maker.initialCapacity(16);
218         fail();
219       } catch (IllegalArgumentException expected) {
220       }
221     }
222 
223     @SuppressWarnings("deprecation") // test of deprecated method
224     public void testExpiration_setTwice() {
225       MapMaker maker = new MapMaker().expireAfterWrite(1, HOURS);
226       try {
227         // even to the same value is not allowed
228         maker.expireAfterWrite(1, HOURS);
229         fail();
230       } catch (IllegalStateException expected) {
231       }
232     }
233 
234     public void testMaximumSize_setTwice() {
235       MapMaker maker = new MapMaker().maximumSize(16);
236       try {
237         // even to the same value is not allowed
238         maker.maximumSize(16);
239         fail();
240       } catch (IllegalStateException expected) {
241       }
242     }
243 
244     public void testReturnsPlainConcurrentHashMapWhenPossible() {
245       Map<?, ?> map = new MapMaker()
246           .initialCapacity(5)
247           .makeMap();
248       assertTrue(map instanceof ConcurrentHashMap);
249     }
250   }
251 
252   /** Tests of the built map with maximumSize. */
253   public static class MaximumSizeTest extends TestCase {
254     public void testPut_sizeIsZero() {
255       ConcurrentMap<Object, Object> map =
256           new MapMaker().maximumSize(0).makeMap();
257       assertEquals(0, map.size());
258       map.put(new Object(), new Object());
259       assertEquals(0, map.size());
260     }
261 
262     public void testSizeBasedEviction() {
263       int numKeys = 10;
264       int mapSize = 5;
265       ConcurrentMap<Object, Object> map =
266           new MapMaker().maximumSize(mapSize).makeMap();
267       for (int i = 0; i < numKeys; i++) {
268         map.put(i, i);
269       }
270       assertEquals(mapSize, map.size());
271       for (int i = numKeys - mapSize; i < mapSize; i++) {
272         assertTrue(map.containsKey(i));
273       }
274     }
275   }
276 
277   /** Tests for recursive computation. */
278   public static class RecursiveComputationTest extends TestCase {
279     Function<Integer, String> recursiveComputer
280         = new Function<Integer, String>() {
281       @Override
282       public String apply(Integer key) {
283         if (key > 0) {
284           return key + ", " + recursiveMap.get(key - 1);
285         } else {
286           return "0";
287         }
288       }
289     };
290 
291     ConcurrentMap<Integer, String> recursiveMap = new MapMaker()
292         .makeComputingMap(recursiveComputer);
293 
294     public void testRecursiveComputation() {
295       assertEquals("3, 2, 1, 0", recursiveMap.get(3));
296     }
297   }
298 
299   /**
300    * Tests for computing functionality.
301    */
302   public static class ComputingTest extends TestCase {
303     public void testComputerThatReturnsNull() {
304       ConcurrentMap<Integer, String> map = new MapMaker()
305           .makeComputingMap(new Function<Integer, String>() {
306             @Override
307             public String apply(Integer key) {
308               return null;
309             }
310           });
311       try {
312         map.get(1);
313         fail();
314       } catch (NullPointerException e) { /* expected */ }
315     }
316 
317     public void testRuntimeException() {
318       final RuntimeException e = new RuntimeException();
319 
320       ConcurrentMap<Object, Object> map = new MapMaker().makeComputingMap(
321           new Function<Object, Object>() {
322         @Override
323         public Object apply(Object from) {
324           throw e;
325         }
326       });
327 
328       try {
329         map.get(new Object());
330         fail();
331       } catch (ComputationException ce) {
332         assertSame(e, ce.getCause());
333       }
334     }
335   }
336 }
337