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") testNullParameters()47 public void testNullParameters() throws Exception { 48 NullPointerTester tester = new NullPointerTester(); 49 tester.testAllPublicInstanceMethods(new MapMaker()); 50 } 51 52 @GwtIncompatible("threads") 53 testRemovalNotification_clear()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 testRemovalNotification_clear_basher()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 DelayingIdentityLoader(CountDownLatch delayLatch)186 DelayingIdentityLoader(CountDownLatch delayLatch) { 187 this.delayLatch = delayLatch; 188 } 189 apply(T key)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 { testInitialCapacity_negative()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 xtestInitialCapacity_setTwice()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 testExpiration_setTwice()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 testMaximumSize_setTwice()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 testReturnsPlainConcurrentHashMapWhenPossible()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 { testPut_sizeIsZero()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 testSizeBasedEviction()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 testRecursiveComputation()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 { testComputerThatReturnsNull()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 testRuntimeException()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