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

org.organicdesign.testUtils.ComparableContract.kt Maven / Gradle / Ivy

// Copyright 2015-07-03 PlanBase Inc. & Glen Peterson
//
// 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 org.organicdesign.testUtils

import org.junit.Assert
import org.organicdesign.testUtils.ComparatorContract.CompToZero
import org.organicdesign.testUtils.EqualsContract.permutations

/**
 * Tests the various properties the Comparable contract is supposed to uphold.  If you think this is
 * confusing, realize that like equals(), it is often not possible to implement a one-sided
 * compareTo() correctly with inheritance - it's a broken concept, but it's still used so often that
 * you have to do your best with it.
 *
 * I got the idea of contract-based testing from watching Bill Venners:
 * https://www.youtube.com/watch?v=bCTZQi2dpl8
 */
object ComparableContract {
    private fun > pairComp(
            first: NamedPair,
            comp: CompToZero,
            second: NamedPair
    ) {
        Assert.assertTrue("Item A in the " + first.name + " pair must be " + comp.english() +
                          " item A in the " + second.name + " pair",
                          comp.vsZero(first.a.compareTo(second.a)))
        Assert.assertTrue("Item A in the " + first.name + " pair must be " + comp.english() +
                          " item B in the " + second.name + " pair",
                          comp.vsZero(first.a.compareTo(second.b)))
        Assert.assertTrue("Item B in the " + first.name + " pair must be " + comp.english() +
                          " item A in the " + second.name + " pair",
                          comp.vsZero(first.b.compareTo(second.a)))
        Assert.assertTrue("Item B in the " + first.name + " pair must be " + comp.english() +
                          " item B in the " + second.name + " pair",
                          comp.vsZero(first.b.compareTo(second.b)))
    }

    /**
     * Tests the various properties the Comparable contract is supposed to uphold.  Also tests that
     * the behavior of compareTo() is compatible with equals() and hashCode() which is strongly
     * suggested, but not actually required.  Write your own test if you don't want that.  Expects
     * three pair of unique objects.  Within a pair, the two objects should be equal.  Both objects in
     * the first pair are less than the ones in the second pair, which in turn are less than the
     * objects in the third pair.
     *
     * See note in class documentation.
     */
    // Because the Comparable interface is only comparable to itself, I think all the objects have to
    // extend the same comparable class.  Thus the type signature is different from
    // EqualsContract.equalsHashCode()
    //
    // The old (BAD/WRONG) signature tried to let you compare against other comparables, which is neither sensible
    // nor possible.  It also makes the Kotlin compiler go haywire:
    //
    //    public static , T1 extends S, T2 extends S, T3 extends S>
    //    void testCompareTo(T1 least1, T1 least2, T2 middle1, T2 middle2, T3 greatest1, T3 greatest2)
    //
    @JvmStatic
    fun > testCompareTo(
            least1: S,
            least2: S,
            middle1: S,
            middle2: S,
            greatest1: S,
            greatest2: S
    ) {
        // Many of the comments in this method are paraphrases or direct quotes from the Javadocs for
        // the Comparable interface.  That is where this contract is specified.
        // https://docs.oracle.com/javase/8/docs/api/
        var anySame = false
        permutations(listOf(least1, least2, middle1, middle2, greatest1, greatest2)
        ) { a: S, b: S ->
            if (a === b) {
                anySame = true
            }
        }
        require(!anySame) { "You must provide three pair of different objects in order" }
        val least = NamedPair(least1, least2, "Least")
        val middle = NamedPair(middle1, middle2, "Middle")
        val greatest = NamedPair(greatest1, greatest2, "Greatest")
        for (comp in listOf(least, middle, greatest)) {
            // Consistent with equals: (e1.compareTo(e2) == 0) if and only if e1.equals(e2)
            pairComp(comp, CompToZero.EQZ, comp)
            Assert.assertEquals(comp.name + " A must be compatibly equal to its paired B element", comp.a, comp.b)
            Assert.assertEquals(comp.name + " B must be compatibly equal to its paired A element", comp.b, comp.a)
        }
        var i = 0
        for (comp in listOf(least1, least2, middle1, middle2, greatest1, greatest2)) {
            i++
            Assert.assertEquals("item.equals(itself) should have returned true for item $i", comp, comp)

            // It is strongly recommended (though not required) that natural orderings be consistent
            // with equals.

            // One exception is java.math.BigDecimal, whose natural ordering equates BigDecimal
            // objects with equal values and different precisions (such as 4.0 and 4.00).

            // null is not an instance of any class, and e.compareTo(null) should throw a
            // NullPointerException even though e.equals(null) returns false.
            // This is because e.compareTo(null) is not reflexive - you can't call null.compareTo(e).
            try {
                BreakNullSafety.INSTANCE.compareToNull(comp)
                Assert.fail("e.compareTo(null) should throw some kind of RuntimeException" +
                            " (NullPointer/IllegalArgument/IllegalState, etc.)" +
                            " even though e.equals(null) returns false." +
                            " Item " + i + " threw no exception!")
            } catch (ignore: RuntimeException) {
                // Previously we had allowed NullPointerException and IllegalArgumentException.
                // Kotlin throws IllegalStateException, so we now expect any RuntimeException
                // to be thrown.
            }
            Assert.assertNotEquals("item.equals(null) should always be false.  Item $i failed", null, comp)
        }
        pairComp(least, CompToZero.LTZ, middle)
        pairComp(least, CompToZero.LTZ, greatest)
        pairComp(middle, CompToZero.LTZ, greatest)
        pairComp(greatest, CompToZero.GTZ, middle)
        pairComp(greatest, CompToZero.GTZ, least)
        pairComp(middle, CompToZero.GTZ, least)
    }

    private class NamedPair>(
            val a: S,
            val b: S,
            val name: String
    )
}