• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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