From d520c37454e46f56e0af3013ae1da906bee40c6a Mon Sep 17 00:00:00 2001 From: Renee Vandervelde Date: Sat, 18 Jul 2020 11:39:53 -0500 Subject: [PATCH] Add assertEquals and assertNotEquals for floating point numbers to kotlin-test #KT-8364 --- .../src/main/kotlin/kotlin/test/Assertions.kt | 24 ++++ .../src/main/kotlin/kotlin/test/Utils.kt | 42 ++++++- .../kotlin/test/tests/BasicAssertionsTest.kt | 113 ++++++++++++++++++ 3 files changed, 178 insertions(+), 1 deletion(-) diff --git a/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Assertions.kt b/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Assertions.kt index 529935a8e11..32212c68c9f 100644 --- a/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Assertions.kt +++ b/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Assertions.kt @@ -63,11 +63,35 @@ fun <@OnlyInputTypes T> assertEquals(expected: T, actual: T, message: String? = asserter.assertEquals(message, expected, actual) } +/** Asserts that the difference between the [actual] and the [expected] is within an [absoluteTolerance], with an optional [message]. */ +@SinceKotlin("1.5") +fun assertEquals(expected: Double, actual: Double, absoluteTolerance: Double, message: String? = null) { + checkDoublesAreEqual(expected, actual, absoluteTolerance, message) +} + +/** Asserts that the difference between the [actual] and the [expected] is within an [absoluteTolerance], with an optional [message]. */ +@SinceKotlin("1.5") +fun assertEquals(expected: Float, actual: Float, absoluteTolerance: Float, message: String? = null) { + checkFloatsAreEqual(expected, actual, absoluteTolerance, message) +} + /** Asserts that the [actual] value is not equal to the illegal value, with an optional [message]. */ fun <@OnlyInputTypes T> assertNotEquals(illegal: T, actual: T, message: String? = null) { asserter.assertNotEquals(message, illegal, actual) } +/** Asserts that the difference between the [actual] and the [illegal] is not within an [absoluteTolerance], with an optional [message]. */ +@SinceKotlin("1.5") +fun assertNotEquals(illegal: Double, actual: Double, absoluteTolerance: Double, message: String? = null) { + checkDoublesAreEqual(illegal, actual, absoluteTolerance, message, shouldFail = true) +} + +/** Asserts that the difference between the [actual] and the [illegal] is not within an [absoluteTolerance], with an optional [message]. */ +@SinceKotlin("1.5") +fun assertNotEquals(illegal: Float, actual: Float, absoluteTolerance: Float, message: String? = null) { + checkFloatsAreEqual(illegal, actual, absoluteTolerance, message, shouldFail = true) +} + /** Asserts that [expected] is the same instance as [actual], with an optional [message]. */ fun <@OnlyInputTypes T> assertSame(expected: T, actual: T, message: String? = null) { asserter.assertSame(message, expected, actual) diff --git a/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Utils.kt b/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Utils.kt index a022582f870..c787e08e9ca 100644 --- a/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Utils.kt +++ b/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Utils.kt @@ -5,8 +5,48 @@ package kotlin.test +import kotlin.math.abs + internal fun messagePrefix(message: String?) = if (message == null) "" else "$message. " internal expect fun lookupAsserter(): Asserter @PublishedApi // required to get stable name as it's called from box tests -internal fun overrideAsserter(value: Asserter?): Asserter? = _asserter.also { _asserter = value } \ No newline at end of file +internal fun overrideAsserter(value: Asserter?): Asserter? = _asserter.also { _asserter = value } + + +private fun checkAbsoluteTolerance(absoluteTolerance: Double) { + require(absoluteTolerance >= 0.0) { "Illegal negative absolute tolerance <$absoluteTolerance>." } + require(!absoluteTolerance.isNaN()) { "Illegal NaN absolute tolerance <$absoluteTolerance>." } +} + +internal fun checkDoublesAreEqual( + expected: Double, + actual: Double, + absoluteTolerance: Double, + message: String?, + shouldFail: Boolean = false +) { + checkAbsoluteTolerance(absoluteTolerance) + val equal = expected.toBits() == actual.toBits() || abs(expected - actual) <= absoluteTolerance + + asserter.assertTrue( + { messagePrefix(message) + "Expected <$expected> with absolute tolerance <$absoluteTolerance>, actual <$actual>." }, + equal != shouldFail + ) +} + +internal fun checkFloatsAreEqual( + expected: Float, + actual: Float, + absoluteTolerance: Float, + message: String?, + shouldFail: Boolean = false +) { + checkAbsoluteTolerance(absoluteTolerance.toDouble()) + val equal = expected.toBits() == actual.toBits() || abs(expected - actual) <= absoluteTolerance + + asserter.assertTrue( + { messagePrefix(message) + "Expected <$expected> with absolute tolerance <$absoluteTolerance>, actual <$actual>." }, + equal != shouldFail + ) +} \ No newline at end of file diff --git a/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/BasicAssertionsTest.kt b/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/BasicAssertionsTest.kt index 730aab377f9..8e2ff76ab86 100644 --- a/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/BasicAssertionsTest.kt +++ b/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/BasicAssertionsTest.kt @@ -88,6 +88,119 @@ class BasicAssertionsTest { checkFailedAssertion { assertSame(instance1, instance2) } } + @Test + fun testAssertEqualsDouble() { + assertEquals(0.01, 0.02, .01) + + // 0.0, -0.0 + assertEquals(0.0, 0.0, 0.0) + assertEquals(0.0, -0.0, 0.0) + assertEquals(-0.0, 0.0, 0.0) + assertEquals(0.0, 0.0, -0.0) + assertEquals(0.0, -0.0, -0.0) + assertEquals(-0.0, 0.0, -0.0) + + // NaN + val nans = doubleArrayOf(Double.NaN, Double.fromBits(0xFFF80000L shl 32)) + for (nan1 in nans) { + assertTrue(nan1.isNaN()) + for (nan2 in nans) { + assertEquals(nan1, nan2, 0.1) + assertEquals(nan1, nan2, 0.0) + } + } + + // MIN_VALUE, MAX_VALUE + assertEquals(Double.MAX_VALUE, Double.MAX_VALUE, 0.0) + assertEquals(Double.MIN_VALUE, Double.MIN_VALUE, 0.0) + assertEquals(Double.MAX_VALUE, Double.MIN_VALUE, Double.MAX_VALUE) + assertEquals(Double.MIN_VALUE, Double.MAX_VALUE, Double.MAX_VALUE) + + // POSITIVE_INFINITY, NEGATIVE_INFINITY + assertEquals(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, 0.0) + assertEquals(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, 0.0) + assertEquals(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY) + assertEquals(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY) + } + + @Test + fun testAssertEqualsDoubleFails() { + checkFailedAssertion { assertEquals(0.01, 1.03, .01) } + + // negative absoluteTolerance + assertFailsWith { assertEquals(0.01, 1.03, -5.0) } + + } + + @Test + fun testAssertEqualsFloat() { + assertEquals(0.01f, 0.02f, .01f) + + // 0.0, -0.0 + assertEquals(0.0f, 0.0f, 0.0f) + assertEquals(0.0f, -0.0f, 0.0f) + assertEquals(-0.0f, 0.0f, 0.0f) + assertEquals(0.0f, 0.0f, -0.0f) + assertEquals(0.0f, -0.0f, -0.0f) + assertEquals(-0.0f, 0.0f, -0.0f) + + // NaN + val nans = floatArrayOf(Float.NaN, Float.fromBits(0xFFC00000.toInt())) + for (nan1 in nans) { + assertTrue(nan1.isNaN()) + for (nan2 in nans) { + assertEquals(nan1, nan2, 0.1f) + assertEquals(nan1, nan2, 0.0f) + } + } + + // MIN_VALUE, MAX_VALUE + assertEquals(Float.MAX_VALUE, Float.MAX_VALUE, 0.0f) + assertEquals(Float.MIN_VALUE, Float.MIN_VALUE, 0.0f) + assertEquals(Float.MAX_VALUE, Float.MIN_VALUE, Float.MAX_VALUE) + assertEquals(Float.MIN_VALUE, Float.MAX_VALUE, Float.MAX_VALUE) + + // POSITIVE_INFINITY, NEGATIVE_INFINITY + assertEquals(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, 0.0f) + assertEquals(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, 0.0f) + assertEquals(Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY) + assertEquals(Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY) + } + + @Test + fun testAssertEqualsFloatFails() { + checkFailedAssertion { assertEquals(0.01f, 1.03f, .01f) } + + // negative absoluteTolerance + assertFailsWith { assertEquals(0.01f, 1.03f, -5.0f) } + } + + @Test + fun testAssertNotEqualsDouble() { + assertNotEquals(0.1, 0.3, 0.1) + } + + @Test + fun testAssertNotEqualsDoubleFails() { + checkFailedAssertion { assertNotEquals(0.1, 0.11, 0.1) } + + // negative absoluteTolerance + assertFailsWith { assertNotEquals(0.1, 0.11, -0.001) } + } + + @Test + fun testAssertNotEqualsFloat() { + assertNotEquals(0.1f, 0.3f, 0.1f) + } + + @Test + fun testAssertNotEqualsFloatFails() { + checkFailedAssertion { assertNotEquals(0.1f, 0.11f, .1f) } + + // negative absoluteTolerance + assertFailsWith { assertNotEquals(0.1f, 0.11f, -0.001f) } + } + @Test fun testAssertTrue() { assertTrue(true)