1 /* 2 * Copyright (C) 2008 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.testing; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 import static com.google.common.collect.testing.features.MapFeature.ALLOWS_NULL_KEYS; 21 import static com.google.common.collect.testing.features.MapFeature.ALLOWS_NULL_VALUES; 22 23 import com.google.common.collect.Lists; 24 import com.google.common.collect.Maps; 25 import com.google.common.collect.testing.features.CollectionFeature; 26 import com.google.common.collect.testing.features.CollectionSize; 27 import com.google.common.collect.testing.features.Feature; 28 import com.google.common.collect.testing.features.MapFeature; 29 import com.google.common.reflect.Reflection; 30 import java.io.Serializable; 31 import java.lang.reflect.InvocationHandler; 32 import java.lang.reflect.InvocationTargetException; 33 import java.lang.reflect.Method; 34 import java.util.AbstractMap; 35 import java.util.AbstractSet; 36 import java.util.Collection; 37 import java.util.Collections; 38 import java.util.HashMap; 39 import java.util.Iterator; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Map.Entry; 43 import java.util.Set; 44 import java.util.concurrent.atomic.AtomicBoolean; 45 import junit.framework.Test; 46 import junit.framework.TestCase; 47 import junit.framework.TestSuite; 48 49 /** 50 * Tests {@link MapTestSuiteBuilder} by using it against maps that have various negative behaviors. 51 * 52 * @author George van den Driessche 53 */ 54 public final class MapTestSuiteBuilderTests extends TestCase { MapTestSuiteBuilderTests()55 private MapTestSuiteBuilderTests() {} 56 suite()57 public static Test suite() { 58 TestSuite suite = new TestSuite(MapTestSuiteBuilderTests.class.getSimpleName()); 59 suite.addTest(testsForHashMapNullKeysForbidden()); 60 suite.addTest(testsForHashMapNullValuesForbidden()); 61 suite.addTest(testsForSetUpTearDown()); 62 return suite; 63 } 64 65 private abstract static class WrappedHashMapGenerator extends TestStringMapGenerator { 66 @Override create(Entry<String, String>[] entries)67 protected final Map<String, String> create(Entry<String, String>[] entries) { 68 HashMap<String, String> map = Maps.newHashMap(); 69 for (Entry<String, String> entry : entries) { 70 map.put(entry.getKey(), entry.getValue()); 71 } 72 return wrap(map); 73 } 74 wrap(HashMap<String, String> map)75 abstract Map<String, String> wrap(HashMap<String, String> map); 76 } 77 wrappedHashMapTests( WrappedHashMapGenerator generator, String name, Feature<?>... features)78 private static TestSuite wrappedHashMapTests( 79 WrappedHashMapGenerator generator, String name, Feature<?>... features) { 80 List<Feature<?>> featuresList = Lists.newArrayList(features); 81 Collections.addAll( 82 featuresList, 83 MapFeature.GENERAL_PURPOSE, 84 CollectionFeature.SUPPORTS_ITERATOR_REMOVE, 85 CollectionSize.ANY); 86 return MapTestSuiteBuilder.using(generator) 87 .named(name) 88 .withFeatures(featuresList) 89 .createTestSuite(); 90 } 91 92 // TODO: consider being null-hostile in these tests 93 testsForHashMapNullKeysForbidden()94 private static Test testsForHashMapNullKeysForbidden() { 95 return wrappedHashMapTests( 96 new WrappedHashMapGenerator() { 97 @Override 98 Map<String, String> wrap(final HashMap<String, String> map) { 99 if (map.containsKey(null)) { 100 throw new NullPointerException(); 101 } 102 return new AbstractMap<String, String>() { 103 @Override 104 public Set<Entry<String, String>> entrySet() { 105 return map.entrySet(); 106 } 107 108 @Override 109 public String put(String key, String value) { 110 checkNotNull(key); 111 return map.put(key, value); 112 } 113 }; 114 } 115 }, 116 "HashMap w/out null keys", 117 ALLOWS_NULL_VALUES); 118 } 119 120 private static Test testsForHashMapNullValuesForbidden() { 121 return wrappedHashMapTests( 122 new WrappedHashMapGenerator() { 123 @Override 124 Map<String, String> wrap(final HashMap<String, String> map) { 125 if (map.containsValue(null)) { 126 throw new NullPointerException(); 127 } 128 129 return new AbstractMap<String, String>() { 130 @Override 131 public Set<Entry<String, String>> entrySet() { 132 return new EntrySet(); 133 } 134 135 @Override 136 public int hashCode() { 137 return map.hashCode(); 138 } 139 140 @Override 141 public boolean equals(Object o) { 142 return map.equals(o); 143 } 144 145 @Override 146 public String toString() { 147 return map.toString(); 148 } 149 150 @Override 151 public String remove(Object key) { 152 return map.remove(key); 153 } 154 155 class EntrySet extends AbstractSet<Map.Entry<String, String>> { 156 @Override 157 public Iterator<Entry<String, String>> iterator() { 158 return new Iterator<Entry<String, String>>() { 159 160 final Iterator<Entry<String, String>> iterator = map.entrySet().iterator(); 161 162 @Override 163 public void remove() { 164 iterator.remove(); 165 } 166 167 @Override 168 public boolean hasNext() { 169 return iterator.hasNext(); 170 } 171 172 @Override 173 public Entry<String, String> next() { 174 return transform(iterator.next()); 175 } 176 177 private Entry<String, String> transform(final Entry<String, String> next) { 178 return new Entry<String, String>() { 179 180 @Override 181 public String setValue(String value) { 182 checkNotNull(value); 183 return next.setValue(value); 184 } 185 186 @Override 187 public String getValue() { 188 return next.getValue(); 189 } 190 191 @Override 192 public String getKey() { 193 return next.getKey(); 194 } 195 196 @Override 197 public boolean equals(Object obj) { 198 return next.equals(obj); 199 } 200 201 @Override 202 public int hashCode() { 203 return next.hashCode(); 204 } 205 }; 206 } 207 }; 208 } 209 210 @Override 211 public int size() { 212 return map.size(); 213 } 214 215 @Override 216 public boolean remove(Object o) { 217 return map.entrySet().remove(o); 218 } 219 220 @Override 221 public boolean containsAll(Collection<?> c) { 222 return map.entrySet().containsAll(c); 223 } 224 225 @Override 226 public boolean removeAll(Collection<?> c) { 227 return map.entrySet().removeAll(c); 228 } 229 230 @Override 231 public boolean retainAll(Collection<?> c) { 232 return map.entrySet().retainAll(c); 233 } 234 235 @Override 236 public int hashCode() { 237 return map.entrySet().hashCode(); 238 } 239 240 @Override 241 public boolean equals(Object o) { 242 return map.entrySet().equals(o); 243 } 244 245 @Override 246 public String toString() { 247 return map.entrySet().toString(); 248 } 249 } 250 251 @Override 252 public String put(String key, String value) { 253 checkNotNull(value); 254 return map.put(key, value); 255 } 256 }; 257 } 258 }, 259 "HashMap w/out null values", 260 ALLOWS_NULL_KEYS); 261 } 262 263 /** 264 * Map generator that verifies that {@code setUp()} methods are called in all the test cases. The 265 * {@code setUpRan} parameter is set true by the {@code setUp} that every test case is supposed to 266 * have registered, and set false by the {@code tearDown}. We use a dynamic proxy to intercept all 267 * of the {@code Map} method calls and check that {@code setUpRan} is true. 268 */ 269 private static class CheckSetUpHashMapGenerator extends WrappedHashMapGenerator { 270 private final AtomicBoolean setUpRan; 271 272 CheckSetUpHashMapGenerator(AtomicBoolean setUpRan) { 273 this.setUpRan = setUpRan; 274 } 275 276 @Override 277 Map<String, String> wrap(HashMap<String, String> map) { 278 @SuppressWarnings("unchecked") 279 Map<String, String> proxy = 280 Reflection.newProxy(Map.class, new CheckSetUpInvocationHandler(map, setUpRan)); 281 return proxy; 282 } 283 } 284 285 /** 286 * Intercepts calls to a {@code Map} to check that {@code setUpRan} is true when they happen. Then 287 * forwards the calls to the underlying {@code Map}. 288 */ 289 private static class CheckSetUpInvocationHandler implements InvocationHandler, Serializable { 290 private final Map<String, String> map; 291 private final AtomicBoolean setUpRan; 292 293 CheckSetUpInvocationHandler(Map<String, String> map, AtomicBoolean setUpRan) { 294 this.map = map; 295 this.setUpRan = setUpRan; 296 } 297 298 @Override 299 public Object invoke(Object target, Method method, Object[] args) throws Throwable { 300 assertTrue("setUp should have run", setUpRan.get()); 301 try { 302 return method.invoke(map, args); 303 } catch (InvocationTargetException e) { 304 throw e.getCause(); 305 } catch (IllegalAccessException e) { 306 throw newLinkageError(e); 307 } 308 } 309 } 310 311 /** Verifies that {@code setUp} and {@code tearDown} are called in all map test cases. */ 312 private static Test testsForSetUpTearDown() { 313 final AtomicBoolean setUpRan = new AtomicBoolean(); 314 Runnable setUp = 315 new Runnable() { 316 @Override 317 public void run() { 318 assertFalse("previous tearDown should have run before setUp", setUpRan.getAndSet(true)); 319 } 320 }; 321 Runnable tearDown = 322 new Runnable() { 323 @Override 324 public void run() { 325 assertTrue("setUp should have run", setUpRan.getAndSet(false)); 326 } 327 }; 328 return MapTestSuiteBuilder.using(new CheckSetUpHashMapGenerator(setUpRan)) 329 .named("setUpTearDown") 330 .withFeatures( 331 MapFeature.GENERAL_PURPOSE, 332 MapFeature.ALLOWS_NULL_KEYS, 333 MapFeature.ALLOWS_NULL_VALUES, 334 CollectionFeature.SERIALIZABLE, 335 CollectionFeature.SUPPORTS_ITERATOR_REMOVE, 336 CollectionSize.ANY) 337 .withSetUp(setUp) 338 .withTearDown(tearDown) 339 .createTestSuite(); 340 } 341 342 private static LinkageError newLinkageError(Throwable cause) { 343 LinkageError error = new LinkageError(cause.toString()); 344 error.initCause(cause); 345 return error; 346 } 347 } 348