diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala index f763451..b1cc8d1 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala @@ -8,6 +8,7 @@ import cc.sukazyo.cono.morny.bot.api.EventListenerManager import cc.sukazyo.cono.morny.bot.event.{MornyEventListeners, MornyOnInlineQuery, MornyOnTelegramCommand, MornyOnUpdateTimestampOffsetLock} import cc.sukazyo.cono.morny.bot.query.MornyQueries import cc.sukazyo.cono.morny.util.schedule.Scheduler +import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis import com.pengrad.telegrambot.TelegramBot import com.pengrad.telegrambot.request.GetMe @@ -54,7 +55,7 @@ class MornyCoeur (using val config: MornyConfig) { * * in milliseconds. */ - val coeurStartTimestamp: Long = System.currentTimeMillis + val coeurStartTimestamp: EpochMillis = System.currentTimeMillis /** [[TelegramBot]] account of this Morny */ val account: TelegramBot = __loginResult.account diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala index 4f80a69..5dbbb9f 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala @@ -73,13 +73,14 @@ class OnCallMe (using coeur: MornyCoeur) extends EventListener { lastDinnerData.forwardFromMessageId ) import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} + import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h - def lastDinner_dateMillis: Long = lastDinnerData.forwardDate longValue; + def lastDinner_dateMillis: EpochMillis = EpochMillis fromEpochSeconds lastDinnerData.forwardDate coeur.account exec SendMessage( req.from.id, "on %s [UTC+8]\n- %s before".formatted( h(formatDate(lastDinner_dateMillis, 8)), - h(formatDuration(lastDinner_dateMillis)) + h(formatDuration(System.currentTimeMillis - lastDinner_dateMillis)) ) ).parseMode(ParseMode HTML).replyToMessageId(sendResp.message.messageId) isAllowed = true diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala index 7d81ebd..522e6ab 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala @@ -6,6 +6,7 @@ import cc.sukazyo.cono.morny.daemon.MedicationTimer.calcNextRoutineTimestamp import cc.sukazyo.cono.morny.util.schedule.RoutineTask import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import cc.sukazyo.cono.morny.util.CommonFormat +import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis import com.pengrad.telegrambot.model.{Message, MessageEntity} import com.pengrad.telegrambot.request.{EditMessageText, SendMessage} import com.pengrad.telegrambot.response.SendResponse @@ -30,15 +31,15 @@ class MedicationTimer (using coeur: MornyCoeur) { override def name: String = DAEMON_THREAD_NAME_DEF - def calcNextSendTime: Long = + def calcNextSendTime: EpochMillis = val next_time = calcNextRoutineTimestamp(System.currentTimeMillis, use_timeZone, notify_atHour) logger info s"medication timer will send next notify at ${CommonFormat.formatDate(next_time, use_timeZone.getTotalSeconds / 60 / 60)} with $use_timeZone [$next_time]" next_time - override def firstRoutineTimeMillis: Long = + override def firstRoutineTimeMillis: EpochMillis = calcNextSendTime - override def nextRoutineTimeMillis (previousRoutineScheduledTimeMillis: Long): Long | Null = + override def nextRoutineTimeMillis (previousRoutineScheduledTimeMillis: EpochMillis): EpochMillis | Null = calcNextSendTime override def main: Unit = { @@ -85,7 +86,7 @@ class MedicationTimer (using coeur: MornyCoeur) { object MedicationTimer { @throws[IllegalArgumentException] - def calcNextRoutineTimestamp (baseTimeMillis: Long, zone: ZoneOffset, notifyAt: Set[Int]): Long = { + def calcNextRoutineTimestamp (baseTimeMillis: EpochMillis, zone: ZoneOffset, notifyAt: Set[Int]): EpochMillis = { if (notifyAt isEmpty) throw new IllegalArgumentException("notify time is not set") var time = LocalDateTime.ofEpochSecond( baseTimeMillis / 1000, ((baseTimeMillis % 1000) * 1000 * 1000) toInt, diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala b/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala index b707576..b2e9172 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala @@ -1,15 +1,16 @@ package cc.sukazyo.cono.morny.data +import cc.sukazyo.cono.morny.util.EpochDateTime.{EpochDays, EpochMillis} import com.pengrad.telegrambot.model.User import scala.language.postfixOps object MornyJrrp { - def jrrp_of_telegramUser (user: User, timestamp: Long): Double = - jrrp_v_xmomi(user.id, timestamp/(1000*60*60*24)) * 100.0 + def jrrp_of_telegramUser (user: User, timestamp: EpochMillis): Double = + jrrp_v_xmomi(user.id, EpochDays fromEpochMillis timestamp) * 100.0 - private def jrrp_v_xmomi (identifier: Long, dayStamp: Long): Double = + private def jrrp_v_xmomi (identifier: Long, dayStamp: EpochDays): Double = import cc.sukazyo.cono.morny.util.CommonEncrypt.MD5 import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex java.lang.Long.parseLong(MD5(s"$identifier@$dayStamp").toHex.substring(0, 4), 16) / (0xffff toDouble) diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/EpochDateTime.scala b/src/main/scala/cc/sukazyo/cono/morny/util/EpochDateTime.scala index 81613c9..5f3d7a2 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/EpochDateTime.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/EpochDateTime.scala @@ -10,9 +10,6 @@ object EpochDateTime { * aka. Milliseconds since 00:00:00 UTC on Thursday, 1 January 1970. */ type EpochMillis = Long - /** Time duration/interval in milliseconds. */ - type DurationMillis = Long - object EpochMillis: /** convert a localtime with timezone to epoch milliseconds * @@ -31,5 +28,41 @@ object EpochDateTime { def apply (time_zone: (String, String)): EpochMillis = time_zone match case (time, zone) => apply(time, zone) + + /** Convert from [[EpochSeconds]]. + * + * Due to the missing accuracy, the converted EpochMillis will + * be always in 0ms aligned. + */ + def fromEpochSeconds (epochSeconds: EpochSeconds): EpochMillis = + epochSeconds.longValue * 1000L + + /** The UNIX Epoch Time in seconds. + * + * aka. Seconds since 00:00:00 UTC on Thursday, 1 January 1970. + * + * Normally is the epochSeconds = (epochMillis / 1000) + * + * Notice that, currently, it stores using [[Int]] (also the implementation + * method of Telegram), which will only store times before 2038-01-19 03:14:07. + */ + type EpochSeconds = Int + + /** The UNIX Epoch Time in day. + * + * aka. days since 00:00:00 UTC on Thursday, 1 January 1970. + * + * Normally is the epochDays = (epochMillis / 1000 / 60 / 60 / 24) + * + * Notice that, currently, it stores using [[Short]] (also the implementation + * method of Telegram), which will only store times before 2059-09-18. + */ + type EpochDays = Short + object EpochDays: + def fromEpochMillis (epochMillis: EpochMillis): EpochDays = + (epochMillis / (1000*60*60*24)).toShort + + /** Time duration/interval in milliseconds. */ + type DurationMillis = Long } diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/DelayedTask.scala b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/DelayedTask.scala index a8a9499..130c4e3 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/DelayedTask.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/DelayedTask.scala @@ -1,16 +1,18 @@ package cc.sukazyo.cono.morny.util.schedule +import cc.sukazyo.cono.morny.util.EpochDateTime.{DurationMillis, EpochMillis} + trait DelayedTask ( - val delayedMillis: Long + val delayedMillis: DurationMillis ) extends Task { - override val scheduledTimeMillis: Long = System.currentTimeMillis + delayedMillis + override val scheduledTimeMillis: EpochMillis = System.currentTimeMillis + delayedMillis } object DelayedTask { - def apply (_name: String, delayedMillis: Long, task: =>Unit): DelayedTask = + def apply (_name: String, delayedMillis: DurationMillis, task: =>Unit): DelayedTask = new DelayedTask (delayedMillis): override val name: String = _name override def main: Unit = task diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/IntervalTask.scala b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/IntervalTask.scala index cc87240..37667ba 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/IntervalTask.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/IntervalTask.scala @@ -1,24 +1,26 @@ package cc.sukazyo.cono.morny.util.schedule +import cc.sukazyo.cono.morny.util.EpochDateTime.{DurationMillis, EpochMillis} + trait IntervalTask extends RoutineTask { - def intervalMillis: Long + def intervalMillis: DurationMillis - override def firstRoutineTimeMillis: Long = + override def firstRoutineTimeMillis: EpochMillis = System.currentTimeMillis() + intervalMillis override def nextRoutineTimeMillis ( - previousScheduledRoutineTimeMillis: Long - ): Long|Null = + previousScheduledRoutineTimeMillis: EpochMillis + ): EpochMillis|Null = previousScheduledRoutineTimeMillis + intervalMillis } object IntervalTask { - def apply (_name: String, _intervalMillis: Long, task: =>Unit): IntervalTask = + def apply (_name: String, _intervalMillis: DurationMillis, task: =>Unit): IntervalTask = new IntervalTask: - override def intervalMillis: Long = _intervalMillis + override def intervalMillis: DurationMillis = _intervalMillis override def name: String = _name override def main: Unit = task diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/IntervalWithTimesTask.scala b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/IntervalWithTimesTask.scala index c2055a9..5e55a8a 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/IntervalWithTimesTask.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/IntervalWithTimesTask.scala @@ -1,11 +1,13 @@ package cc.sukazyo.cono.morny.util.schedule +import cc.sukazyo.cono.morny.util.EpochDateTime.{DurationMillis, EpochMillis} + trait IntervalWithTimesTask extends IntervalTask { def times: Int private var currentExecutedTimes = 1 - override def nextRoutineTimeMillis (previousScheduledRoutineTimeMillis: Long): Long | Null = + override def nextRoutineTimeMillis (previousScheduledRoutineTimeMillis: EpochMillis): EpochMillis | Null = if currentExecutedTimes >= times then null else @@ -16,11 +18,11 @@ trait IntervalWithTimesTask extends IntervalTask { object IntervalWithTimesTask { - def apply (_name: String, _intervalMillis: Long, _times: Int, task: =>Unit): IntervalWithTimesTask = + def apply (_name: String, _intervalMillis: DurationMillis, _times: Int, task: =>Unit): IntervalWithTimesTask = new IntervalWithTimesTask: override def name: String = _name override def times: Int = _times - override def intervalMillis: Long = _intervalMillis + override def intervalMillis: DurationMillis = _intervalMillis override def main: Unit = task } diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/RoutineTask.scala b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/RoutineTask.scala index 09ade60..dce86fe 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/RoutineTask.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/RoutineTask.scala @@ -1,12 +1,45 @@ package cc.sukazyo.cono.morny.util.schedule +import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis + +/** The task that can execute multiple times with custom routine function. + * + * When creating a Routine Task, the task's [[firstRoutineTimeMillis]] function + * will be called and the result value will be the first task scheduled time. + * + * After every execution complete and enter the post effect, the [[nextRoutineTimeMillis]] + * function will be called, then its value will be stored as the new task's + * scheduled time and re-scheduled by its scheduler. + */ trait RoutineTask extends Task { - private[schedule] var currentScheduledTimeMillis: Long = firstRoutineTimeMillis - override def scheduledTimeMillis: Long = currentScheduledTimeMillis + private[schedule] var currentScheduledTimeMillis: EpochMillis = firstRoutineTimeMillis - def firstRoutineTimeMillis: Long + /** Next running time of this task. + * + * Should be auto generated from [[firstRoutineTimeMillis]] and + * [[nextRoutineTimeMillis]]. + */ + override def scheduledTimeMillis: EpochMillis = currentScheduledTimeMillis - def nextRoutineTimeMillis (previousRoutineScheduledTimeMillis: Long): Long|Null + /** The task scheduled time at initial. + * + * In the default environment, this function will only be called once + * when the task object is just created. + */ + def firstRoutineTimeMillis: EpochMillis + + /** The function to calculate the next scheduled time after previous task + * routine complete. + * + * This function will be called every time the task is done once, in the + * task runner thread and the post effect scope. + * + * @param previousRoutineScheduledTimeMillis The previous task routine's + * scheduled time. + * @return The next task routine's scheduled time, or [[null]] means end + * of the task. + */ + def nextRoutineTimeMillis (previousRoutineScheduledTimeMillis: EpochMillis): EpochMillis|Null } diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Scheduler.scala b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Scheduler.scala index 190f9f7..411be39 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Scheduler.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Scheduler.scala @@ -1,5 +1,7 @@ package cc.sukazyo.cono.morny.util.schedule +import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis + import scala.annotation.targetName import scala.collection.mutable @@ -78,7 +80,7 @@ class Scheduler { runtimeStatus = State.PREPARE_RUN - val nextMove: Task|Long|"None" = taskList.synchronized { + val nextMove: Task|EpochMillis|"None" = taskList.synchronized { taskList.headOption match case Some(_readyToRun) if System.currentTimeMillis >= _readyToRun.scheduledTimeMillis => taskList -= _readyToRun @@ -107,7 +109,7 @@ class Scheduler { currentRunning match case routine: RoutineTask => routine.nextRoutineTimeMillis(routine.currentScheduledTimeMillis) match - case next: Long => + case next: EpochMillis => routine.currentScheduledTimeMillis = next if (!currentRunning_isScheduledCancel) schedule(routine) case _ => @@ -117,7 +119,7 @@ class Scheduler { currentRunning = null this setName runnerName - case needToWaitMillis: Long => + case needToWaitMillis: EpochMillis => runtimeStatus = State.WAITING try Thread.sleep(needToWaitMillis) catch case _: InterruptedException => {} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Task.scala b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Task.scala index 2dc4503..6250d9a 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Task.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/Task.scala @@ -55,3 +55,13 @@ trait Task extends Ordered[Task] { s"""${super.toString}{"$name": $scheduledTimeMillis}""" } + +object Task { + + def apply (_name: String, _scheduledTime: EpochMillis, _main: =>Unit): Task = + new Task: + override def name: String = _name + override def scheduledTimeMillis: EpochMillis = _scheduledTime + override def main: Unit = _main + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/EpochDateTimeTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/EpochDateTimeTest.scala index a0d218a..a766428 100644 --- a/src/test/scala/cc/sukazyo/cono/morny/test/utils/EpochDateTimeTest.scala +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/EpochDateTimeTest.scala @@ -1,11 +1,55 @@ package cc.sukazyo.cono.morny.test.utils import cc.sukazyo.cono.morny.test.MornyTests -import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis +import cc.sukazyo.cono.morny.util.EpochDateTime.{EpochDays, EpochMillis, EpochSeconds} import org.scalatest.prop.TableDrivenPropertyChecks class EpochDateTimeTest extends MornyTests with TableDrivenPropertyChecks { + "while converting to EpochMillis :" - { + + "from EpochSeconds :" - { + + val examples = Table[EpochSeconds, EpochMillis]( + ("EpochSeconds", "EpochMillis"), + (1699176068, 1699176068000L), + (1699176000, 1699176000000L), + (1, 1000L), + ) + + forAll(examples) { (epochSeconds, epochMillis) => + s"EpochSeconds($epochSeconds) should be converted to EpochMillis($epochMillis)" in { + (EpochMillis fromEpochSeconds epochSeconds) shouldEqual epochMillis + } + } + + } + + } + + "while converting to EpochDays :" - { + + "from EpochMillis :" - { + + val examples = Table( + ("EpochMillis", "EpochDays"), + (0L, 0), + (1000L, 0), + (80000000L, 0), + (90000000L, 1), + (1699176549059L, 19666) + ) + + forAll(examples) { (epochMillis, epochDays) => + s"EpochMillis($epochMillis) should be converted to EpochDays($epochDays)" in { + (EpochDays fromEpochMillis epochMillis) shouldEqual epochDays + } + } + + } + + } + "while converting date-time string to time-millis : " - { "while using ISO-Offset-Date-Time : " - { diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/IntervalsTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/IntervalsTest.scala new file mode 100644 index 0000000..c0d1d81 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/IntervalsTest.scala @@ -0,0 +1,23 @@ +package cc.sukazyo.cono.morny.test.utils.schedule + +import cc.sukazyo.cono.morny.test.MornyTests +import cc.sukazyo.cono.morny.util.schedule.{IntervalWithTimesTask, Scheduler} +import org.scalatest.tagobjects.Slow + +class IntervalsTest extends MornyTests { + + "IntervalWithTimesTest should work even scheduler is scheduled to stop" taggedAs Slow in { + val scheduler = Scheduler() + var times = 0 + scheduler ++ IntervalWithTimesTask("intervals-10", 200, 10, { + times = times + 1 + }) + val startTime = System.currentTimeMillis() + scheduler.waitForStopAtAllDone() + val timeUsed = System.currentTimeMillis() - startTime + times shouldEqual 10 + timeUsed should (be <= 2100L and be >= 1900L) + info(s"interval 200ms for 10 times used time ${timeUsed}ms") + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/SchedulerTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/SchedulerTest.scala new file mode 100644 index 0000000..bb9271e --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/SchedulerTest.scala @@ -0,0 +1,47 @@ +package cc.sukazyo.cono.morny.test.utils.schedule + +import cc.sukazyo.cono.morny.test.MornyTests +import cc.sukazyo.cono.morny.util.schedule.{DelayedTask, Scheduler, Task} +import org.scalatest.tagobjects.Slow + +import scala.collection.mutable + +class SchedulerTest extends MornyTests { + + "While executing tasks using scheduler :" - { + + "Task with scheduleTime smaller than current time should be executed immediately" in { + val scheduler = Scheduler() + var time = System.currentTimeMillis + scheduler ++ Task("task", 0, { + time = System.currentTimeMillis - time + }) + scheduler.waitForStopAtAllDone() + time should be <= 10L + info(s"Immediately Task done with time $time") + } + + "Task's running thread name should be task name" in { + val scheduler = Scheduler() + var executedThread: Option[String] = None + scheduler ++ Task("task", 0, { + executedThread = Some(Thread.currentThread.getName) + }) + scheduler.waitForStopAtAllDone() + executedThread shouldEqual Some("task") + } + + "Task's execution order should be ordered by task Ordering but not insert order" taggedAs Slow in { + val scheduler = Scheduler() + val result = mutable.ArrayBuffer.empty[String] + scheduler + ++ DelayedTask("task-later", 400L, { result += Thread.currentThread.getName }) + ++ DelayedTask("task-very-late", 800L, { result += Thread.currentThread.getName }) + ++ DelayedTask("task-early", 100L, { result += Thread.currentThread.getName }) + scheduler.waitForStopAtAllDone() + result.toArray shouldEqual Array("task-early", "task-later", "task-very-late") + } + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/TaskBasicTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/TaskBasicTest.scala new file mode 100644 index 0000000..0883040 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/TaskBasicTest.scala @@ -0,0 +1,46 @@ +package cc.sukazyo.cono.morny.test.utils.schedule + +import cc.sukazyo.cono.morny.test.MornyTests +import cc.sukazyo.cono.morny.util.schedule.Task +import org.scalatest.tagobjects.Slow + +class TaskBasicTest extends MornyTests { + + "while comparing tasks :" - { + + "tasks with different scheduleTime should be compared using scheduledTime" in { + Task("task-a", 21747013400912L, {}) should be > Task("task-b", 21747013400138L, {}) + Task("task-a", 100L, {}) should be > Task("task-b", 99L, {}) + Task("task-a", 100L, {}) should be < Task("task-b", 101, {}) + Task("task-a", -19943L, {}) should be < Task("task-b", 0L, {}) + } + + "task with the same scheduledTime should not be equal" in { + Task("same-task?", 0L, {}) should not equal Task("same-task?", 0L, {}) + } + + "tasks which is only the same object should be equal" in { + def createNewTask = Task("same-task?", 0L, {}) + val task1 = createNewTask + val task2 = createNewTask + val task1_copy = task1 + task1 shouldEqual task1_copy + task1 should not equal task2 + } + + } + + "task can be sync executed by calling its main method." taggedAs Slow in { + + Thread.currentThread setName "parent-thread" + val data = StringBuilder("") + val task = Task("some-task", 0L, { + Thread.sleep(100) + data ++= Thread.currentThread.getName ++= " // " ++= "task-complete" + }) + task.main + data.toString shouldEqual "parent-thread // task-complete" + + } + +}