add CronTask, tests optimize

- add lib cron-utils: v9.2.0
- add CronTask
  - add CronTask's test
- change MedicationTimer using cron as time calculation backend (not using CronTask)
- change OnQuestionMarkReply support `⸘`
- minor SchedulerTest "immediately" test logic changes
This commit is contained in:
A.C.Sukazyo Eyre 2023-11-09 22:07:10 +08:00
parent 89c414e853
commit 3d44972233
Signed by: Eyre_S
GPG Key ID: C17CE40291207874
9 changed files with 141 additions and 21 deletions

View File

@ -90,6 +90,7 @@ dependencies {
implementation group: 'com.softwaremill.sttp.client3', name: scala('okhttp-backend'), version: lib_sttp_v implementation group: 'com.softwaremill.sttp.client3', name: scala('okhttp-backend'), version: lib_sttp_v
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: lib_okhttp_v implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: lib_okhttp_v
implementation group: 'com.google.code.gson', name: 'gson', version: lib_gson_v implementation group: 'com.google.code.gson', name: 'gson', version: lib_gson_v
implementation group: 'com.cronutils', name: 'cron-utils', version: lib_cron_utils_v
testImplementation group: 'org.scalatest', name: scala('scalatest'), version: lib_scalatest_v testImplementation group: 'org.scalatest', name: scala('scalatest'), version: lib_scalatest_v
testImplementation group: 'org.scalatest', name: scala('scalatest-freespec'), version: lib_scalatest_v testImplementation group: 'org.scalatest', name: scala('scalatest-freespec'), version: lib_scalatest_v

View File

@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur
MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono
MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s
VERSION = 1.3.0-dev1 VERSION = 1.3.0-dev2
USE_DELTA = false USE_DELTA = false
VERSION_DELTA = VERSION_DELTA =
@ -25,5 +25,6 @@ lib_javatelegramapi_v = 6.2.0
lib_sttp_v = 3.9.0 lib_sttp_v = 3.9.0
lib_okhttp_v = 4.11.0 lib_okhttp_v = 4.11.0
lib_gson_v = 2.10.1 lib_gson_v = 2.10.1
lib_cron_utils_v = 9.2.0
lib_scalatest_v = 3.2.17 lib_scalatest_v = 3.2.17

View File

@ -4,7 +4,6 @@ import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.bot.event.OnQuestionMarkReply.isAllMessageMark import cc.sukazyo.cono.morny.bot.event.OnQuestionMarkReply.isAllMessageMark
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.request.SendMessage import com.pengrad.telegrambot.request.SendMessage
import scala.language.postfixOps import scala.language.postfixOps
@ -33,7 +32,9 @@ class OnQuestionMarkReply (using coeur: MornyCoeur) extends EventListener {
object OnQuestionMarkReply { object OnQuestionMarkReply {
private val QUESTION_MARKS = Set('?', '', '¿', '⁈', '⁇', '‽', '❔', '❓') // todo: due to the limitation of Java char, the character (actually not a
// single character) is not supported yet.
private val QUESTION_MARKS = Set('?', '', '¿', '⁈', '⁇', '‽', '⸘', '❔', '❓')
def isAllMessageMark (using text: String): Boolean = { def isAllMessageMark (using text: String): Boolean = {
boundary[Boolean] { boundary[Boolean] {

View File

@ -7,11 +7,14 @@ import cc.sukazyo.cono.morny.util.schedule.RoutineTask
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
import cc.sukazyo.cono.morny.util.CommonFormat import cc.sukazyo.cono.morny.util.CommonFormat
import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis
import com.cronutils.builder.CronBuilder
import com.cronutils.model.definition.{CronDefinition, CronDefinitionBuilder}
import com.cronutils.model.time.ExecutionTime
import com.pengrad.telegrambot.model.{Message, MessageEntity} import com.pengrad.telegrambot.model.{Message, MessageEntity}
import com.pengrad.telegrambot.request.{EditMessageText, SendMessage} import com.pengrad.telegrambot.request.{EditMessageText, SendMessage}
import com.pengrad.telegrambot.response.SendResponse import com.pengrad.telegrambot.response.SendResponse
import java.time.{LocalDateTime, ZoneOffset} import java.time.{Instant, ZonedDateTime, ZoneOffset}
import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ArrayBuffer
import scala.language.implicitConversions import scala.language.implicitConversions
@ -85,18 +88,24 @@ class MedicationTimer (using coeur: MornyCoeur) {
object MedicationTimer { object MedicationTimer {
//noinspection ScalaWeakerAccess
val cronDef: CronDefinition = CronDefinitionBuilder.defineCron
.withHours.and
.instance
@throws[IllegalArgumentException] @throws[IllegalArgumentException]
def calcNextRoutineTimestamp (baseTimeMillis: EpochMillis, zone: ZoneOffset, notifyAt: Set[Int]): EpochMillis = { def calcNextRoutineTimestamp (baseTimeMillis: EpochMillis, zone: ZoneOffset, notifyAt: Set[Int]): EpochMillis = {
if (notifyAt isEmpty) throw new IllegalArgumentException("notify time is not set") if (notifyAt isEmpty) throw new IllegalArgumentException("notify time is not set")
var time = LocalDateTime.ofEpochSecond( import com.cronutils.model.field.expression.FieldExpressionFactory.*
baseTimeMillis / 1000, ((baseTimeMillis % 1000) * 1000 * 1000) toInt, ExecutionTime.forCron(CronBuilder.cron(cronDef)
zone .withHour(and({
).withMinute(0).withSecond(0).withNano(0) import scala.jdk.CollectionConverters.*
time = time plusHours 1 (for (i <- notifyAt) yield on(i)).toList.asJava
while (!(notifyAt contains(time getHour))) { }))
time = time plusHours 1 .instance
} ).nextExecution(
(time toInstant zone) toEpochMilli ZonedDateTime ofInstant (Instant ofEpochMilli baseTimeMillis, zone.normalized)
).get.toInstant.toEpochMilli
} }
} }

View File

@ -0,0 +1,46 @@
package cc.sukazyo.cono.morny.util.schedule
import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis
import com.cronutils.model.time.ExecutionTime
import com.cronutils.model.Cron
import java.time.{Instant, ZonedDateTime, ZoneId}
import scala.jdk.OptionConverters.*
trait CronTask extends RoutineTask {
private transparent inline def cronCalc = ExecutionTime.forCron(cron)
def cron: Cron
def zone: ZoneId
override def firstRoutineTimeMillis: EpochMillis =
cronCalc.nextExecution(
ZonedDateTime.ofInstant(
Instant.now, zone
)
).get.toInstant.toEpochMilli
override def nextRoutineTimeMillis (previousRoutineScheduledTimeMillis: EpochMillis): EpochMillis | Null =
cronCalc.nextExecution(
ZonedDateTime.ofInstant(
Instant.ofEpochMilli(previousRoutineScheduledTimeMillis),
zone
)
).toScala match
case Some(time) => time.toInstant.toEpochMilli
case None => null
}
object CronTask {
def apply (_name: String, _cron: Cron, _zone: ZoneId, _main: =>Unit): CronTask =
new CronTask:
override def name: String = _name
override def cron: Cron = _cron
override def zone: ZoneId = _zone
override def main: Unit = _main
}

View File

@ -16,10 +16,19 @@ class OnQuestionMarkReplyTest extends MornyTests with TableDrivenPropertyChecks
("为什么?", false), ("为什么?", false),
("?这不合理", false), ("?这不合理", false),
("??尊嘟假嘟", false), ("??尊嘟假嘟", false),
(":¿", false),
("?????", true), ("?????", true),
("¿", true),
("⁈??", true),
("?!??", false),
("⁇", true),
("‽", true),
("?⸘?", true),
("?", true), ("?", true),
("", true), ("?", true),
("??❔", true), ("❔", true),
("❓❓❓", true),
// ("⁉️", true)
) )
forAll(examples) { (text, is) => forAll(examples) { (text, is) =>

View File

@ -0,0 +1,51 @@
package cc.sukazyo.cono.morny.test.utils.schedule
import cc.sukazyo.cono.morny.test.MornyTests
import cc.sukazyo.cono.morny.util.schedule.{CronTask, Scheduler}
import cc.sukazyo.cono.morny.util.CommonFormat.formatDate
import com.cronutils.builder.CronBuilder
import com.cronutils.model.definition.CronDefinitionBuilder
import com.cronutils.model.field.expression.FieldExpressionFactory as C
import com.cronutils.model.time.ExecutionTime
import org.scalatest.tagobjects.Slow
import java.lang.System.currentTimeMillis
import java.time.{ZonedDateTime, ZoneOffset}
import java.time.temporal.ChronoUnit
class CronTaskTest extends MornyTests {
"cron task works fine" taggedAs Slow in {
val scheduler = Scheduler()
val cronSecondly =
CronBuilder.cron(
CronDefinitionBuilder.defineCron
.withSeconds.and
.instance
).withSecond(C.every(1)).instance
Thread.sleep(
ExecutionTime.forCron(cronSecondly)
.timeToNextExecution(ZonedDateTime.now)
.get.get(ChronoUnit.NANOS)/1000
) // aligned current time to millisecond 000
note(s"CronTask test time aligned to ${formatDate(currentTimeMillis, 0)}")
var times = 0
val task = CronTask("cron-task", cronSecondly, ZoneOffset.ofHours(0).normalized, {
times = times + 1
note(s"CronTask executed at ${formatDate(currentTimeMillis, 0)}")
})
scheduler ++ task
Thread.sleep(10300)
// it should be at 300ms position to 10 seconds
scheduler % task
scheduler.stop()
note(s"CronTasks done at ${formatDate(currentTimeMillis, 0)}")
times shouldEqual 10
}
}

View File

@ -17,7 +17,7 @@ class IntervalsTest extends MornyTests {
val timeUsed = System.currentTimeMillis() - startTime val timeUsed = System.currentTimeMillis() - startTime
times shouldEqual 10 times shouldEqual 10
timeUsed should (be <= 2100L and be >= 1900L) timeUsed should (be <= 2100L and be >= 1900L)
info(s"interval 200ms for 10 times used time ${timeUsed}ms") info(s"Interval Task with interval 200ms for 10 times used time ${timeUsed}ms")
} }
} }

View File

@ -12,13 +12,15 @@ class SchedulerTest extends MornyTests {
"Task with scheduleTime smaller than current time should be executed immediately" in { "Task with scheduleTime smaller than current time should be executed immediately" in {
val scheduler = Scheduler() val scheduler = Scheduler()
var time = System.currentTimeMillis val time = System.currentTimeMillis
var doneTime: Option[Long] = None
scheduler ++ Task("task", 0, { scheduler ++ Task("task", 0, {
time = System.currentTimeMillis - time doneTime = Some(System.currentTimeMillis)
}) })
scheduler.waitForStopAtAllDone() Thread.sleep(10)
time should be <= 10L scheduler.stop()
info(s"Immediately Task done with time $time") doneTime shouldBe defined
info(s"Immediately Task done in ${doneTime.get - time}ms")
} }
"Task's running thread name should be task name" in { "Task's running thread name should be task name" in {