blob: a0d4ab160b719873d7aaa532ac8e52a1bd2e32ec [file] [log] [blame]
/*
* Copyright (C) 2005 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.common.testutils;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
import junit.framework.Assert;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/**
* A test utility that verifies that your methods throw {@link
* NullPointerException} or {@link UnsupportedOperationException} whenever any
* of their parameters are null. To use it, you must first provide valid default
* values for the parameter types used by the class.
*
* @author Kevin Bourrillion
*/
public class NullPointerTester {
private Map<Class<?>, Object> defaults = Maps.newHashMap();
public NullPointerTester() {
setCommonDefaults();
}
private final void setCommonDefaults() {
setDefault(Appendable.class, new StringBuilder());
setDefault(CharSequence.class, "");
setDefault(Class.class, Class.class);
setDefault(Collection.class, Collections.emptySet());
setDefault(Comparable.class, 0);
setDefault(Comparator.class, Collections.reverseOrder());
setDefault(Function.class, Functions.identity());
setDefault(Integer.class, 0);
setDefault(Iterable.class, Collections.emptySet());
setDefault(Iterator.class, Iterators.emptyIterator());
setDefault(List.class, Collections.emptyList());
setDefault(Map.class, Collections.emptyMap());
setDefault(Object.class, new Object());
setDefault(Object[].class, new Object[0]);
setDefault(Pattern.class, Pattern.compile(""));
setDefault(Predicate.class, Predicates.alwaysTrue());
setDefault(Set.class, Collections.emptySet());
setDefault(String.class, "");
setDefault(int.class, 0);
setDefault(long.class, 0L);
setDefault(short.class, (short) 0);
setDefault(char.class, 'a');
setDefault(byte.class, (byte) 0);
setDefault(float.class, 0.0f);
setDefault(double.class, 0.0d);
setDefault(boolean.class, false);
}
/**
* Sets a default value that can be used for any parameter of type
* {@code type}.
*/
public <T> void setDefault(Class<T> type, T value) {
defaults.put(type, value);
}
/**
* Runs {@link #testConstructor} on every public constructor in class {@code
* c}.
*/
public void testAllPublicConstructors(Class<?> c) throws Exception {
for (Constructor<?> constructor : c.getDeclaredConstructors()) {
if (isPublic(constructor) && !isStatic(constructor)) {
testConstructor(constructor);
}
}
}
/**
* Runs {@link #testMethod} on every public static method in class
* {@code c}.
*/
public void testAllPublicStaticMethods(Class<?> c) throws Exception {
for (Method method : c.getDeclaredMethods()) {
if (isPublic(method) && isStatic(method)) {
testMethod(null, method);
}
}
}
/**
* Runs {@link #testMethod} on every public instance method of
* {@code instance}.
*/
public void testAllPublicInstanceMethods(Object instance) throws Exception {
Class<?> c = instance.getClass();
for (Method method : c.getDeclaredMethods()) {
if (isPublic(method) && !isStatic(method)) {
testMethod(instance, method);
}
}
}
/**
* Verifies that {@code method} produces a {@link NullPointerException}
* or {@link UnsupportedOperationException} whenever <i>any</i> of its
* non-{@link Nullable} parameters are null.
*
* @param instance the instance to invoke {@code method} on, or null if
* {@code method} is static
*/
public void testMethod(Object instance, Method method) throws Exception {
Class<?>[] types = method.getParameterTypes();
for (int nullIndex = 0; nullIndex < types.length; nullIndex++) {
testMethodParameter(instance, method, nullIndex);
}
}
/**
* Verifies that {@code ctor} produces a {@link NullPointerException} or
* {@link UnsupportedOperationException} whenever <i>any</i> of its
* non-{@link Nullable} parameters are null.
*/
public void testConstructor(Constructor<?> ctor) throws Exception {
Class<?>[] types = ctor.getParameterTypes();
for (int nullIndex = 0; nullIndex < types.length; nullIndex++) {
testConstructorParameter(ctor, nullIndex);
}
}
/**
* Verifies that {@code method} produces a {@link NullPointerException} or
* {@link UnsupportedOperationException} when the parameter in position {@code
* paramIndex} is null. If this parameter is marked {@link Nullable}, this
* method does nothing.
*
* @param instance the instance to invoke {@code method} on, or null if
* {@code method} is static
*/
public void testMethodParameter(Object instance, final Method method,
int paramIndex) throws Exception {
method.setAccessible(true);
testFunctorParameter(instance, new Functor() {
public Class<?>[] getParameterTypes() {
return method.getParameterTypes();
}
public Annotation[][] getParameterAnnotations() {
return method.getParameterAnnotations();
}
public void invoke(Object instance, Object[] params)
throws InvocationTargetException, IllegalAccessException {
method.invoke(instance, params);
}
@Override public String toString() {
return method.getName();
}
}, paramIndex, method.getDeclaringClass());
}
/**
* Verifies that {@code ctor} produces a {@link NullPointerException} or
* {@link UnsupportedOperationException} when the parameter in position {@code
* paramIndex} is null. If this parameter is marked {@link Nullable}, this
* method does nothing.
*/
public void testConstructorParameter(final Constructor<?> ctor,
int paramIndex) throws Exception {
ctor.setAccessible(true);
testFunctorParameter(null, new Functor() {
public Class<?>[] getParameterTypes() {
return ctor.getParameterTypes();
}
public Annotation[][] getParameterAnnotations() {
return ctor.getParameterAnnotations();
}
public void invoke(Object instance, Object[] params)
throws InvocationTargetException, IllegalAccessException,
InstantiationException {
ctor.newInstance(params);
}
}, paramIndex, ctor.getDeclaringClass());
}
/**
* Verifies that {@code func} produces a {@link NullPointerException} or
* {@link UnsupportedOperationException} when the parameter in position {@code
* paramIndex} is null. If this parameter is marked {@link Nullable}, this
* method does nothing.
*
* @param instance the instance to invoke {@code func} on, or null if
* {@code func} is static
*/
private void testFunctorParameter(Object instance, Functor func,
int paramIndex, Class<?> testedClass) throws Exception {
if (parameterIsPrimitiveOrNullable(func, paramIndex)) {
return; // there's nothing to test
}
Object[] params = buildParamList(func, paramIndex);
try {
func.invoke(instance, params);
Assert.fail("No exception thrown from " + func +
Arrays.toString(params) + " for " + testedClass);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
Assert.assertTrue("wrong exception thrown from " + func + ": " + cause,
cause instanceof NullPointerException
|| cause instanceof UnsupportedOperationException);
}
}
private static boolean parameterIsPrimitiveOrNullable(
Functor func, int paramIndex) {
if (func.getParameterTypes()[paramIndex].isPrimitive()) {
return true;
}
Annotation[] annotations = func.getParameterAnnotations()[paramIndex];
for (Annotation annotation : annotations) {
if (annotation instanceof Nullable) {
return true;
}
}
return false;
}
private Object[] buildParamList(Functor func, int indexOfParamToSetToNull) {
Class<?>[] types = func.getParameterTypes();
Object[] params = new Object[types.length];
for (int i = 0; i < types.length; i++) {
if (i != indexOfParamToSetToNull) {
params[i] = defaults.get(types[i]);
if (!parameterIsPrimitiveOrNullable(func, indexOfParamToSetToNull)) {
Assert.assertNotNull("No default value found for "
+ types[i].getName(), params[i]);
}
}
}
return params;
}
private interface Functor {
Class<?>[] getParameterTypes();
Annotation[][] getParameterAnnotations();
void invoke(Object o, Object[] params) throws Exception;
}
private static boolean isPublic(Member member) {
return Modifier.isPublic(member.getModifiers());
}
private static boolean isStatic(Member member) {
return Modifier.isStatic(member.getModifiers());
}
}