diff --git a/libraries/stdlib/src/kotlin/time/Clocks.kt b/libraries/stdlib/src/kotlin/time/Clocks.kt index f9b06906935..7f48120c393 100644 --- a/libraries/stdlib/src/kotlin/time/Clocks.kt +++ b/libraries/stdlib/src/kotlin/time/Clocks.kt @@ -5,8 +5,6 @@ package kotlin.time -import kotlin.js.JsName - /** * The most precise clock available in the platform. * @@ -64,19 +62,52 @@ public abstract class AbstractDoubleClock(protected val unit: DurationUnit) : Cl /** * A clock that has programmatically updatable readings. It is useful as a predictable source of time in tests. * - * @param reading The initial value of the clock reading. - * @param unit The unit of time in which [reading] value is expressed. + * The current clock reading value can be advanced by the specified duration amount with the operator [plusAssign]: * - * @property reading Gets or sets this clock's current reading value. + * ``` + * val clock = TestClock() + * clock += 10.seconds + * ``` + * + * Implementation note: the current clock reading value is stored as a [Long] number of nanoseconds, + * thus it's capable to represent a time range of approximately ±292 years. + * Should the reading value overflow as the result of [plusAssign] operation, an [IllegalStateException] is thrown. */ @SinceKotlin("1.3") @ExperimentalTime -public class TestClock( - @JsName("readingValue") - public var reading: Long = 0L, - unit: DurationUnit = DurationUnit.NANOSECONDS -) : AbstractLongClock(unit) { +public class TestClock : AbstractLongClock(unit = DurationUnit.NANOSECONDS) { + private var reading: Long = 0L + override fun read(): Long = reading + + /** + * Advances the current reading value of this clock by the specified [duration]. + * + * [duration] value is rounded down towards zero when converting it to a [Long] number of nanoseconds. + * For example, if the duration being added is `0.6.nanoseconds`, the clock reading won't advance because + * the duration value will be rounded to zero nanoseconds. + * + * @throws IllegalStateException when the reading value overflows as the result of this operation. + */ + public operator fun plusAssign(duration: Duration) { + val delta = duration.toDouble(unit) + val longDelta = delta.toLong() + reading = if (longDelta != Long.MIN_VALUE && longDelta != Long.MAX_VALUE) { + // when delta fits in long, add it as long + val newReading = reading + longDelta + if (reading xor longDelta >= 0 && reading xor newReading < 0) overflow(duration) + newReading + } else { + // when delta is greater than long, add it as double + val newReading = reading + delta + if (newReading > Long.MAX_VALUE || newReading < Long.MIN_VALUE) overflow(duration) + newReading.toLong() + } + } + + private fun overflow(duration: Duration) { + throw IllegalStateException("TestClock will overflow if its reading ${reading}ns is advanced by $duration.") + } } /* diff --git a/libraries/stdlib/test/time/ClockMarkTest.kt b/libraries/stdlib/test/time/ClockMarkTest.kt index d3de90ed25a..40042b6736b 100644 --- a/libraries/stdlib/test/time/ClockMarkTest.kt +++ b/libraries/stdlib/test/time/ClockMarkTest.kt @@ -12,7 +12,7 @@ class ClockMarkTest { @Test fun adjustment() { - val clock = TestClock(unit = DurationUnit.NANOSECONDS) + val clock = TestClock() val mark = clock.mark() val markFuture1 = mark + 1.milliseconds @@ -21,7 +21,7 @@ class ClockMarkTest { val markPast1 = mark - 1.milliseconds val markPast2 = markFuture1 + (-2).milliseconds - clock.reading = 500_000L + clock += 500_000.nanoseconds val elapsed = mark.elapsed() val elapsedFromFuture = elapsed - 1.milliseconds diff --git a/libraries/stdlib/test/time/MeasureTimeTest.kt b/libraries/stdlib/test/time/MeasureTimeTest.kt index 9d85d783fba..26b9485bedd 100644 --- a/libraries/stdlib/test/time/MeasureTimeTest.kt +++ b/libraries/stdlib/test/time/MeasureTimeTest.kt @@ -47,11 +47,12 @@ class MeasureTimeTest { } - @Test fun measureTimeTestClock() { - val clock = TestClock(unit = DurationUnit.NANOSECONDS) + @Test + fun measureTimeTestClock() { + val clock = TestClock() val expectedNs = Random.nextLong(1_000_000_000L) val elapsed = clock.measureTime { - clock.reading += expectedNs + clock += expectedNs.nanoseconds } assertEquals(expectedNs.nanoseconds, elapsed) @@ -59,7 +60,7 @@ class MeasureTimeTest { val expectedResult: Long val (result, elapsed2) = clock.measureTimedValue { - clock.reading += expectedNs + clock += expectedNs.nanoseconds expectedResult = expectedNs expectedNs } diff --git a/libraries/stdlib/test/time/TestClockTest.kt b/libraries/stdlib/test/time/TestClockTest.kt new file mode 100644 index 00000000000..ec787787a77 --- /dev/null +++ b/libraries/stdlib/test/time/TestClockTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +@file:UseExperimental(ExperimentalTime::class) +package test.time + +import kotlin.test.* +import kotlin.time.* + +class TestClockTest { + + @Test + fun overflows() { + for (enormousDuration in listOf(Duration.INFINITE, Double.MAX_VALUE.nanoseconds, Long.MAX_VALUE.nanoseconds * 2)) { + assertFailsWith(enormousDuration.toString()) { TestClock() += enormousDuration } + assertFailsWith((-enormousDuration).toString()) { TestClock() += -enormousDuration } + } + + val moderatePositiveDuration = Long.MAX_VALUE.takeHighestOneBit().nanoseconds + val borderlinePositiveDuration = Long.MAX_VALUE.nanoseconds // rounded to 2.0^63, which is slightly more than Long.MAX_VALUE + val borderlineNegativeDuration = Long.MIN_VALUE.nanoseconds + run { + val clock = TestClock() + clock += moderatePositiveDuration + assertFailsWith("Should overflow positive") { clock += moderatePositiveDuration } + } + run { + val clock = TestClock() + clock += borderlinePositiveDuration + assertFailsWith("Should overflow positive") { clock += 1.nanoseconds } + } + run { + val clock = TestClock() + clock += borderlineNegativeDuration + assertFailsWith("Should overflow negative") { clock += -1.nanoseconds } + } + + run { + val clock = TestClock() + clock += moderatePositiveDuration + // does not overflow event if duration doesn't fit in long + clock += -moderatePositiveDuration + borderlineNegativeDuration + } + } + + @Test + fun nanosecondRounding() { + val clock = TestClock() + val mark = clock.mark() + + repeat(10_000) { + clock += 0.9.nanoseconds + + assertEquals(Duration.ZERO, mark.elapsed()) + } + + clock += 1.9.nanoseconds + assertEquals(1.nanoseconds, mark.elapsed()) + } +} \ No newline at end of file diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt index 9ed961c6187..970f12308e2 100644 --- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt +++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt @@ -5390,10 +5390,7 @@ public final class kotlin/time/MonoClock : kotlin/time/AbstractLongClock, kotlin public final class kotlin/time/TestClock : kotlin/time/AbstractLongClock { public fun ()V - public fun (JLjava/util/concurrent/TimeUnit;)V - public synthetic fun (JLjava/util/concurrent/TimeUnit;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getReading ()J - public final fun setReading (J)V + public final fun plusAssign-LRDsOJo (D)V } public final class kotlin/time/TimedValue {