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