/*
* Copyright 2010 Proofpoint, 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 io.airlift.testing;
/**
* Derived from http://code.google.com/p/kawala
*
* Licensed under Apache License, Version 2.0
*/
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static com.google.common.collect.Lists.newArrayList;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.COMPARE_CLASS_CAST_EXCEPTION;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.COMPARE_EQUAL;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.COMPARE_EQUAL_TO_NULL;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.COMPARE_NOT_EQUAL;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.COMPARE_NOT_REFLEXIVE;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.EQUAL;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.EQUAL_NULL_EXCEPTION;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.EQUAL_TO_NULL;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.EQUAL_TO_UNRELATED_CLASS;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.EQUAL_TO_UNRELATED_CLASS_CLASS_CAST_EXCEPTION;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.HASH_CODE_NOT_SAME;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.NOT_EQUAL;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.NOT_GREATER_THAN;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.NOT_LESS_THAN;
import static io.airlift.testing.EquivalenceTester.EquivalenceFailureType.NOT_REFLEXIVE;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
/**
* Equivalence tester streamlining tests of {@link #equals(Object)} and {@link #hashCode} methods. Using this tester makes it
* easy to verify that {@link #equals(Object)} is indeed an <a href="http://en.wikipedia.org/wiki/Equivalence_relation">equivalence
* relation</a> (reflexive, symmetric and transitive). It also verifies that equality between two objects implies hash
* code equality, as required by the {@link #hashCode()} contract.
*/
public final class EquivalenceTester
{
@Deprecated
public static void check(Collection<?>... equivalenceClasses)
{
EquivalenceCheck<Object> tester = equivalenceTester();
for (Collection<?> equivalenceClass : equivalenceClasses) {
tester.addEquivalentGroup(equivalenceClass);
}
tester.check();
}
public static <T> EquivalenceCheck<T> equivalenceTester()
{
return new EquivalenceCheck<T>();
}
public static class EquivalenceCheck<T>
{
private final List<List<T>> equivalenceClasses = new ArrayList<>();
private EquivalenceCheck()
{
}
@SafeVarargs
public final EquivalenceCheck<T> addEquivalentGroup(T value, T... moreValues)
{
equivalenceClasses.add(Lists.asList(value, moreValues));
return this;
}
public EquivalenceCheck<T> addEquivalentGroup(Iterable<T> objects)
{
equivalenceClasses.add(newArrayList(objects));
return this;
}
public void check()
{
List<ElementCheckFailure> failures = checkEquivalence();
if (!failures.isEmpty()) {
throw new EquivalenceAssertionError(failures);
}
}
@SuppressWarnings({"ObjectEqualsNull"})
private List<ElementCheckFailure> checkEquivalence()
{
ImmutableList.Builder<ElementCheckFailure> errors = new ImmutableList.Builder<ElementCheckFailure>();
//
// equal(null)
//
int classNumber = 0;
for (Collection<?> congruenceClass : equivalenceClasses) {
int elementNumber = 0;
for (Object element : congruenceClass) {
// nothing can be equal to null
try {
if (element.equals(null)) {
errors.add(new ElementCheckFailure(EQUAL_TO_NULL, classNumber, elementNumber, element));
}
}
catch (NullPointerException e) {
errors.add(new ElementCheckFailure(EQUAL_NULL_EXCEPTION, classNumber, elementNumber, element));
}
// if a class implements comparable, object.compareTo(null) must throw NPE
if (element instanceof Comparable) {
try {
((Comparable<?>) element).compareTo(null);
errors.add(new ElementCheckFailure(COMPARE_EQUAL_TO_NULL, classNumber, elementNumber, element));
}
catch (NullPointerException e) {
// ok
}
}
// nothing can be equal to object of an unrelated class
try {
if (element.equals(new OtherClass())) {
errors.add(new ElementCheckFailure(EQUAL_TO_UNRELATED_CLASS, classNumber, elementNumber, element));
}
} catch (ClassCastException e) {
errors.add(new ElementCheckFailure(EQUAL_TO_UNRELATED_CLASS_CLASS_CAST_EXCEPTION, classNumber, elementNumber, element));
}
++elementNumber;
}
++classNumber;
}
//
// reflexivity
//
classNumber = 0;
for (Collection<?> congruenceClass : equivalenceClasses) {
int elementNumber = 0;
for (Object element : congruenceClass) {
if (!element.equals(element)) {
errors.add(new ElementCheckFailure(NOT_REFLEXIVE, classNumber, elementNumber, element));
}
if (!doesCompareReturn0(element, element)) {
errors.add(new ElementCheckFailure(COMPARE_NOT_REFLEXIVE, classNumber, elementNumber, element));
}
++elementNumber;
}
++classNumber;
}
//
// equality within congruence classes
//
classNumber = 0;
for (List<?> congruenceClass : equivalenceClasses) {
for (int primaryElementNumber = 0; primaryElementNumber < congruenceClass.size(); primaryElementNumber++) {
Object primary = congruenceClass.get(primaryElementNumber);
for (int secondaryElementNumber = primaryElementNumber + 1; secondaryElementNumber < congruenceClass.size(); secondaryElementNumber++) {
Object secondary = congruenceClass.get(secondaryElementNumber);
if (!primary.equals(secondary)) {
errors.add(new PairCheckFailure(NOT_EQUAL, classNumber, primaryElementNumber, primary, classNumber, secondaryElementNumber, secondary));
}
if (!secondary.equals(primary)) {
errors.add(new PairCheckFailure(NOT_EQUAL, classNumber, secondaryElementNumber, secondary, classNumber, primaryElementNumber, primary));
}
try {
if (!doesCompareReturn0(primary, secondary)) {
errors.add(new PairCheckFailure(COMPARE_NOT_EQUAL,
classNumber,
primaryElementNumber,
primary,
classNumber,
secondaryElementNumber,
secondary));
}
}
catch (ClassCastException e) {
errors.add(new PairCheckFailure(COMPARE_CLASS_CAST_EXCEPTION, classNumber, primaryElementNumber, primary, classNumber, secondaryElementNumber, secondary));
}
try {
if (!doesCompareReturn0(secondary, primary)) {
errors.add(new PairCheckFailure(COMPARE_NOT_EQUAL,
classNumber,
secondaryElementNumber,
secondary,
classNumber,
primaryElementNumber,
primary));
}
}
catch (ClassCastException e) {
errors.add(new PairCheckFailure(COMPARE_CLASS_CAST_EXCEPTION, classNumber, secondaryElementNumber, secondary, classNumber, primaryElementNumber, primary));
}
if (primary.hashCode() != secondary.hashCode()) {
errors.add(new PairCheckFailure(HASH_CODE_NOT_SAME, classNumber, primaryElementNumber, primary, classNumber, secondaryElementNumber, secondary));
}
}
}
++classNumber;
}
//
// inequality across congruence classes
//
for (int primaryClassNumber = 0; primaryClassNumber < equivalenceClasses.size(); primaryClassNumber++) {
List<?> primaryCongruenceClass = equivalenceClasses.get(primaryClassNumber);
for (int secondaryClassNumber = primaryClassNumber + 1; secondaryClassNumber < equivalenceClasses.size(); secondaryClassNumber++) {
List<?> secondaryCongruenceClass = equivalenceClasses.get(secondaryClassNumber);
int primaryElementNumber = 0;
for (Object primary : primaryCongruenceClass) {
int secondaryElementNumber = 0;
for (Object secondary : secondaryCongruenceClass) {
if (primary.equals(secondary)) {
errors.add(new PairCheckFailure(EQUAL, primaryClassNumber, primaryElementNumber, primary, secondaryClassNumber, secondaryElementNumber, secondary));
}
if (secondary.equals(primary)) {
errors.add(new PairCheckFailure(EQUAL, secondaryClassNumber, secondaryElementNumber, secondary, primaryClassNumber, primaryElementNumber, primary));
}
try {
if (!doesCompareNotReturn0(primary, secondary)) {
errors.add(new PairCheckFailure(COMPARE_EQUAL, primaryClassNumber, primaryElementNumber, primary, secondaryClassNumber, secondaryElementNumber, secondary));
}
}
catch (ClassCastException e) {
errors.add(new PairCheckFailure(COMPARE_CLASS_CAST_EXCEPTION,
primaryClassNumber,
primaryElementNumber,
primary,
secondaryClassNumber,
secondaryElementNumber,
secondary));
}
try {
if (!doesCompareNotReturn0(secondary, primary)) {
errors.add(new PairCheckFailure(COMPARE_EQUAL, secondaryClassNumber, secondaryElementNumber, secondary, primaryClassNumber, primaryElementNumber, primary));
}
}
catch (ClassCastException e) {
errors.add(new PairCheckFailure(COMPARE_CLASS_CAST_EXCEPTION,
secondaryClassNumber,
secondaryElementNumber,
secondary,
primaryClassNumber,
primaryElementNumber,
primary));
}
secondaryElementNumber++;
}
primaryElementNumber++;
}
}
}
return errors.build();
}
@SuppressWarnings("unchecked")
private static <T> boolean doesCompareReturn0(T e1, T e2)
{
if (!(e1 instanceof Comparable<?>)) {
return true;
}
Comparable<T> comparable = (Comparable<T>) e1;
return comparable.compareTo(e2) == 0;
}
@SuppressWarnings("unchecked")
private static <T> boolean doesCompareNotReturn0(T e1, T e2)
{
if (!(e1 instanceof Comparable<?>)) {
return true;
}
Comparable<T> comparable = (Comparable<T>) e1;
return comparable.compareTo(e2) != 0;
}
private static class OtherClass
{
}
}
@SafeVarargs
@Deprecated
public static <T extends Comparable<T>> void checkComparison(Iterable<T> initialGroup, Iterable<T> greaterGroup, Iterable<T>... moreGreaterGroup)
{
ComparisonCheck<T> tester = comparisonTester()
.addLesserGroup(initialGroup)
.addGreaterGroup(greaterGroup);
for (Iterable<T> equivalenceClass : moreGreaterGroup) {
tester.addGreaterGroup(equivalenceClass);
}
tester.check();
}
public static InitialComparisonCheck comparisonTester()
{
return new InitialComparisonCheck();
}
public static class InitialComparisonCheck
{
private InitialComparisonCheck()
{
}
@SafeVarargs
public final <T extends Comparable<T>> ComparisonCheck<T> addLesserGroup(T value, T... moreValues)
{
ComparisonCheck<T> comparisonCheck = new ComparisonCheck<T>();
comparisonCheck.addGreaterGroup(Lists.asList(value, moreValues));
return comparisonCheck;
}
public <T extends Comparable<T>> ComparisonCheck<T> addLesserGroup(Iterable<T> objects)
{
ComparisonCheck<T> comparisonCheck = new ComparisonCheck<T>();
comparisonCheck.addGreaterGroup(objects);
return comparisonCheck;
}
}
public static class ComparisonCheck <T extends Comparable<T>>
{
private final EquivalenceCheck<T> equivalence = new EquivalenceCheck<T>();
private ComparisonCheck()
{
}
@SafeVarargs
public final ComparisonCheck<T> addGreaterGroup(T value, T... moreValues)
{
equivalence.addEquivalentGroup(Lists.asList(value, moreValues));
return this;
}
public ComparisonCheck<T> addGreaterGroup(Iterable<T> objects)
{
equivalence.addEquivalentGroup(objects);
return this;
}
public void check()
{
ImmutableList.Builder<ElementCheckFailure> builder = new ImmutableList.Builder<ElementCheckFailure>();
builder.addAll(equivalence.checkEquivalence());
List<List<T>> equivalenceClasses = equivalence.equivalenceClasses;
for (int lesserClassNumber = 0; lesserClassNumber < equivalenceClasses.size(); lesserClassNumber++) {
List<T> lesserBag = equivalenceClasses.get(lesserClassNumber);
for (int greaterClassNumber = lesserClassNumber + 1; greaterClassNumber < equivalenceClasses.size(); greaterClassNumber++) {
List<T> greaterBag = equivalenceClasses.get(greaterClassNumber);
for (int lesserElementNumber = 0; lesserElementNumber < lesserBag.size(); lesserElementNumber++) {
T lesser = lesserBag.get(lesserElementNumber);
for (int greaterElementNumber = 0; greaterElementNumber < greaterBag.size(); greaterElementNumber++) {
T greater = greaterBag.get(greaterElementNumber);
try {
if (lesser.compareTo(greater) >= 0) {
builder.add(new PairCheckFailure(NOT_LESS_THAN, lesserClassNumber, lesserElementNumber, lesser, greaterClassNumber, greaterElementNumber, greater));
}
}
catch (ClassCastException e) {
// this has already been reported in the checkEquivalence section
}
try {
if (greater.compareTo(lesser) <= 0) {
builder.add(new PairCheckFailure(NOT_GREATER_THAN, greaterClassNumber, greaterElementNumber, greater, lesserClassNumber, lesserElementNumber, lesser));
}
}
catch (ClassCastException e) {
// this has already been reported in the checkEquivalence section
}
}
}
}
}
List<ElementCheckFailure> failures = builder.build();
if (!failures.isEmpty()) {
throw new EquivalenceAssertionError(failures);
}
}
}
public static enum EquivalenceFailureType {
EQUAL_TO_NULL("Element (%d, %d):<%s> returns true when compared to null via equals()"),
EQUAL_NULL_EXCEPTION("Element (%d, %d):<%s> throws NullPointerException when when compared to null via equals()"),
COMPARE_EQUAL_TO_NULL("Element (%d, %d):<%s> implements Comparable but does not throw NullPointerException when compared to null"),
EQUAL_TO_UNRELATED_CLASS("Element (%d, %d):<%s> returns true when compared to an unrelated class via equals()"),
EQUAL_TO_UNRELATED_CLASS_CLASS_CAST_EXCEPTION("Element (%d, %d):<%s> throws a ClassCastException when compared to an unrelated class via equals()"),
NOT_REFLEXIVE("Element (%d, %d):<%s> is not equal to itself when compared via equals()"),
COMPARE_NOT_REFLEXIVE("Element (%d, %d):<%s> implements Comparable but compare does not return 0 when compared to itself"),
NOT_EQUAL("Element (%d, %d):<%s> is not equal to element (%d, %d):<%s> when compared via equals()"),
COMPARE_NOT_EQUAL("Element (%d, %d):<%s> is not equal to element (%d, %d):<%s> when compared via compareTo(T)"),
COMPARE_CLASS_CAST_EXCEPTION("Element (%d, %d):<%s> throws a ClassCastException when compared to element (%d, %d):<%s> via compareTo(T)"),
HASH_CODE_NOT_SAME("Elements (%d, %d):<%s> and (%d, %d):<%s> have different hash codes"),
EQUAL("Element (%d, %d):<%s> is equal to element (%d, %d):<%s> when compared via equals()"),
COMPARE_EQUAL("Element (%d, %d):<%s> implements Comparable and returns 0 when compared to element (%d, %d):<%s>"),
NOT_LESS_THAN("Element (%d, %d):<%s> is not less than (%d, %d):<%s>"),
NOT_GREATER_THAN("Element (%d, %d):<%s> is not greater than (%d, %d):<%s>"),
;
private final String message;
EquivalenceFailureType(String message)
{
this.message = message;
}
public String getMessage()
{
return message;
}
}
public static class ElementCheckFailure
{
protected final EquivalenceFailureType type;
protected final int primaryClassNumber;
protected final int primaryElementNumber;
protected final Object primaryObject;
public ElementCheckFailure(EquivalenceFailureType type, int primaryClassNumber, int primaryElementNumber, Object primaryObject)
{
requireNonNull(type, "type is null");
this.type = type;
this.primaryClassNumber = primaryClassNumber;
this.primaryElementNumber = primaryElementNumber;
this.primaryObject = primaryObject;
}
public EquivalenceFailureType getType()
{
return type;
}
public int getPrimaryClassNumber()
{
return primaryClassNumber;
}
public int getPrimaryElementNumber()
{
return primaryElementNumber;
}
@Override
public String toString()
{
return format(type.getMessage(), primaryClassNumber, primaryElementNumber, primaryObject);
}
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean equals(Object o)
{
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ElementCheckFailure that = (ElementCheckFailure) o;
if (primaryClassNumber != that.primaryClassNumber) {
return false;
}
if (primaryElementNumber != that.primaryElementNumber) {
return false;
}
if (!type.equals(that.type)) {
return false;
}
if (primaryObject != that.primaryObject && // Some testing objects not reflexive
!primaryObject.equals(that.primaryObject)) {
return false;
}
return true;
}
@Override
public int hashCode()
{
int result = type.hashCode();
result = 31 * result + primaryClassNumber;
result = 31 * result + primaryElementNumber;
result = 31 * result + primaryObject.hashCode();
return result;
}
}
public static class PairCheckFailure extends ElementCheckFailure
{
private final int secondaryClassNumber;
private final int secondaryElementNumber;
private final Object secondaryObject;
public PairCheckFailure(EquivalenceFailureType type, int primaryClassNumber, int primaryElementNumber, Object primaryObject, int secondaryClassNumber, int secondaryElementNumber, Object secondaryObject)
{
super(type, primaryClassNumber, primaryElementNumber, primaryObject);
this.secondaryClassNumber = secondaryClassNumber;
this.secondaryElementNumber = secondaryElementNumber;
this.secondaryObject = secondaryObject;
}
public int getSecondaryClassNumber()
{
return secondaryClassNumber;
}
public int getSecondaryElementNumber()
{
return secondaryElementNumber;
}
@Override
public String toString()
{
return format(type.getMessage(), primaryClassNumber, primaryElementNumber, primaryObject, secondaryClassNumber, secondaryElementNumber, secondaryObject);
}
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean equals(Object o)
{
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
PairCheckFailure that = (PairCheckFailure) o;
if (primaryClassNumber != that.primaryClassNumber) {
return false;
}
if (primaryElementNumber != that.primaryElementNumber) {
return false;
}
if (primaryObject != that.primaryObject && // Some testing objects not reflexive
!primaryObject.equals(that.primaryObject)) {
return false;
}
if (secondaryClassNumber != that.secondaryClassNumber) {
return false;
}
if (secondaryElementNumber != that.secondaryElementNumber) {
return false;
}
if (secondaryObject != that.secondaryObject && // Some testing objects not reflexive
!secondaryObject.equals(that.secondaryObject)) {
return false;
}
if (!type.equals(that.type)) {
return false;
}
return true;
}
@Override
public int hashCode()
{
int result = super.hashCode();
result = 31 * result + type.hashCode();
result = 31 * result + primaryClassNumber;
result = 31 * result + primaryElementNumber;
result = 31 * result + primaryObject.hashCode();
result = 31 * result + secondaryClassNumber;
result = 31 * result + secondaryElementNumber;
result = 31 * result + secondaryObject.hashCode();
return result;
}
}
}