1/*
2 * Copyright (C) 2005 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
17package com.google.common.testing;
18
19import com.google.common.annotations.Beta;
20import com.google.common.base.Function;
21import com.google.common.base.Functions;
22import com.google.common.base.Predicate;
23import com.google.common.base.Predicates;
24import com.google.common.base.Supplier;
25import com.google.common.base.Suppliers;
26import com.google.common.collect.Iterators;
27import com.google.common.collect.Lists;
28import com.google.common.collect.Maps;
29
30import junit.framework.Assert;
31import junit.framework.AssertionFailedError;
32
33import java.lang.annotation.Annotation;
34import java.lang.reflect.Constructor;
35import java.lang.reflect.InvocationTargetException;
36import java.lang.reflect.Member;
37import java.lang.reflect.Method;
38import java.lang.reflect.Modifier;
39import java.util.Arrays;
40import java.util.Collection;
41import java.util.Collections;
42import java.util.Comparator;
43import java.util.Iterator;
44import java.util.List;
45import java.util.Map;
46import java.util.Set;
47import java.util.SortedSet;
48import java.util.TreeSet;
49import java.util.concurrent.TimeUnit;
50import java.util.regex.Pattern;
51
52import javax.annotation.Nullable;
53
54/**
55 * A test utility that verifies that your methods throw {@link
56 * NullPointerException} or {@link UnsupportedOperationException} whenever any
57 * of their parameters are null. To use it, you must first provide valid default
58 * values for the parameter types used by the class.
59 *
60 * @author Kevin Bourrillion
61 * @since 10.0
62 */
63@Beta
64public final class NullPointerTester {
65  private final Map<Class<?>, Object> defaults = Maps.newHashMap();
66  private final List<Member> ignoredMembers = Lists.newArrayList();
67
68  public NullPointerTester() {
69    setCommonDefaults();
70  }
71
72  private final void setCommonDefaults() {
73    setDefault(Appendable.class, new StringBuilder());
74    setDefault(CharSequence.class, "");
75    setDefault(Class.class, Class.class);
76    setDefault(Collection.class, Collections.emptySet());
77    setDefault(Comparable.class, 0);
78    setDefault(Comparator.class, Collections.reverseOrder());
79    setDefault(Function.class, Functions.identity());
80    setDefault(Integer.class, 0);
81    setDefault(Iterable.class, Collections.emptySet());
82    setDefault(Iterator.class, Iterators.emptyIterator());
83    setDefault(List.class, Collections.emptyList());
84    setDefault(Map.class, Collections.emptyMap());
85    setDefault(Object.class, new Object());
86    setDefault(Object[].class, new Object[0]);
87    setDefault(Pattern.class, Pattern.compile(""));
88    setDefault(Predicate.class, Predicates.alwaysTrue());
89    setDefault(Set.class, Collections.emptySet());
90    setDefault(SortedSet.class, new TreeSet());
91    setDefault(String.class, "");
92    setDefault(Supplier.class, Suppliers.ofInstance(1));
93    setDefault(Throwable.class, new Exception());
94    setDefault(TimeUnit.class, TimeUnit.SECONDS);
95    setDefault(int.class, 0);
96    setDefault(long.class, 0L);
97    setDefault(short.class, (short) 0);
98    setDefault(char.class, 'a');
99    setDefault(byte.class, (byte) 0);
100    setDefault(float.class, 0.0f);
101    setDefault(double.class, 0.0d);
102    setDefault(boolean.class, false);
103  }
104
105  /**
106   * Sets a default value that can be used for any parameter of type
107   * {@code type}. Returns this object.
108   */
109  public <T> NullPointerTester setDefault(Class<T> type, T value) {
110    defaults.put(type, value);
111    return this;
112  }
113
114  /**
115   * Ignore a member (constructor or method) in testAllXxx methods. Returns
116   * this object.
117   */
118  public NullPointerTester ignore(Member member) {
119    ignoredMembers.add(member);
120    return this;
121  }
122
123  /**
124   * Runs {@link #testConstructor} on every public constructor in class {@code
125   * c}.
126   */
127  public void testAllPublicConstructors(Class<?> c) throws Exception {
128    for (Constructor<?> constructor : c.getDeclaredConstructors()) {
129      if (isPublic(constructor) && !isStatic(constructor)
130          && !isIgnored(constructor)) {
131        testConstructor(constructor);
132      }
133    }
134  }
135
136  /**
137   * Runs {@link #testMethod} on every public static method in class
138   * {@code c}.
139   */
140  public void testAllPublicStaticMethods(Class<?> c) throws Exception {
141    for (Method method : c.getDeclaredMethods()) {
142      if (isPublic(method) && isStatic(method) && !isIgnored(method)) {
143        testMethod(null, method);
144      }
145    }
146  }
147
148  /**
149   * Runs {@link #testMethod} on every public instance method of
150   * {@code instance}.
151   */
152  public void testAllPublicInstanceMethods(Object instance) throws Exception {
153    Class<?> c = instance.getClass();
154    for (Method method : c.getDeclaredMethods()) {
155      if (isPublic(method) && !isStatic(method) && !isIgnored(method)) {
156        testMethod(instance, method);
157      }
158    }
159  }
160
161  /**
162   * Verifies that {@code method} produces a {@link NullPointerException}
163   * or {@link UnsupportedOperationException} whenever <i>any</i> of its
164   * non-{@link Nullable} parameters are null.
165   *
166   * @param instance the instance to invoke {@code method} on, or null if
167   *     {@code method} is static
168   */
169  public void testMethod(Object instance, Method method) throws Exception {
170    Class<?>[] types = method.getParameterTypes();
171    for (int nullIndex = 0; nullIndex < types.length; nullIndex++) {
172      testMethodParameter(instance, method, nullIndex);
173    }
174  }
175
176  /**
177   * Verifies that {@code ctor} produces a {@link NullPointerException} or
178   * {@link UnsupportedOperationException} whenever <i>any</i> of its
179   * non-{@link Nullable} parameters are null.
180   */
181  public void testConstructor(Constructor<?> ctor) throws Exception {
182    Class<?>[] types = ctor.getParameterTypes();
183    for (int nullIndex = 0; nullIndex < types.length; nullIndex++) {
184      testConstructorParameter(ctor, nullIndex);
185    }
186  }
187
188  /**
189   * Verifies that {@code method} produces a {@link NullPointerException} or
190   * {@link UnsupportedOperationException} when the parameter in position {@code
191   * paramIndex} is null.  If this parameter is marked {@link Nullable}, this
192   * method does nothing.
193   *
194   * @param instance the instance to invoke {@code method} on, or null if
195   *     {@code method} is static
196   */
197  public void testMethodParameter(Object instance, final Method method,
198      int paramIndex) throws Exception {
199    method.setAccessible(true);
200    testFunctorParameter(instance, new Functor() {
201        @Override public Class<?>[] getParameterTypes() {
202          return method.getParameterTypes();
203        }
204        @Override public Annotation[][] getParameterAnnotations() {
205          return method.getParameterAnnotations();
206        }
207        @Override public void invoke(Object instance, Object[] params)
208            throws InvocationTargetException, IllegalAccessException {
209          method.invoke(instance, params);
210        }
211        @Override public String toString() {
212          return method.getName()
213              + "(" + Arrays.toString(getParameterTypes()) + ")";
214        }
215      }, paramIndex, method.getDeclaringClass());
216  }
217
218  /**
219   * Verifies that {@code ctor} produces a {@link NullPointerException} or
220   * {@link UnsupportedOperationException} when the parameter in position {@code
221   * paramIndex} is null.  If this parameter is marked {@link Nullable}, this
222   * method does nothing.
223   */
224  public void testConstructorParameter(final Constructor<?> ctor,
225      int paramIndex) throws Exception {
226    ctor.setAccessible(true);
227    testFunctorParameter(null, new Functor() {
228        @Override public Class<?>[] getParameterTypes() {
229          return ctor.getParameterTypes();
230        }
231        @Override public Annotation[][] getParameterAnnotations() {
232          return ctor.getParameterAnnotations();
233        }
234        @Override public void invoke(Object instance, Object[] params)
235            throws InvocationTargetException, IllegalAccessException,
236            InstantiationException {
237          ctor.newInstance(params);
238        }
239      }, paramIndex, ctor.getDeclaringClass());
240  }
241
242  /**
243   * Verifies that {@code func} produces a {@link NullPointerException} or
244   * {@link UnsupportedOperationException} when the parameter in position {@code
245   * paramIndex} is null.  If this parameter is marked {@link Nullable}, this
246   * method does nothing.
247   *
248   * @param instance the instance to invoke {@code func} on, or null if
249   *     {@code func} is static
250   */
251  private void testFunctorParameter(Object instance, Functor func,
252      int paramIndex, Class<?> testedClass) throws Exception {
253    if (parameterIsPrimitiveOrNullable(func, paramIndex)) {
254      return; // there's nothing to test
255    }
256    Object[] params = buildParamList(func, paramIndex);
257    try {
258      func.invoke(instance, params);
259      Assert.fail("No exception thrown from " + func +
260          Arrays.toString(params) + " for " + testedClass);
261    } catch (InvocationTargetException e) {
262      Throwable cause = e.getCause();
263      if (cause instanceof NullPointerException ||
264          cause instanceof UnsupportedOperationException) {
265        return;
266      }
267      AssertionFailedError error = new AssertionFailedError(
268          "wrong exception thrown from " + func + ": " + cause);
269      error.initCause(cause);
270      throw error;
271    }
272  }
273
274  private static boolean parameterIsPrimitiveOrNullable(
275      Functor func, int paramIndex) {
276    if (func.getParameterTypes()[paramIndex].isPrimitive()) {
277      return true;
278    }
279    Annotation[] annotations = func.getParameterAnnotations()[paramIndex];
280    for (Annotation annotation : annotations) {
281      if (annotation instanceof Nullable) {
282        return true;
283      }
284    }
285    return false;
286  }
287
288  private Object[] buildParamList(Functor func, int indexOfParamToSetToNull) {
289    Class<?>[] types = func.getParameterTypes();
290    Object[] params = new Object[types.length];
291
292    for (int i = 0; i < types.length; i++) {
293      if (i != indexOfParamToSetToNull) {
294        params[i] = defaults.get(types[i]);
295        if (!parameterIsPrimitiveOrNullable(func, i)) {
296          Assert.assertTrue("No default value found for " + types[i].getName(),
297              params[i] != null);
298        }
299      }
300    }
301    return params;
302  }
303
304  private interface Functor {
305    Class<?>[] getParameterTypes();
306    Annotation[][] getParameterAnnotations();
307    void invoke(Object o, Object[] params) throws Exception;
308  }
309
310  private static boolean isPublic(Member member) {
311    return Modifier.isPublic(member.getModifiers());
312  }
313
314  private static boolean isStatic(Member member) {
315    return Modifier.isStatic(member.getModifiers());
316  }
317
318  private boolean isIgnored(Member member) {
319    return member.isSynthetic() || ignoredMembers.contains(member);
320  }
321}
322