/* * Copyright 2010-2022 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. */ package test.time import test.TestPlatform import test.current import kotlin.math.sign import kotlin.test.* import kotlin.time.* import kotlin.time.Duration.Companion.microseconds import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.seconds class TimeMarkTest { private val units = DurationUnit.entries private fun TimeMark.assertHasPassed(hasPassed: Boolean) { assertEquals(!hasPassed, this.hasNotPassedNow(), "Expected mark in the future") assertEquals(hasPassed, this.hasPassedNow(), "Expected mark in the past") assertEquals( !hasPassed, this.elapsedNow() < Duration.ZERO, "Mark elapsed: ${this.elapsedNow()}, expected hasPassed: $hasPassed" ) } fun testAdjustment(timeSource: TimeSource.WithComparableMarks) { val mark = timeSource.markNow() for (unit in units) { val markFuture1 = (mark + 1.toDuration(unit)).apply { assertHasPassed(false) } val markFuture2 = (mark - (-1).toDuration(unit)).apply { assertHasPassed(false) } assertDifferentMarks(markFuture1, mark, 1) assertDifferentMarks(markFuture2, mark, 1) val markPast1 = (mark - 1.toDuration(unit)).apply { assertHasPassed(true) } val markPast2 = (markFuture1 + (-2).toDuration(unit)).apply { assertHasPassed(true) } assertDifferentMarks(markPast1, mark, -1) assertDifferentMarks(markPast2, mark, -1) if (unit > DurationUnit.NANOSECONDS) { val d = 1.toDuration(unit) val h = d / 2 val markH1 = mark + h val markH2 = mark + d - h assertEqualMarks(markH1, markH2) } } } @Test fun adjustment() { testAdjustment(TestTimeSource()) for (unit in units) { testAdjustment(LongTimeSource(unit)) } } @Test fun adjustmentTestTimeSource() { val timeSource = TestTimeSource() val mark = timeSource.markNow() val markFuture1 = mark + 1.milliseconds val markPast1 = mark - 1.milliseconds timeSource += 500_000.nanoseconds val markElapsed = timeSource.markNow() val elapsedDiff = markElapsed - mark val elapsed = mark.elapsedNow() val elapsedFromFuture = elapsed - 1.milliseconds val elapsedFromPast = elapsed + 1.milliseconds assertEquals(0.5.milliseconds, elapsed) assertEquals(elapsedFromFuture, markFuture1.elapsedNow()) assertEquals(elapsedDiff, elapsed) val markToElapsed = mark + elapsedDiff assertEqualMarks(markElapsed, markToElapsed) assertEquals(elapsedFromPast, markPast1.elapsedNow()) markFuture1.assertHasPassed(false) markPast1.assertHasPassed(true) timeSource += 1.milliseconds markFuture1.assertHasPassed(true) markPast1.assertHasPassed(true) } fun testAdjustmentBig(timeSource: TimeSource.WithComparableMarks) { val baseMark = timeSource.markNow() val longDuration = Long.MAX_VALUE.nanoseconds val long2Duration = longDuration + 1001.milliseconds val pastMark = baseMark - longDuration val futureMark = pastMark + long2Duration val sameMark = futureMark - (long2Duration - longDuration) val elapsedMark = timeSource.markNow() run { val iterations = 1..100 for (i in iterations) { val elapsedDiff1 = (sameMark.elapsedNow() - baseMark.elapsedNow()).absoluteValue val elapsedDiff2 = (baseMark.elapsedNow() - sameMark.elapsedNow()).absoluteValue // some iterations of this assertion can fail due to an unpredictable delay between subsequent elapsedNow calls // but if the mark adjustment arithmetic was wrong, all of them will fail if (maxOf(elapsedDiff1, elapsedDiff2) < 1.milliseconds) break if (i == iterations.last) fail("$elapsedDiff1, $elapsedDiff2") } } // may not pass exactly for double-based value time marks in JS/WASM due to rounding // assertEquals(elapsedMark - baseMark, elapsedMark - sameMark, "$elapsedMark; $baseMark; $sameMark") val elapsedBaseDiff = elapsedMark - baseMark val elapsedSameDiff = elapsedMark - sameMark assertTrue((elapsedBaseDiff - elapsedSameDiff).absoluteValue < 1.milliseconds, "elapsedMark=$elapsedMark; baseMark=$baseMark; sameMark=$sameMark") } @Test fun adjustmentBig() { testAdjustmentBig(TestTimeSource()) for (unit in units) { testAdjustmentBig(LongTimeSource(unit)) } } fun testAdjustmentInfinite(timeSource: TimeSource.WithComparableMarks) { val baseMark = timeSource.markNow() val infiniteFutureMark = baseMark + Duration.INFINITE val infinitePastMark = baseMark - Duration.INFINITE assertDifferentMarks(infinitePastMark, baseMark, -1) assertDifferentMarks(infiniteFutureMark, baseMark, 1) assertDifferentMarks(infinitePastMark, infiniteFutureMark, -1) assertEquals(Duration.INFINITE, infiniteFutureMark - infinitePastMark) assertEquals(Duration.INFINITE, infiniteFutureMark - baseMark) assertEquals(-Duration.INFINITE, infinitePastMark - baseMark) assertEqualMarks(infiniteFutureMark, infiniteFutureMark) assertEqualMarks(infinitePastMark, infinitePastMark) assertEquals(-Duration.INFINITE, infiniteFutureMark.elapsedNow()) assertTrue(infiniteFutureMark.hasNotPassedNow()) assertEquals(Duration.INFINITE, infinitePastMark.elapsedNow()) assertTrue(infinitePastMark.hasPassedNow()) assertFailsWith { infiniteFutureMark - Duration.INFINITE } assertFailsWith { infinitePastMark + Duration.INFINITE } for (infiniteMark in listOf(infiniteFutureMark, infinitePastMark)) { for (offset in listOf(Duration.ZERO, 1.nanoseconds, 10.microseconds, 1.milliseconds, 15.seconds)) { assertEqualMarks(infiniteMark, infiniteMark + offset) assertEqualMarks(infiniteMark, infiniteMark - offset) } } } @Test fun adjustmentInfinite() { testAdjustmentInfinite(TestTimeSource()) for (unit in units) { testAdjustmentInfinite(LongTimeSource(unit)) } } fun testLongAdjustmentElapsedPrecision(timeSource: TimeSource.WithComparableMarks, wait: (Duration) -> Unit) { val baseMark = timeSource.markNow() val longDuration = Long.MAX_VALUE.nanoseconds val waitDuration = 20.milliseconds val pastMark = baseMark - longDuration wait(waitDuration) val elapsedMark = timeSource.markNow() val elapsed = pastMark.elapsedNow() val elapsedDiff = elapsedMark - pastMark assertTrue(elapsed > longDuration) assertTrue(elapsed >= longDuration + waitDuration, "$elapsed, $longDuration, $waitDuration") assertTrue(elapsedDiff >= longDuration + waitDuration) // 'elapsed' was measured later than time marks from 'elapsedDiff' assertTrue(elapsed >= elapsedDiff) } @Test fun longDisplacement() { val timeSource = TestTimeSource() testLongAdjustmentElapsedPrecision(timeSource, { waitDuration -> timeSource += waitDuration }) } private fun assertEqualMarks(mark1: ComparableTimeMark, mark2: ComparableTimeMark) { assertEquals(Duration.ZERO, mark1 - mark2) assertEquals(Duration.ZERO, mark2 - mark1) assertEquals(0, mark1 compareTo mark2) assertEquals(0, mark2 compareTo mark1) assertEquals(mark1, mark2) assertEquals(mark1.hashCode(), mark2.hashCode(), "hashCodes of: $mark1, $mark2") } private fun assertDifferentMarks(mark1: ComparableTimeMark, mark2: ComparableTimeMark, expectedCompare: Int) { assertNotEquals(Duration.ZERO, mark1 - mark2) assertNotEquals(Duration.ZERO, mark2 - mark1) assertEquals(expectedCompare, (mark1 compareTo mark2).sign) assertEquals(-expectedCompare, (mark2 compareTo mark1).sign) assertNotEquals(mark1, mark2) // can't say anything about hash codes for non-equal marks // assertNotEquals(mark1.hashCode(), mark2.hashCode(), "hashCodes of: $mark1, $mark2") } @Test fun timeMarkDifferenceAndComparison() { val timeSource = TestTimeSource() val timeSource2 = TestTimeSource() val baseMark = timeSource.markNow() var markBefore = baseMark markBefore -= 100.microseconds markBefore -= 100.microseconds val markAfter = baseMark + 100.microseconds assertEquals(300.microseconds,markAfter - markBefore) assertTrue(markBefore < markAfter) assertFalse(markBefore > markAfter) assertEqualMarks(baseMark, baseMark) timeSource += 100.microseconds val markElapsed = timeSource.markNow() assertEqualMarks(markElapsed, markAfter) val differentSourceMark = TimeSource.Monotonic.markNow() assertFailsWith { baseMark - differentSourceMark } assertFailsWith { baseMark < differentSourceMark } val differentSourceMark2 = timeSource2.markNow() assertFailsWith { baseMark - differentSourceMark2 } assertFailsWith { baseMark < differentSourceMark2 } } private class LongTimeSource(unit: DurationUnit) : AbstractLongTimeSource(unit) { var reading: Long = 0L override fun read(): Long = reading } @OptIn(ExperimentalTime::class) @Suppress("DEPRECATION") private class DoubleTimeSource(unit: DurationUnit) : AbstractDoubleTimeSource(unit) { var reading: Double = 0.0 override fun read(): Double = reading } @Test fun longTimeMarkInfinities() { for (unit in units) { val timeSource = LongTimeSource(unit).apply { markNow() // fix zero reading reading = Long.MIN_VALUE + 1 } val mark1 = timeSource.markNow() timeSource.reading = 0 val mark2 = timeSource.markNow() - Duration.INFINITE if (unit >= DurationUnit.MILLISECONDS) { assertEquals(Duration.INFINITE, mark1.elapsedNow()) } assertEquals(Duration.INFINITE, mark2.elapsedNow()) assertDifferentMarks(mark1, mark2, 1) val mark3 = mark1 + Duration.INFINITE assertEquals(-Duration.INFINITE, mark3.elapsedNow(), "infinite offset should override distant past reading") val mark4 = timeSource.markNow() + Duration.INFINITE assertEquals(-Duration.INFINITE, mark4.elapsedNow()) assertEqualMarks(mark3, mark4) // different readings, same infinite offset } } @Test fun doubleTimeMarkInfiniteEqualHashCode() { val timeSource = DoubleTimeSource(unit = DurationUnit.MILLISECONDS).apply { reading = -Double.MAX_VALUE } val mark1 = timeSource.markNow() timeSource.reading = 0.0 val mark2 = timeSource.markNow() - Duration.INFINITE assertEquals(Duration.INFINITE, mark1.elapsedNow()) assertEquals(Duration.INFINITE, mark2.elapsedNow()) assertEqualMarks(mark1, mark2) } @Test fun longTimeMarkRoundingEqualHashCode() { run { val step = Long.MAX_VALUE / 4 val timeSource = LongTimeSource(DurationUnit.NANOSECONDS) val mark0 = timeSource.markNow() + step.nanoseconds + step.nanoseconds timeSource.reading += step val mark1 = timeSource.markNow() + step.nanoseconds timeSource.reading += step val mark2 = timeSource.markNow() assertEqualMarks(mark1, mark2) assertEqualMarks(mark0, mark2) assertEqualMarks(mark0, mark1) } for (unit in units) { val baseReading = Long.MAX_VALUE - 1000 val timeSource = LongTimeSource(unit).apply { reading = baseReading } // large reading, small offset val baseMark = timeSource.markNow() for (delta in listOf((1..<500).random(), (500..<1000).random())) { val deltaDuration = delta.toDuration(unit) timeSource.reading = baseReading + delta val mark1e = timeSource.markNow() assertEquals(deltaDuration, mark1e - baseMark) val mark1d = baseMark + deltaDuration assertEqualMarks(mark1e, mark1d) val subUnit = units.getOrNull(units.indexOf(unit) - 1) ?: continue val deltaSubUnitDuration = delta.toDuration(subUnit) val mark1s = baseMark + deltaSubUnitDuration assertDifferentMarks(mark1s, baseMark, 1) assertEquals(deltaSubUnitDuration, mark1s - baseMark) } // compared saturated reading from time source and saturated time mark as a result of plus operation run { val delta = 1000 val deltaDuration = delta.toDuration(unit) timeSource.reading = baseReading + 1000 val mark2 = timeSource.markNow() assertEquals(deltaDuration, mark2 - baseMark) val offset = Long.MAX_VALUE.nanoseconds val mark2e = mark2 + offset val mark2d = baseMark + offset + deltaDuration assertEqualMarks(mark2e, mark2d) } } } @Test fun defaultTimeMarkAdjustment() { val baseMark = TimeSource.Monotonic.markNow() var markBefore = baseMark markBefore -= 100.microseconds markBefore -= 100.microseconds val markAfter = baseMark + 100.microseconds MeasureTimeTest.longRunningCalc() val elapsedMark = TimeSource.Monotonic.markNow() val elapsedDiff = elapsedMark - baseMark assertTrue(elapsedDiff > Duration.ZERO) val elapsedAfter = markAfter.elapsedNow() val elapsedBase = baseMark.elapsedNow() val elapsedBefore = markBefore.elapsedNow() assertTrue(elapsedBefore >= elapsedBase + 200.microseconds) assertTrue(elapsedAfter <= elapsedBase - 100.microseconds) assertTrue(elapsedBase >= elapsedDiff) } @Test fun defaultTimeMarkAdjustmentBig() { if (TestPlatform.current == TestPlatform.WasmWasi) return testAdjustmentBig(TimeSource.Monotonic) // do the same with specialized methods val baseMark = TimeSource.Monotonic.markNow() val longDuration = Long.MAX_VALUE.nanoseconds val long2Duration = longDuration + 1001.milliseconds val pastMark = baseMark - longDuration val futureMark = pastMark + long2Duration val sameMark = futureMark - (long2Duration - longDuration) run { val iterations = 1..100 for (i in iterations) { val elapsedDiff1 = (sameMark.elapsedNow() - baseMark.elapsedNow()).absoluteValue val elapsedDiff2 = (baseMark.elapsedNow() - sameMark.elapsedNow()).absoluteValue // some iterations of this assertion can fail due to an unpredictable delay between subsequent elapsedNow calls // but if the mark adjustment arithmetic was wrong, all of them will fail if (maxOf(elapsedDiff1, elapsedDiff2) < 1.milliseconds) break if (i == iterations.last) fail("$elapsedDiff1, $elapsedDiff2") } } val elapsedMark = TimeSource.Monotonic.markNow() val elapsedBaseDiff = elapsedMark - baseMark val elapsedSameDiff = elapsedMark - sameMark assertTrue((elapsedBaseDiff - elapsedSameDiff).absoluteValue < 1.milliseconds, "elapsedMark=$elapsedMark; baseMark=$baseMark; sameMark=$sameMark") } @Test fun defaultTimeMarkAdjustmentInfinite() { if (TestPlatform.current == TestPlatform.WasmWasi) return testAdjustmentInfinite(TimeSource.Monotonic) // do the same with specialized methods val baseMark = TimeSource.Monotonic.markNow() val infiniteFutureMark = baseMark + Duration.INFINITE val infinitePastMark = baseMark - Duration.INFINITE assertEquals(-Duration.INFINITE, infiniteFutureMark.elapsedNow()) assertTrue(infiniteFutureMark.hasNotPassedNow()) assertEquals(Duration.INFINITE, infinitePastMark.elapsedNow()) assertTrue(infinitePastMark.hasPassedNow()) assertFailsWith { infiniteFutureMark - Duration.INFINITE } assertFailsWith { infinitePastMark + Duration.INFINITE } } @Test fun defaultTimeMarkDifferenceAndComparison() { val baseMark = TimeSource.Monotonic.markNow() var markBefore = baseMark markBefore -= 100.microseconds markBefore -= 100.microseconds val markAfter = baseMark + 100.microseconds assertEquals(300.microseconds,markAfter - markBefore) assertTrue(markBefore < markAfter) assertFalse(markBefore > markAfter) assertEquals(0,baseMark compareTo baseMark) assertEquals(baseMark as Any, baseMark as Any) assertEquals(baseMark.hashCode(), baseMark.hashCode()) val differentSourceMark = TestTimeSource().markNow() assertFailsWith { baseMark - differentSourceMark } assertFailsWith { baseMark < differentSourceMark } } }