Home | History | Annotate | Download | only in collect
      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