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