/*
* The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
* (the "License"). You may not use this work except in compliance with the License, which is
* available at www.apache.org/licenses/LICENSE-2.0
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied, as more fully set forth in the License.
*
* See the NOTICE file distributed with this work for information regarding copyright ownership.
*/
package alluxio;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.testing.EqualsTester;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.reflect.Whitebox;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Common utilities for testing.
*/
public final class CommonTestUtils {
private static final Map<Class<?>, List<?>> PRIMITIVE_VALUES =
ImmutableMap.<Class<?>, List<?>>builder()
.put(boolean.class, Lists.newArrayList(true, false))
.put(char.class, Lists.newArrayList('a', 'b'))
.put(byte.class, Lists.newArrayList((byte) 10, (byte) 11))
.put(short.class, Lists.newArrayList((short) 20, (short) 21))
.put(int.class, Lists.newArrayList(30, 31))
.put(long.class, Lists.newArrayList((long) 40, (long) 41))
.put(float.class, Lists.newArrayList((float) 50, (float) 51))
.put(double.class, Lists.newArrayList((double) 60, (double) 61)).build();
/**
* Traverses a chain of potentially private fields using {@link Whitebox}.
*
* For example, if you have the classes
*
*<pre>{@code
*public class Foo {
* private Bar myBar = new Bar();
*}
*
*public class Bar {
* private String secret = "puppy";
*}
*}</pre>
*
* then you can access {@code "puppy"} with
* {@code CommonTestUtils.getInternalState(new Foo(), "myBar", "secret")}.
*
* @param instance the object to start the traversal from
* @param fieldNames the field names to traverse
* @return the final value at the end of the traversal
*/
public static <T> T getInternalState(Object instance, String... fieldNames) {
Object current = instance;
for (String fieldName : fieldNames) {
Object next = Whitebox.getInternalState(current, fieldName);
if (next == null) {
throw new RuntimeException(
"Couldn't find field " + fieldName + " in " + current.getClass());
}
current = next;
}
@SuppressWarnings("unchecked")
T finalObject = (T) current;
return finalObject;
}
/**
* Uses reflection to test the equals and hashCode methods for the given simple java object.
*
* It is required that the given class has a no-arg constructor.
*
* Note: To use this method to test a class which contains a final non-enum class as a field, the
* class must either have a no-arg constructor, or you must prepare the final class for testing.
* See the top of {@link CommonTestUtilsTest} for an example.
*
* @param clazz the class to test the equals and hashCode methods for
* @param excludedFields names of fields which should not impact equality
*/
public static <T> void testEquals(Class<T> clazz, String... excludedFields) {
Set<String> excludedFieldsSet = new HashSet<>(Arrays.asList(excludedFields));
EqualsTester equalsTester = new EqualsTester();
equalsTester.addEqualityGroup(createBaseObject(clazz), createBaseObject(clazz));
// For each non-excluded field, create an object of the class with only that field changed.
for (Field field : getNonStaticFields(clazz)) {
if (excludedFieldsSet.contains(field.getName())) {
continue;
}
field.setAccessible(true);
T instance = createBaseObject(clazz);
try {
field.set(instance, getValuesForFieldType(field.getType()).get(1));
} catch (Exception e) {
throw Throwables.propagate(e);
}
equalsTester.addEqualityGroup(instance);
}
equalsTester.testEquals();
}
/**
* @param clazz a class
* @return an object of the given class with fields set according to the first values returned by
* {@link #getValuesForFieldType(Class)}
*/
private static <T> T createBaseObject(Class<T> clazz) {
try {
Constructor<T> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
T instance = constructor.newInstance();
for (Field field : getNonStaticFields(clazz)) {
field.setAccessible(true);
field.set(instance, getValuesForFieldType(field.getType()).get(0));
}
return instance;
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
/**
* Returns a list of at least two values of the given type.
*
* This is done for primitive types by looking them up in a map of preset values. For other types,
* the first value used is {@code null} and the second value is a mock created by Powermock.
*
* @param type the type to return values for
* @return at least two values assignable to the given type
*/
private static List<?> getValuesForFieldType(Class<?> type) throws Exception {
if (type.isEnum()) {
List<?> enumValues = Lists.newArrayList(type.getEnumConstants());
enumValues.add(null);
return enumValues;
}
List<?> primitiveValues = PRIMITIVE_VALUES.get(type);
if (primitiveValues != null) {
return primitiveValues;
}
List<Object> classValues = Lists.newArrayList();
classValues.add(null);
try {
classValues.add(PowerMockito.mock(type));
} catch (Exception e) {
try {
Constructor<?> constructor = type.getDeclaredConstructor();
constructor.setAccessible(true);
classValues.add(constructor.newInstance());
} catch (ReflectiveOperationException e1) {
throw new RuntimeException(
"Couldn't create an instance of " + type.getName() + ". Please use @PrepareForTest.");
}
}
return classValues;
}
/**
* @param type the type to get the fields for
* @return all nonstatic fields of an object of the given type
*/
private static List<Field> getNonStaticFields(Class<?> type) {
List<Field> fields = new ArrayList<>();
for (Class<?> c = type; c != null; c = c.getSuperclass()) {
for (Field field : c.getDeclaredFields()) {
if (!Modifier.isStatic(field.getModifiers())) {
fields.add(field);
}
}
}
return fields;
}
}