Skip to content

Commit

Permalink
Add fs2-cron-cron-utils module (#412)
Browse files Browse the repository at this point in the history
  • Loading branch information
fthomas authored Mar 8, 2023
1 parent e8fd385 commit a0d8316
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 50 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master')
run: mkdir -p target .js/target modules/cron4s/.jvm/target .jvm/target .native/target modules/readme/target modules/core/.jvm/target modules/calev/.jvm/target project/target
run: mkdir -p target .js/target modules/cron4s/.jvm/target .jvm/target .native/target modules/cron-utils/.jvm/target modules/readme/target modules/core/.jvm/target modules/calev/.jvm/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master')
run: tar cf targets.tar target .js/target modules/cron4s/.jvm/target .jvm/target .native/target modules/readme/target modules/core/.jvm/target modules/calev/.jvm/target project/target
run: tar cf targets.tar target .js/target modules/cron4s/.jvm/target .jvm/target .native/target modules/cron-utils/.jvm/target modules/readme/target modules/core/.jvm/target modules/calev/.jvm/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master')
Expand Down
51 changes: 29 additions & 22 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import sbtcrossproject.{CrossProject, CrossType, Platform}
import org.typelevel.sbt.gha.JavaSpec.Distribution.Temurin

/// variables

Expand All @@ -13,9 +12,10 @@ val Scala_2_13 = "2.13.10"
val Scala_3 = "3.2.2"

val moduleCrossPlatformMatrix: Map[String, List[Platform]] = Map(
"calev" -> List(JVMPlatform),
"core" -> List(JVMPlatform),
"cron4s" -> List(JVMPlatform),
"calev" -> List(JVMPlatform)
"cron-utils" -> List(JVMPlatform)
)

/// global settings
Expand Down Expand Up @@ -66,7 +66,7 @@ ThisBuild / mergifyPrRules := {
/// projects

lazy val root = tlCrossRootProject
.aggregate(calev, core, cron4s, readme)
.aggregate(calev, core, cron4s, cronUtils, readme)

lazy val core = myCrossProject("core")
.settings(
Expand All @@ -75,53 +75,60 @@ lazy val core = myCrossProject("core")
)
)

lazy val coreJVM = core.jvm

lazy val cron4s = myCrossProject("cron4s")
lazy val calev = myCrossProject("calev")
.dependsOn(core)
.settings(
crossScalaVersions := List(Scala_2_12, Scala_2_13),
libraryDependencies ++= Seq(
Dependencies.cron4s,
Dependencies.fs2Core,
Dependencies.scalaTest % Test
Dependencies.calevCore,
Dependencies.munitCatsEffect % Test
),
initialCommands := s"""
import $rootPkg._
import cats.effect.unsafe.implicits.global
import $rootPkg.calev._
import cats.effect.IO
import _root_.cron4s.Cron
import cats.effect.unsafe.implicits.global
import com.github.eikek.calev._
import fs2.Stream
import scala.concurrent.ExecutionContext
"""
)

lazy val cron4sJVM = cron4s.jvm

lazy val calev = myCrossProject("calev")
lazy val cron4s = myCrossProject("cron4s")
.dependsOn(core)
.settings(
crossScalaVersions := List(Scala_2_12, Scala_2_13),
libraryDependencies ++= Seq(
Dependencies.calevCore,
Dependencies.scalaTest % Test
Dependencies.cron4s,
Dependencies.munitCatsEffect % Test
),
initialCommands := s"""
import $rootPkg._
import $rootPkg.calev._
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import com.github.eikek.calev._
import cats.effect.IO
import _root_.cron4s.Cron
import fs2.Stream
import scala.concurrent.ExecutionContext
"""
)

lazy val calevJVM = calev.jvm
lazy val cronUtils = myCrossProject("cron-utils")
.dependsOn(core)
.settings(
libraryDependencies ++= Seq(
Dependencies.cronUtils,
Dependencies.munitCatsEffect % Test
),
tlVersionIntroduced := Map(
"2.12" -> "0.8.2",
"2.13" -> "0.8.2",
"3" -> "0.8.2"
)
)

lazy val readme = project
.in(file("modules/readme"))
.enablePlugins(MdocPlugin, NoPublishPlugin)
.dependsOn(calevJVM, cron4sJVM)
.dependsOn(calev.jvm, cron4s.jvm)
.settings(commonSettings)
.settings(
crossScalaVersions := List(Scala_2_12, Scala_2_13),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
package eu.timepit.fs2cron.calev

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import com.github.eikek.calev.CalEvent
import fs2.Stream
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import munit.CatsEffectSuite

import java.time.{Instant, ZoneId, ZoneOffset}

class CalevSchedulerTest extends AnyFunSuite with Matchers {
class CalevSchedulerTest extends CatsEffectSuite {
private val everySecond: CalEvent = CalEvent.unsafe("*-*-* *:*:*")
private val evenSeconds: CalEvent = CalEvent.unsafe("*-*-* *:*:0/2")

private def isEven(i: Long): Boolean = i % 2 == 0
private def instantSeconds(i: Instant): Long = i.getEpochSecond
private val evalInstantNow: Stream[IO, Instant] = Stream.eval(IO(Instant.now()))
Expand All @@ -37,26 +37,25 @@ class CalevSchedulerTest extends AnyFunSuite with Matchers {
test("awakeEvery") {
val s1 = schedulerSys.awakeEvery(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).take(2).forall(isEven)
s2.compile.last.map(_ should be(Option(true))).unsafeRunSync()
assertIO(s2.compile.last, Some(true))
}

test("sleep") {
val s1 = schedulerUtc.sleep(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).forall(isEven)
s2.compile.last.map(_ should be(Option(true))).unsafeRunSync()
assertIO(s2.compile.last, Some(true))
}

test("schedule") {
val everySecond: CalEvent = CalEvent.unsafe("*-*-* *:*:*")
val s1 = schedulerSys
.schedule(List(everySecond -> evalInstantNow, evenSeconds -> evalInstantNow))
.map(instantSeconds)

(for {
for {
seconds <- s1.take(3).compile.toList
_ <- IO(seconds.count(isEven) shouldBe 2)
_ <- IO(seconds.count(!isEven(_)) shouldBe 1)
} yield ()).unsafeRunSync()
_ = assertEquals(seconds.count(isEven), 2)
_ = assertEquals(seconds.count(!isEven(_)), 1)
} yield ()
}

test("timezones") {
Expand All @@ -65,6 +64,6 @@ class CalevSchedulerTest extends AnyFunSuite with Matchers {

val s1 = scheduler.awakeEvery(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).take(2).forall(!isEven(_))
s2.compile.last.map(_ should be(Option(true))).unsafeRunSync()
assertIO(s2.compile.last, Some(true))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2018-2021 fs2-cron contributors
*
* 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 eu.timepit.fs2cron.cronutils

import cats.effect.{Sync, Temporal}
import com.cronutils.model.time.ExecutionTime
import eu.timepit.fs2cron.{Scheduler, ZonedDateTimeScheduler}

import java.time.{ZoneId, ZoneOffset, ZonedDateTime}

object CronUtilsScheduler {
def systemDefault[F[_]](implicit temporal: Temporal[F], F: Sync[F]): Scheduler[F, ExecutionTime] =
from(F.delay(ZoneId.systemDefault()))

def utc[F[_]](implicit F: Temporal[F]): Scheduler[F, ExecutionTime] =
from(F.pure(ZoneOffset.UTC))

def from[F[_]](zoneId: F[ZoneId])(implicit F: Temporal[F]): Scheduler[F, ExecutionTime] =
new ZonedDateTimeScheduler[F, ExecutionTime](zoneId) {
override def next(from: ZonedDateTime, schedule: ExecutionTime): F[ZonedDateTime] =
schedule.nextExecution(from).map[F[ZonedDateTime]](zdt => F.pure(zdt)).orElse {
val msg = s"Could not calculate the next date-time from $from " +
s"given the cron expression '$schedule'."
F.raiseError(new Throwable(msg))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2018-2021 fs2-cron contributors
*
* 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 eu.timepit.fs2cron.cronutils

import cats.effect.IO
import com.cronutils.model.CronType
import com.cronutils.model.definition.CronDefinitionBuilder
import com.cronutils.model.time.ExecutionTime
import com.cronutils.parser.CronParser
import fs2.Stream
import munit.CatsEffectSuite

import java.time.{Instant, ZoneId, ZoneOffset}

class CronUtilsSchedulerTest extends CatsEffectSuite {
private val cronDef = CronDefinitionBuilder.instanceDefinitionFor(CronType.SPRING)
private val parser = new CronParser(cronDef)

private val everySecond = ExecutionTime.forCron(parser.parse("* * * ? * *"))
private val evenSeconds = ExecutionTime.forCron(parser.parse("*/2 * * ? * *"))

private def isEven(i: Long): Boolean = i % 2 == 0
private def instantSeconds(i: Instant): Long = i.getEpochSecond
private val evalInstantNow: Stream[IO, Instant] = Stream.eval(IO(Instant.now()))

private val schedulerSys = CronUtilsScheduler.systemDefault[IO]
private val schedulerUtc = CronUtilsScheduler.utc[IO]

test("awakeEvery") {
val s1 = schedulerSys.awakeEvery(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).take(2).forall(isEven)
assertIO(s2.compile.last, Some(true))
}

test("sleep") {
val s1 = schedulerUtc.sleep(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).forall(isEven)
assertIO(s2.compile.last, Some(true))
}

test("schedule") {
val s1 = schedulerSys
.schedule(List(everySecond -> evalInstantNow, evenSeconds -> evalInstantNow))
.map(instantSeconds)

for {
seconds <- s1.take(3).compile.toList
_ = assertEquals(seconds.count(isEven), 2)
_ = assertEquals(seconds.count(!isEven(_)), 1)
} yield ()
}

test("timezones") {
val zoneId: ZoneId = ZoneOffset.ofTotalSeconds(1)
val scheduler = CronUtilsScheduler.from(IO.pure(zoneId))

val s1 = scheduler.awakeEvery(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).take(2).forall(!isEven(_))
assertIO(s2.compile.last, Some(true))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@
package eu.timepit.fs2cron.cron4s

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import cron4s.Cron
import cron4s.expr.CronExpr
import fs2.Stream
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import munit.CatsEffectSuite

import java.time.{Instant, ZoneId, ZoneOffset}

class Cron4sSchedulerTest extends AnyFunSuite with Matchers {
class Cron4sSchedulerTest extends CatsEffectSuite {
private val everySecond: CronExpr = Cron.unsafeParse("* * * ? * *")
private val evenSeconds: CronExpr = Cron.unsafeParse("*/2 * * ? * *")

private def isEven(i: Long): Boolean = i % 2 == 0
private def instantSeconds(i: Instant): Long = i.getEpochSecond
private val evalInstantNow: Stream[IO, Instant] = Stream.eval(IO(Instant.now()))
Expand All @@ -38,26 +38,25 @@ class Cron4sSchedulerTest extends AnyFunSuite with Matchers {
test("awakeEvery") {
val s1 = schedulerSys.awakeEvery(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).take(2).forall(isEven)
s2.compile.last.map(_ should be(Option(true))).unsafeRunSync()
assertIO(s2.compile.last, Some(true))
}

test("sleep") {
val s1 = schedulerUtc.sleep(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).forall(isEven)
s2.compile.last.map(_ should be(Option(true))).unsafeRunSync()
assertIO(s2.compile.last, Some(true))
}

test("schedule") {
val everySecond: CronExpr = Cron.unsafeParse("* * * ? * *")
val s1 = schedulerSys
.schedule(List(everySecond -> evalInstantNow, evenSeconds -> evalInstantNow))
.map(instantSeconds)

(for {
for {
seconds <- s1.take(3).compile.toList
_ <- IO(seconds.count(isEven) shouldBe 2)
_ <- IO(seconds.count(!isEven(_)) shouldBe 1)
} yield ()).unsafeRunSync()
_ = assertEquals(seconds.count(isEven), 2)
_ = assertEquals(seconds.count(!isEven(_)), 1)
} yield ()
}

test("timezones") {
Expand All @@ -66,6 +65,6 @@ class Cron4sSchedulerTest extends AnyFunSuite with Matchers {

val s1 = scheduler.awakeEvery(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).take(2).forall(!isEven(_))
s2.compile.last.map(_ should be(Option(true))).unsafeRunSync()
assertIO(s2.compile.last, Some(true))
}
}
5 changes: 3 additions & 2 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import sbt._

object Dependencies {
val calevCore = "com.github.eikek" %% "calev-core" % "0.6.4"
val cron4s = "com.github.alonsodomin.cron4s" %% "cron4s-core" % "0.6.1"
val cronUtils = "com.cronutils" % "cron-utils" % "9.2.0"
val fs2Core = "co.fs2" %% "fs2-core" % "3.6.1"
val scalaTest = "org.scalatest" %% "scalatest" % "3.2.15"
val calevCore = "com.github.eikek" %% "calev-core" % "0.6.4"
val munitCatsEffect = "org.typelevel" %% "munit-cats-effect-3" % "1.0.7"
}

0 comments on commit a0d8316

Please sign in to comment.