Skip to content

Commit

Permalink
fix: use a MacrotaskExecutor for ScalaJS
Browse files Browse the repository at this point in the history
  • Loading branch information
symbiont-eric-torreborre committed Sep 7, 2021
1 parent 93ceb9a commit 2a1242a
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 23 deletions.
4 changes: 2 additions & 2 deletions build.sbt
Expand Up @@ -66,6 +66,7 @@ lazy val commonJvmSettings =
import org.scalajs.linker.interface.ESVersion

lazy val commonJsSettings =
depends.jsMacrotaskExecutor ++
Seq(scalaJSLinkerConfig ~= { _.withESFeatures(_.withESVersion(ESVersion.ES2018)) }) ++
testJsSettings

Expand Down Expand Up @@ -326,5 +327,4 @@ lazy val aggregateCompile = ScopeFilter(
xml.jvm,
examples.jvm
),
inConfigurations(Compile)
)
inConfigurations(Compile))
Expand Up @@ -17,7 +17,7 @@ def runActionToFuture[A](
case Some(to) =>
given ExecutionContext = ee.executionContext
val promise = Promise[A]
ee.scheduler.schedule({ promise.tryFailure(new TimeoutException("timeout after " + to)); () }, to)
ee.schedule({ promise.tryFailure(new TimeoutException("timeout after " + to)); () }, to)
promise.completeWith(runNow(ee))
promise.future

Expand Down
16 changes: 6 additions & 10 deletions common/.js/src/main/scala/org/specs2/concurrent/ExecutionEnv.scala
Expand Up @@ -3,8 +3,7 @@ package concurrent

import org.specs2.control.Logger
import org.specs2.main.Arguments

import scala.concurrent.ExecutionContext
import scala.concurrent.*, duration.*

/** Execution environment for javascript
*/
Expand All @@ -13,22 +12,19 @@ case class ExecutionEnv(executorServices: ExecutorServices, timeFactor: Int):
def shutdown(): Unit = ()

given executionContext: ExecutionContext = executorServices.executionContext
given ec: ExecutionContext = executorServices.executionContext
given ec: ExecutionContext = executionContext

lazy val scheduler = executorServices.scheduler
def schedule(action: =>Unit, duration: FiniteDuration): Unit =
executorServices.schedule(action, duration)

object ExecutionEnv:

/** create an ExecutionEnv from an execution context only */
def fromExecutionContext(ec: =>ExecutionContext): ExecutionEnv =
ExecutionEnv(ExecutorServices.fromExecutionContext(ec), timeFactor = 1)

def create(arguments: Arguments, systemLogger: Logger, tag: Option[String] = None): ExecutionEnv =
createSpecs2(arguments, systemLogger, tag)

def createSpecs2(arguments: Arguments, systemLogger: Logger, tag: Option[String] = None): ExecutionEnv =
fromGlobalExecutionContext

/** create an ExecutionEnv from Scala global execution context */
/** create an ExecutionEnv from the MacrotaskExecutor, an ExecutionContext which truly works with timeouts */
def fromGlobalExecutionContext: ExecutionEnv =
fromExecutionContext(scala.concurrent.ExecutionContext.global)
ExecutionEnv(ExecutorServices.fromGlobalExecutionContext, timeFactor = 1)
Expand Up @@ -2,6 +2,7 @@ package org.specs2.concurrent

import scala.concurrent.ExecutionContext
import scala.concurrent.duration.FiniteDuration
import org.scalajs.macrotaskexecutor.*

/** Executor services for javascript
*
Expand All @@ -10,7 +11,6 @@ import scala.concurrent.duration.FiniteDuration
case class ExecutorServices(executionContextEval: () => ExecutionContext, schedulerEval: () => Scheduler) {

given executionContext: ExecutionContext = executionContextEval()

given scheduler: Scheduler = schedulerEval()

def shutdownNow(): Unit =
Expand All @@ -20,7 +20,9 @@ case class ExecutorServices(executionContextEval: () => ExecutionContext, schedu
def shutdownOnComplete[A](future: scala.concurrent.Future[A]): ExecutorServices =
this

def schedule(action: =>Unit, duration: FiniteDuration): () => Unit =
def schedule(action: =>Unit, duration: FiniteDuration): Unit =
// the timeout is started with a side effect
// the return value, which is a handler to clear the timeout is ignored
scheduler.schedule(action, duration)

}
Expand All @@ -31,12 +33,9 @@ object ExecutorServices {
lazy val specs2ThreadsNb: Int = 1

def fromExecutionContext(ec: =>ExecutionContext): ExecutorServices =
ExecutorServices(
() => ec,
() => Schedulers.default
)
ExecutorServices(() => ec, () => Schedulers.default)

def fromGlobalExecutionContext: ExecutorServices =
fromExecutionContext(scala.concurrent.ExecutionContext.global)
fromExecutionContext(MacrotaskExecutor)

}
Expand Up @@ -19,7 +19,7 @@ case class ExecutionEnv(executorServices: ExecutorServices, timeFactor: Int) {

}

object ExecutionEnv {
object ExecutionEnv:

/** create an ExecutionEnv from an execution context only */
def fromExecutionContext(ec: =>ExecutionContext): ExecutionEnv =
Expand All @@ -34,5 +34,3 @@ object ExecutionEnv {
/** create an ExecutionEnv from Scala global execution context */
def fromGlobalExecutionContext: ExecutionEnv =
fromExecutionContext(scala.concurrent.ExecutionContext.global)

}
@@ -0,0 +1,39 @@
package org.specs2
package concurrent

import scala.concurrent.*, duration.*
import LoopingCode.*
import scala.scalajs.*
import org.scalajs.macrotaskexecutor.MacrotaskExecutor
import specification.core.*
import runner.*

class MacrotaskExecutorSpec(env: Env) extends Specification:
def is = s2"""

An example can be timed-out when using ScalaJS $timeoutExample

"""

def timeoutExample =
given ExecutionContext = env.executionContext
TextRunner.runFuture(MacrotaskExecutorSpecification())(env).map { output =>
output.messages must contain(contain("timeout after 500 milliseconds"))
}

class MacrotaskExecutorSpecification extends Specification:
def is = args.execute(timeout = 500.millis) ^ s2"""

An example can be timed-out when using ScalaJS $timeoutExample

"""

def timeoutExample =
given ExecutionContext = MacrotaskExecutor
loop.map(_ => ok)

object LoopingCode:
var cancel = false

def loop(using executionContext: ExecutionContext): Future[Unit] =
Future(cancel).flatMap(canceled => if canceled then Future.unit else loop)
@@ -0,0 +1,40 @@
package org.specs2
package concurrent

import scala.concurrent.*, duration.*
import LoopingCode.*
import specification.core.*
import runner.*

class MacrotaskExecutorSpec(env: Env) extends Specification:
def is = s2"""

This specification is a copy of the coreJS/MacrotaskExecutorSpec to check
if timeouts work ok on the JVM

An example can be timed-out when using Scala on the $timeoutExample

"""

def timeoutExample =
given ExecutionContext = env.executionContext
TextRunner.runFuture(MacrotaskExecutorSpecification())(env).map { output =>
output.messages must contain(contain("timeout after 500 milliseconds"))
}

class MacrotaskExecutorSpecification extends Specification:
def is = args.execute(timeout = 500.millis) ^ s2"""

An example can be timed-out when using ScalaJS $timeoutExample

"""

def timeoutExample =
given ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
loop.map(_ => ok)

object LoopingCode:
var cancel = false

def loop(using executionContext: ExecutionContext): Future[Unit] =
Future(cancel).flatMap(canceled => if canceled then Future.unit else loop)
9 changes: 9 additions & 0 deletions core/src/main/scala/org/specs2/runner/ClassRunner.scala
Expand Up @@ -9,6 +9,7 @@ import reporter.*
import main.Arguments
import fp.syntax.*
import Runner.*
import scala.concurrent.*

trait ClassRunner:
def run(className: String): Action[Stats]
Expand Down Expand Up @@ -117,3 +118,11 @@ object TextRunner extends ClassRunnerMain:

action.runAction(env1.specs2ExecutionEnv)
logger

/** this method returns a Future and does not try to instantiate any class so it is suitable for ScalaJS */
def runFuture(spec: SpecificationStructure, arguments: Arguments = Arguments())(env: Env): Future[PrinterLogger & StringOutput] =
val logger = PrinterLogger.stringPrinterLogger
val env1 = env.setPrinterLogger(logger).setArguments(env.arguments.overrideWith(arguments))
given ExecutionContext = env1.executionContext
val reporter = Reporter.create(List(TextPrinter(env1)), env1)
reporter.report(spec.structure).runFuture(env1.specs2ExecutionEnv).map(_ => logger)
4 changes: 4 additions & 0 deletions project/depends.scala
Expand Up @@ -52,6 +52,10 @@ object depends {
)
)


def jsMacrotaskExecutor =
Seq(libraryDependencies += "org.scala-js" %%% "scala-js-macrotask-executor" % "0.1.0")

def jsTest =
Seq(
libraryDependencies ++=
Expand Down

0 comments on commit 2a1242a

Please sign in to comment.