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 java.util.function.Predicate; 46 import junit.framework.Test; 47 import junit.framework.TestCase; 48 import junit.framework.TestSuite; 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 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(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 String remove(Object key) { 153 return map.remove(key); 154 } 155 156 @Override 157 public boolean remove(Object key, Object value) { 158 return map.remove(key, value); 159 } 160 161 class EntrySet extends AbstractSet<Map.Entry<String, String>> { 162 @Override 163 public Iterator<Entry<String, String>> iterator() { 164 return new Iterator<Entry<String, String>>() { 165 166 final Iterator<Entry<String, String>> iterator = map.entrySet().iterator(); 167 168 @Override 169 public void remove() { 170 iterator.remove(); 171 } 172 173 @Override 174 public boolean hasNext() { 175 return iterator.hasNext(); 176 } 177 178 @Override 179 public Entry<String, String> next() { 180 return transform(iterator.next()); 181 } 182 183 private Entry<String, String> transform(final Entry<String, String> next) { 184 return new Entry<String, String>() { 185 186 @Override 187 public String setValue(String value) { 188 checkNotNull(value); 189 return next.setValue(value); 190 } 191 192 @Override 193 public String getValue() { 194 return next.getValue(); 195 } 196 197 @Override 198 public String getKey() { 199 return next.getKey(); 200 } 201 202 @Override 203 public boolean equals(Object obj) { 204 return next.equals(obj); 205 } 206 207 @Override 208 public int hashCode() { 209 return next.hashCode(); 210 } 211 }; 212 } 213 }; 214 } 215 216 @Override 217 public int size() { 218 return map.size(); 219 } 220 221 @Override 222 public boolean remove(Object o) { 223 return map.entrySet().remove(o); 224 } 225 226 @Override 227 public boolean removeIf(Predicate<? super Entry<String, String>> filter) { 228 return map.entrySet().removeIf(filter); 229 } 230 231 @Override 232 public boolean containsAll(Collection<?> c) { 233 return map.entrySet().containsAll(c); 234 } 235 236 @Override 237 public boolean removeAll(Collection<?> c) { 238 return map.entrySet().removeAll(c); 239 } 240 241 @Override 242 public boolean retainAll(Collection<?> c) { 243 return map.entrySet().retainAll(c); 244 } 245 246 @Override 247 public int hashCode() { 248 return map.entrySet().hashCode(); 249 } 250 251 @Override 252 public boolean equals(Object o) { 253 return map.entrySet().equals(o); 254 } 255 256 @Override 257 public String toString() { 258 return map.entrySet().toString(); 259 } 260 } 261 262 @Override 263 public String put(String key, String value) { 264 checkNotNull(value); 265 return map.put(key, value); 266 } 267 }; 268 } 269 }, 270 "HashMap w/out null values", 271 ALLOWS_NULL_KEYS); 272 } 273 274 /** 275 * Map generator that verifies that {@code setUp()} methods are called in all the test cases. The 276 * {@code setUpRan} parameter is set true by the {@code setUp} that every test case is supposed to 277 * have registered, and set false by the {@code tearDown}. We use a dynamic proxy to intercept all 278 * of the {@code Map} method calls and check that {@code setUpRan} is true. 279 */ 280 private static class CheckSetUpHashMapGenerator extends WrappedHashMapGenerator { 281 private final AtomicBoolean setUpRan; 282 283 CheckSetUpHashMapGenerator(AtomicBoolean setUpRan) { 284 this.setUpRan = setUpRan; 285 } 286 287 @Override 288 Map<String, String> wrap(HashMap<String, String> map) { 289 @SuppressWarnings("unchecked") 290 Map<String, String> proxy = 291 Reflection.newProxy(Map.class, new CheckSetUpInvocationHandler(map, setUpRan)); 292 return proxy; 293 } 294 } 295 296 /** 297 * Intercepts calls to a {@code Map} to check that {@code setUpRan} is true when they happen. Then 298 * forwards the calls to the underlying {@code Map}. 299 */ 300 private static class CheckSetUpInvocationHandler implements InvocationHandler, Serializable { 301 private final Map<String, String> map; 302 private final AtomicBoolean setUpRan; 303 304 CheckSetUpInvocationHandler(Map<String, String> map, AtomicBoolean setUpRan) { 305 this.map = map; 306 this.setUpRan = setUpRan; 307 } 308 309 @Override 310 public Object invoke(Object target, Method method, Object[] args) throws Throwable { 311 assertTrue("setUp should have run", setUpRan.get()); 312 try { 313 return method.invoke(map, args); 314 } catch (InvocationTargetException e) { 315 throw e.getCause(); 316 } catch (IllegalAccessException e) { 317 throw newLinkageError(e); 318 } 319 } 320 } 321 322 /** Verifies that {@code setUp} and {@code tearDown} are called in all map test cases. */ 323 private static Test testsForSetUpTearDown() { 324 final AtomicBoolean setUpRan = new AtomicBoolean(); 325 Runnable setUp = 326 new Runnable() { 327 @Override 328 public void run() { 329 assertFalse("previous tearDown should have run before setUp", setUpRan.getAndSet(true)); 330 } 331 }; 332 Runnable tearDown = 333 new Runnable() { 334 @Override 335 public void run() { 336 assertTrue("setUp should have run", setUpRan.getAndSet(false)); 337 } 338 }; 339 return MapTestSuiteBuilder.using(new CheckSetUpHashMapGenerator(setUpRan)) 340 .named("setUpTearDown") 341 .withFeatures( 342 MapFeature.GENERAL_PURPOSE, 343 MapFeature.ALLOWS_NULL_KEYS, 344 MapFeature.ALLOWS_NULL_VALUES, 345 CollectionFeature.SERIALIZABLE, 346 CollectionFeature.SUPPORTS_ITERATOR_REMOVE, 347 CollectionSize.ANY) 348 .withSetUp(setUp) 349 .withTearDown(tearDown) 350 .createTestSuite(); 351 } 352 353 private static LinkageError newLinkageError(Throwable cause) { 354 LinkageError error = new LinkageError(cause.toString()); 355 error.initCause(cause); 356 return error; 357 } 358 } 359