All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.airlift.testing.EquivalenceTester Maven / Gradle / Ivy

The newest version!
/*
 * 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.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;

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;

/**
 * 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 equivalence
 * relation (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 tester = equivalenceTester();
        for (Collection equivalenceClass : equivalenceClasses) {
            tester.addEquivalentGroup((Iterable)equivalenceClass);
        }
        tester.check();
    }

    public static  EquivalenceCheck equivalenceTester()
    {
        return new EquivalenceCheck();
    }

    public static class EquivalenceCheck
    {
        private final List> equivalenceClasses = newArrayList();

        private EquivalenceCheck()
        {
        }

        public EquivalenceCheck addEquivalentGroup(T value, T... moreValues)
        {
            equivalenceClasses.add(Lists.asList(value, moreValues));
            return this;
        }

        public EquivalenceCheck addEquivalentGroup(Iterable objects)
        {
            equivalenceClasses.add(newArrayList(objects));
            return this;
        }

        public void check()
        {
            List failures = checkEquivalence();

            if (!failures.isEmpty()) {
                throw new EquivalenceAssertionError(failures);
            }
        }

        @SuppressWarnings({"ObjectEqualsNull"})
        private List checkEquivalence()
        {
            ImmutableList.Builder errors = new ImmutableList.Builder();

            //
            // 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  boolean doesCompareReturn0(T e1, T e2)
        {
            if (!(e1 instanceof Comparable)) {
                return true;
            }

            Comparable comparable = (Comparable) e1;
            return comparable.compareTo(e2) == 0;
        }

        @SuppressWarnings("unchecked")
        private static  boolean doesCompareNotReturn0(T e1, T e2)
        {
            if (!(e1 instanceof Comparable)) {
                return true;
            }

            Comparable comparable = (Comparable) e1;
            return comparable.compareTo(e2) != 0;
        }

        private static class OtherClass
        {
        }
    }

    @SafeVarargs
    @Deprecated
    public static > void checkComparison(Iterable initialGroup, Iterable greaterGroup, Iterable... moreGreaterGroup)
    {
        ComparisonCheck tester = comparisonTester()
                .addLesserGroup(initialGroup)
                .addGreaterGroup(greaterGroup);

        for (Iterable equivalenceClass : moreGreaterGroup) {
            tester.addGreaterGroup(equivalenceClass);
        }
        tester.check();
    }

    public static InitialComparisonCheck comparisonTester()
    {
        return new InitialComparisonCheck();
    }

    public static class InitialComparisonCheck
    {

        private InitialComparisonCheck()
        {
        }

        public > ComparisonCheck addLesserGroup(T value, T... moreValues)
        {
            ComparisonCheck comparisonCheck = new ComparisonCheck();
            comparisonCheck.addGreaterGroup(Lists.asList(value, moreValues));
            return comparisonCheck;
        }

        public > ComparisonCheck addLesserGroup(Iterable objects)
        {
            ComparisonCheck comparisonCheck = new ComparisonCheck();
            comparisonCheck.addGreaterGroup(objects);
            return comparisonCheck;
        }
    }

    public static class ComparisonCheck >
    {
        private final EquivalenceCheck equivalence = new EquivalenceCheck();

        private ComparisonCheck()
        {
        }

        public ComparisonCheck addGreaterGroup(T value, T... moreValues)
        {
            equivalence.addEquivalentGroup(Lists.asList(value, moreValues));
            return this;
        }

        public ComparisonCheck addGreaterGroup(Iterable objects)
        {
            equivalence.addEquivalentGroup(objects);
            return this;
        }

        public void check()
        {
            ImmutableList.Builder builder = new ImmutableList.Builder();

            builder.addAll(equivalence.checkEquivalence());

            List> equivalenceClasses = equivalence.equivalenceClasses;
            for (int lesserClassNumber = 0; lesserClassNumber < equivalenceClasses.size(); lesserClassNumber++) {
                List lesserBag = equivalenceClasses.get(lesserClassNumber);

                for (int greaterClassNumber = lesserClassNumber + 1; greaterClassNumber < equivalenceClasses.size(); greaterClassNumber++) {
                    List 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 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)
        {
            Preconditions.checkNotNull(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;
        }
    }
}