-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Close #602 - [effectie-time] Add TimeSource
- Loading branch information
Showing
4 changed files
with
396 additions
and
0 deletions.
There are no files selected for viewing
94 changes: 94 additions & 0 deletions
94
modules/effectie-time/shared/src/main/scala/effectie/time/TimeSource.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package effectie.time | ||
|
||
import cats._ | ||
import cats.syntax.all._ | ||
import effectie.core._ | ||
import effectie.syntax.all._ | ||
import effectie.time.TimeSource.TimeSpent | ||
|
||
import java.time.Instant | ||
import scala.concurrent.duration.{FiniteDuration, MILLISECONDS, NANOSECONDS, SECONDS, TimeUnit} | ||
|
||
/** @author Kevin Lee | ||
*/ | ||
trait TimeSource[F[*]] { | ||
|
||
implicit protected def M: Monad[F] | ||
|
||
def name: String | ||
|
||
def currentTime(): F[Instant] | ||
|
||
def realTimeTo(unit: TimeUnit): F[FiniteDuration] | ||
|
||
def monotonicTo(unit: TimeUnit): F[FiniteDuration] | ||
|
||
def realTime: F[FiniteDuration] | ||
|
||
def monotonic: F[FiniteDuration] | ||
|
||
def timeSpent[A](fa: => F[A]): F[(A, TimeSpent)] = | ||
for { | ||
start <- monotonic | ||
a <- fa | ||
end <- monotonic | ||
} yield (a, TimeSpent(end - start)) | ||
|
||
override def toString: String = s"TimeSource(name=$name)" | ||
} | ||
|
||
object TimeSource { | ||
|
||
def apply[F[*]: TimeSource]: TimeSource[F] = implicitly[TimeSource[F]] | ||
|
||
implicit def showTimeSource[F[*]]: Show[TimeSource[F]] = Show.fromToString | ||
|
||
def withSources[F[*]: Fx: Monad]( | ||
timeSourceName: String, | ||
realTimeSource: F[Instant], | ||
monotonicSource: F[Long], | ||
): TimeSource[F] = | ||
new DefaultTimeSource[F](timeSourceName, realTimeSource, monotonicSource) | ||
|
||
private class DefaultTimeSource[F[_]: Fx]( | ||
timeSourceName: String, | ||
realTimeSource: F[Instant], | ||
monotonicSource: F[Long], | ||
)(implicit protected val M: Monad[F]) | ||
extends TimeSource[F] { | ||
|
||
override val name: String = timeSourceName | ||
|
||
override val toString: String = s"DefaultTimeSource(name=$name)" | ||
|
||
override def currentTime(): F[Instant] = realTimeSource | ||
|
||
override def realTimeTo(unit: TimeUnit): F[FiniteDuration] = | ||
for { | ||
now <- currentTime() | ||
real <- effectOf(unit.convert(now.getEpochSecond, SECONDS) + unit.convert(now.getNano.toLong, NANOSECONDS)) | ||
} yield FiniteDuration(real, unit) | ||
|
||
override def monotonicTo(unit: TimeUnit): F[FiniteDuration] = | ||
monotonicSource.map(nano => FiniteDuration(unit.convert(nano, NANOSECONDS), unit)) | ||
|
||
override def realTime: F[FiniteDuration] = realTimeTo(MILLISECONDS) | ||
|
||
override def monotonic: F[FiniteDuration] = monotonicTo(NANOSECONDS) | ||
} | ||
|
||
/** The default behaviours of TimeSource depend on Instant and System. | ||
* For realTime, it uses Instant.now() to get the epoch seconds and nano seconds. | ||
* For monotonic, it uses System.nanoTime() to get the nano seconds. | ||
* | ||
* NOTE: To get the current time (Instant.now()), you should use realTime not monotonic. | ||
*/ | ||
def default[F[*]: Fx: Monad](timeSourceName: String): TimeSource[F] = | ||
withSources( | ||
timeSourceName, | ||
effectOf(Instant.now()), | ||
effectOf(System.nanoTime()), | ||
) | ||
|
||
final case class TimeSpent(timeSpent: FiniteDuration) extends AnyVal | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
231 changes: 231 additions & 0 deletions
231
modules/effectie-time/shared/src/test/scala/effectie/time/TimeSourceSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
package effectie.time | ||
|
||
import cats.syntax.all._ | ||
import cats.effect._ | ||
import effectie.syntax.all._ | ||
import hedgehog._ | ||
import hedgehog.runner._ | ||
|
||
import java.time.Instant | ||
import scala.concurrent.duration._ | ||
import effectie.time.syntax._ | ||
|
||
object TimeSourceSpec extends Properties { | ||
import effectie.instances.ce2.fx.ioFx | ||
|
||
type F[A] = IO[A] | ||
val F = IO | ||
|
||
implicit val timer: Timer[F] = F.timer(scala.concurrent.ExecutionContext.global) | ||
|
||
override def tests: List[Test] = List( | ||
example("example test realTime and monotonic", testRealTimeAndMonotonicExample), | ||
property("property test realTime and monotonic", testRealTimeAndMonotonicProperty), | ||
example("test default TimeSource - realTime", testDefaultTimeSourceRealTime), | ||
example("test default TimeSource - monotonic", testDefaultTimeSourceMonotonic), | ||
property("test default TimeSource - timeSpent", testDefaultTimeSourceTimeSpent).withTests(count = 5), | ||
) | ||
|
||
private def runIO(test: F[Result]): Result = test.unsafeRunSync() | ||
|
||
def testRealTimeAndMonotonicExample: Result = runIO { | ||
val epochSeconds = 123456789L | ||
val nanoSeconds = 123456789L | ||
val fullNanos = epochSeconds * 1000000000L + nanoSeconds | ||
|
||
val expectedMillis = fullNanos / 1000000L | ||
val expectedNanos = fullNanos | ||
|
||
val timeSource = TimeSource.withSources[F]( | ||
"Test", | ||
pureOf(Instant.ofEpochSecond(epochSeconds, nanoSeconds)), | ||
pureOf(fullNanos), | ||
) | ||
|
||
import scala.concurrent.duration._ | ||
List( | ||
timeSource | ||
.realTime | ||
.map(time => (time ==== expectedMillis.milliseconds).log("timeSource.realTime")), | ||
timeSource | ||
.realTimeTo(MILLISECONDS) | ||
.map(time => (time ==== expectedMillis.milliseconds).log("timeSource.realTimeTo(MILLISECONDS)")), | ||
timeSource | ||
.realTimeTo(NANOSECONDS) | ||
.map(time => (time ==== expectedNanos.nanoseconds).log("timeSource.realTimeTo(NANOSECONDS)")), | ||
timeSource | ||
.monotonic | ||
.map(time => (time ==== expectedNanos.nanoseconds).log("timeSource.monotonic")), | ||
timeSource | ||
.monotonicTo(MILLISECONDS) | ||
.map(time => (time ==== expectedMillis.milliseconds).log("timeSource.monotonicTo(MILLISECONDS)")), | ||
timeSource | ||
.monotonicTo(NANOSECONDS) | ||
.map(time => (time ==== expectedNanos.nanoseconds).log("timeSource.monotonicTo(NANOSECONDS)")), | ||
).sequence | ||
.map(Result.all) | ||
} | ||
|
||
def testRealTimeAndMonotonicProperty: Property = { | ||
for { | ||
epochSeconds <- Gen.long(Range.linear(1L, 999999999L)).log("epochSeconds") | ||
nanoSeconds <- Gen.long(Range.linear(1L, 999999999L)).log("nanoSeconds") | ||
fullNanos <- Gen.constant(epochSeconds * 1000000000L + nanoSeconds).log("fullNanos") | ||
|
||
} yield runIO { | ||
val expectedMillis = fullNanos / 1000000L | ||
val expectedNanos = fullNanos | ||
|
||
val timeSource = TimeSource.withSources[F]( | ||
"Test", | ||
pureOf(Instant.ofEpochSecond(epochSeconds, nanoSeconds)), | ||
pureOf(fullNanos), | ||
) | ||
|
||
import scala.concurrent.duration._ | ||
List( | ||
timeSource | ||
.realTime | ||
.map(time => (time ==== expectedMillis.milliseconds).log("timeSource.realTime")), | ||
timeSource | ||
.realTimeTo(MILLISECONDS) | ||
.map(time => (time ==== expectedMillis.milliseconds).log("timeSource.realTimeTo(MILLISECONDS)")), | ||
timeSource | ||
.realTimeTo(NANOSECONDS) | ||
.map(time => (time ==== expectedNanos.nanoseconds).log("timeSource.realTimeTo(NANOSECONDS)")), | ||
timeSource | ||
.monotonic | ||
.map(time => (time ==== expectedNanos.nanoseconds).log("timeSource.monotonic")), | ||
timeSource | ||
.monotonicTo(MILLISECONDS) | ||
.map(time => (time ==== expectedMillis.milliseconds).log("timeSource.monotonicTo(MILLISECONDS)")), | ||
timeSource | ||
.monotonicTo(NANOSECONDS) | ||
.map(time => (time ==== expectedNanos.nanoseconds).log("timeSource.monotonicTo(NANOSECONDS)")), | ||
).sequence | ||
.map(Result.all) | ||
} | ||
} | ||
|
||
def testDefaultTimeSourceRealTime: Result = runIO { | ||
|
||
val now = Instant.now() | ||
val expectedNanos = now.getEpochSecond * 1000000000L + now.getNano | ||
val expectedMillis = expectedNanos / 1000000L | ||
|
||
val timeSource = TimeSource.default[F]( | ||
"Test" | ||
) | ||
|
||
List( | ||
timeSource | ||
.realTime | ||
.map(time => | ||
Result | ||
.diff(time, (expectedMillis.milliseconds +- 1000.milliseconds))(_.isWithIn(_)) | ||
.log("timeSource.realTime"), | ||
), | ||
timeSource | ||
.realTimeTo(MILLISECONDS) | ||
.map(time => | ||
Result | ||
.diff(time, (expectedMillis.milliseconds +- 1000.milliseconds))(_.isWithIn(_)) | ||
.log("timeSource.realTimeTo(MILLISECONDS)"), | ||
), | ||
timeSource | ||
.realTimeTo(NANOSECONDS) | ||
.map(time => | ||
Result | ||
.diff(time, (expectedNanos.nanoseconds +- 1000.milliseconds))(_.isWithIn(_)) | ||
.log("timeSource.realTimeTo(NANOSECONDS)"), | ||
), | ||
).sequence | ||
.map(Result.all) | ||
} | ||
|
||
def testDefaultTimeSourceMonotonic: Result = runIO { | ||
|
||
val expectedNanos = System.nanoTime() | ||
val expectedMillis = expectedNanos / 1000000L | ||
|
||
val timeSource = TimeSource.default[F]( | ||
"Test" | ||
) | ||
|
||
List( | ||
timeSource | ||
.monotonic | ||
.map(time => | ||
Result | ||
.diff(time, (expectedNanos.nanoseconds +- 1000.milliseconds))(_.isWithIn(_)) | ||
.log("timeSource.monotonic"), | ||
), | ||
timeSource | ||
.monotonicTo(MILLISECONDS) | ||
.map(time => | ||
Result | ||
.diff(time, (expectedMillis.milliseconds +- 1000.milliseconds))(_.isWithIn(_)) | ||
.log("timeSource.monotonicTo(MILLISECONDS)"), | ||
), | ||
timeSource | ||
.monotonicTo(NANOSECONDS) | ||
.map(time => | ||
Result | ||
.diff(time, (expectedNanos.nanoseconds +- 1000.milliseconds))(_.isWithIn(_)) | ||
.log("timeSource.monotonicTo(NANOSECONDS)"), | ||
), | ||
).sequence | ||
.map(Result.all) | ||
} | ||
|
||
def testDefaultTimeSourceTimeSpent: Property = { | ||
for { | ||
waitFor <- Gen.int(Range.linear(200, 700)).map(_.milliseconds).log("waitFor") | ||
diff <- Gen.constant(180.milliseconds).log("diff") | ||
} yield runIO { | ||
val timeSource = TimeSource.default[F]( | ||
"Test" | ||
) | ||
|
||
for { | ||
_ <- F.sleep(500.milliseconds) // warm up | ||
resultAndTimeSpent <- timeSource.timeSpent { | ||
F.sleep(waitFor) *> | ||
pureOf("Done") | ||
} | ||
(result, timeSpent) = resultAndTimeSpent | ||
_ <- effectOf( | ||
println( | ||
s""">>> waitFor: ${waitFor.show} | ||
|>>> timeSpent: ${timeSpent.timeSpent.toMillis.show} milliseconds | ||
|>>> diff: ${(timeSpent.timeSpent - waitFor).toMillis.show} milliseconds | ||
|""".stripMargin | ||
) | ||
) | ||
} yield { | ||
Result.all( | ||
List( | ||
result ==== "Done", | ||
Result | ||
.diffNamed( | ||
s"timeSpent (${timeSpent.timeSpent.toMillis.show} milliseconds) should be " + | ||
s"within ${(waitFor - diff).show} to ${(waitFor + diff).show}.", | ||
timeSpent, | ||
(waitFor +- diff), | ||
)(_.timeSpent.isWithIn(_)) | ||
.log( | ||
s"""--- diff test log --- | ||
|> actual: ${timeSpent.timeSpent.toMillis.show} milliseconds | ||
|> expected range: ${(waitFor - diff).show} to ${(waitFor + diff).show} | ||
|> waitFor: ${waitFor.show} | ||
|> expected diff: +- ${diff.show}) | ||
|> actual diff: ${(timeSpent.timeSpent - waitFor).toMillis.show} milliseconds | ||
|""".stripMargin | ||
), | ||
) | ||
) | ||
} | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.