blob: 2e4fd8c857462db50106cc2c73e1b89f2e4ff839 [file] [log] [blame]
/*
* Copyright 2016 The Bazel Authors. All rights reserved.
*
* 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.idea.blaze.base.model.blaze.deepequalstester;
import com.google.common.collect.Lists;
import com.google.idea.blaze.base.model.blaze.deepequalstester.Examples.ExampleNotFoundException;
import com.google.idea.blaze.base.model.blaze.deepequalstester.Examples.Pair;
import com.google.idea.blaze.base.model.blaze.deepequalstester.ReachabilityAnalysis.ReachableClasses;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.junit.Assert;
/** Used to test equals implementations */
public final class DeepEqualsTester {
/** Thrown when the test fails */
public static class TestCorrectnessException extends Exception {
public TestCorrectnessException(String s) {
super(s);
}
}
/**
* Ensure that the equals method of {@param rootObject} uses all of its fields in its comparison.
* Recurse into every field of {@param rootObject} to ensure that they also use all of their
* fields in their equals method. Continue recursion until primitives are hit.
*
* <p>If multiple failures could occur, there is no guarantee that they will always occur in the
* same order. Only the first failure is reported.
*
* @param rootObject an example instantiation of the class we want to test for deep equals sanity.
* The object must be a standard java object (no collections, no arrays, no primitives). If
* you would like to pass these types in, you should put them in a box first.
* @param examples examples of objects to use for comparison. This should contain a pair of
* examples for every type that could be reachable from the root object. This value may be
* mutated by this test.
*/
public static <T extends Serializable> void doDeepEqualsAndHashTest(
@NotNull T rootObject, Examples examples)
throws InstantiationException, IllegalAccessException, NoSuchFieldException,
ExampleNotFoundException, TestCorrectnessException {
ReachableClasses reachableClasses = new ReachableClasses();
try {
ArrayList<String> initialPath = Lists.newArrayList("root");
// Find all of the classes reachable from the root object. This is not sound since it
// ignores subtypes (or supertypes) that could be used
ReachabilityAnalysis.computeReachableFromObject(
rootObject, rootObject.getClass(), initialPath, reachableClasses);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// Add the root object to our list of reachable classes so we can do all the testing in one
// loop
reachableClasses.addPath(rootObject.getClass(), Lists.newArrayList("root"));
// In our situations, we never need a second example of the root object
examples.addExample(rootObject.getClass(), rootObject, rootObject);
// For each reachable class, do a shallow equals test where we change each value of the
// object one at a time and test for equality
for (Class<? extends Serializable> clazz : reachableClasses.getClasses()) {
Serializable workitem = (Serializable) examples.getExamples(clazz).getFirst();
testShallowEquals(workitem, reachableClasses, examples);
}
}
private static String getFailureMessage(String method, Field field, List<String> examplePath) {
StringBuilder sb = new StringBuilder();
sb.append(field.toString())
.append(" is not represented in it's parent's ")
.append(method)
.append(" method\n");
for (String path : examplePath) {
sb.append("\t").append(path).append("\n");
}
sb.append("\n");
return sb.toString();
}
/**
* Mutate each field in the object one at a time and test for equality of the object. Assert a
* failure if any mutation doesn't result in the two objects not being equal
*/
private static <T extends Serializable> void testShallowEquals(
@NotNull T original, ReachableClasses reachableClasses, Examples examples)
throws ExampleNotFoundException, IllegalAccessException, TestCorrectnessException {
T clone = (T) DeepEqualsTesterUtil.cloneWithSerialization(original);
List<Field> allFields = DeepEqualsTesterUtil.getAllFields(original.getClass());
for (Field field : allFields) {
if (!Modifier.isStatic(field.getModifiers())) {
field.setAccessible(true);
Pair<?, ?> examplesPair =
examples.getExamples((Class<? extends Serializable>) field.getType());
Object newValueForOriginal = examplesPair.getFirst();
Object newValueForClone = examplesPair.getSecond();
Object oldValueForOriginal = field.get(original);
Object oldValueForClone = field.get(clone);
// Ensure that the two objects really are equal before we tweak them
boolean objectsTheSameBeforeTweak = original.equals(clone);
if (!objectsTheSameBeforeTweak) {
throw new TestCorrectnessException(
"original was not equal to clone before tweaking them");
}
boolean objectsHashTheSameBeforeTweak = original.hashCode() == clone.hashCode();
if (!objectsHashTheSameBeforeTweak) {
throw new TestCorrectnessException(
"original hash code was not equal to clone hash code before tweaking the objects");
}
field.set(original, newValueForOriginal);
field.set(clone, newValueForClone);
boolean equalsWorksAsIntended = !original.equals(clone);
boolean hashWorksAsIntended = original.hashCode() != clone.hashCode();
// Return to our original state before possibly failing
field.set(original, oldValueForOriginal);
field.set(clone, oldValueForClone);
Assert.assertTrue(
getFailureMessage(
"equals", field, reachableClasses.getExamplePathTo(original.getClass())),
equalsWorksAsIntended);
Assert.assertTrue(
getFailureMessage(
"hash", field, reachableClasses.getExamplePathTo(original.getClass())),
hashWorksAsIntended);
}
}
}
}