All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eaming.weaver-framework_2.13.0.8.4.source-code.RunnerCompat.scala Maven / Gradle / Ivy

package weaver
package framework

import java.io.PrintStream
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger }

import scala.concurrent.duration._
import scala.concurrent.{ ExecutionContext, Promise }
import scala.util.Try

import cats.data.Chain
import cats.effect.std.Semaphore
import cats.effect.{ Ref, Sync, _ }
import cats.syntax.all._

import sbt.testing.{ Task, TaskDef }

trait RunnerCompat[F[_]] { self: sbt.testing.Runner =>

  protected val suiteLoader: SuiteLoader[F]
  protected val unsafeRun: UnsafeRun[F]
  protected val errorStream: PrintStream
  import unsafeRun._

  private type MakeSuite = GlobalResourceF.Read[F] => F[EffectSuite[F]]

  private var cancelToken: Option[unsafeRun.CancelToken] = None

  @volatile private var failedOutcomes: Chain[(SuiteName, TestOutcome)] =
    Chain.empty

  override def done(): String = {
    isDone.set(true)
    cancelToken.foreach(unsafeRun.cancel)
    ""
  }

  // Required on js
  def receiveMessage(msg: String): Option[String] = None

  // Flag meant to be raised if build-tool call `done`
  protected val isDone: AtomicBoolean = new AtomicBoolean(false)

  private def runBackground(
      globalResources: List[GlobalResourceF[F]],
      waitForResourcesShutdown: java.util.concurrent.Semaphore,
      tasks: List[IOTask],
      gate: Promise[Unit]): Unit = {
    cancelToken = Some(unsafeRun.background(run(globalResources,
                                                waitForResourcesShutdown,
                                                tasks,
                                                gate)))
  }

  def tasks(taskDefs: Array[TaskDef]): Array[Task] = {
    val stillRunning             = new AtomicInteger(0)
    val waitForResourcesShutdown = new java.util.concurrent.Semaphore(0)

    val tasksAndSuites = taskDefs.toList.map { taskDef =>
      taskDef -> suiteLoader(taskDef)
    }.collect { case (taskDef, Some(suite)) => (taskDef, suite) }

    def makeTasks(
        taskDef: TaskDef,
        mkSuite: MakeSuite): (IOTask, Task) = {
      val promise = scala.concurrent.Promise[Unit]()
      if (args().contains("--quickstart") || args().contains("-qs"))
        promise.success(())

      // 1 permit semaphore protecting against build tools not
      // dispatching logs through a single logger at a time.
      val loggerPermit = new java.util.concurrent.Semaphore(1, true)

      val queue  = new ConcurrentLinkedQueue[SuiteEvent]()
      val broker = new ConcurrentQueueEventBroker(queue)
      val startingBlock = unsafeRun.fromFuture {
        promise.future.map(_ => ())(ExecutionContext.global)
      }

      val ioTask =
        IOTask(
          taskDef.fullyQualifiedName(),
          mkSuite,
          args.toList,
          startingBlock,
          broker)

      val sbtTask =
        new SbtTask(taskDef,
                    isDone,
                    stillRunning,
                    waitForResourcesShutdown,
                    promise,
                    queue,
                    loggerPermit,
                    () => failedOutcomes)
      (ioTask, sbtTask)
    }

    val (ioTasks, sbtTasks) = tasksAndSuites.collect {
      case (taskDef, suiteLoader.SuiteRef(mkSuite)) =>
        makeTasks(taskDef, _ => mkSuite)
      case (taskDef, suiteLoader.ResourcesSharingSuiteRef(mkSuite)) =>
        makeTasks(taskDef, mkSuite)
    }.unzip

    val globalResources = tasksAndSuites.collect {
      case (_, suiteLoader.GlobalResourcesRef(init)) => init
    }.toList

    // Passing a promise to the FP side that needs to be fulfilled
    // when the global resources have been allocated.
    val gate = Promise[Unit]()
    runBackground(globalResources,
                  waitForResourcesShutdown,
                  ioTasks.toList,
                  gate)
    stillRunning.set(sbtTasks.size)

    val maybeTimeout: Option[FiniteDuration] =
      sys.props.get("weaver.test.startupTimeout")
        .flatMap(timeoutString => Try(timeoutString.toInt).toOption)
        .map(_.seconds)

    // Waiting for the resources to be allocated.
    scala.concurrent.blocking {
      scala.concurrent.Await.result(gate.future,
                                    maybeTimeout.getOrElse(120.second))
    }
    sbtTasks.toArray
  }

  def serializeTask(task: Task, serializer: TaskDef => String): String =
    serializer(task.taskDef())

  def deserializeTask(
      task: String,
      deserializer: String => TaskDef): Task = {
    tasks(Array(deserializer(task))).head
  }

  private[this] def onErrorEnsure[A](r: Resource[F, A])(
      f: Throwable => F[Unit]): Resource[F, A] = {
    import Resource.ExitCase._
    r.onFinalizeCase {
      case Canceled   => Async[F].unit
      case Succeeded  => Async[F].unit
      case Errored(e) => f(e)
    }
  }

  private def run(
      globalResources: List[GlobalResourceF[F]],
      waitForResourcesShutdown: java.util.concurrent.Semaphore,
      tasks: List[IOTask],
      gate: Promise[Unit]): F[Unit] = {

    def preventDeadlock[A](resource: Resource[F, A]) = {
      onErrorEnsure(resource) {
        error =>
          effect.delay(isDone.set(true)) *>
            effect.delay(error.printStackTrace(errorStream)) *>
            effect.delay(gate.failure(error))
      }
    }

    val cleanupResource = Resource.make(effect.unit)(_ =>
      effect.delay(waitForResourcesShutdown.release()))

    val globalResourcesWithCompletion =
      cleanupResource.flatMap(_ => resourceMap(globalResources))

    preventDeadlock(globalResourcesWithCompletion).use { read =>
      for {
        _   <- effect.delay(gate.success(()))
        ref <- Ref.of[F, Chain[(SuiteName, TestOutcome)]](Chain.empty)
        sem <- Semaphore[F](0L)
        maybePublish: F[Unit] =
          sem.release
            .productR(sem.tryAcquireN(tasks.size.toLong))
            .flatMap { isLast =>
              ref.get.flatMap { failed =>
                unsafeRun
                  .effect
                  .delay(self.failedOutcomes = failed)
              }.whenA(isLast)
            }
        _ <- tasks.parTraverse(_.run(read, ref, maybePublish))
      } yield ()
    }

  }

  private def resourceMap(
      globalResources: List[GlobalResourceF[F]]
  ): Resource[F, GlobalResourceF.Read[F]] =
    Resource.eval(GlobalResourceF.createMap[F]).flatTap { map =>
      globalResources.traverse(_.sharedResources(map)).void
    }

  private case class IOTask(
      fqn: String,
      mkSuite: MakeSuite,
      args: List[String],
      start: F[Unit],
      broker: SuiteEventBroker) {
    def run(
        globalResources: GlobalResourceF.Read[F],
        outcomes: Ref[F, Chain[(SuiteName, TestOutcome)]],
        maybePublish: F[Unit]): F[Unit] = {

      val runSuite = for {
        suite <- mkSuite(globalResources)
        _     <- start // waiting for SBT to tell us to start
        _     <- broker.send(SuiteStarted(SuiteName(fqn)))
        _ <- suite.run(args) { testOutcome =>
          outcomes
            .update(_.append(SuiteName(fqn) -> testOutcome))
            .whenA(testOutcome.status.isFailed)
            .productR(broker.send(TestFinished(testOutcome)))
        }
      } yield ()

      val finalizer =
        maybePublish.productR(broker.send(SuiteFinished(SuiteName(fqn))))

      Async[F].guaranteeCase(runSuite)(_.fold(
        completed = _ *> finalizer,
        canceled = finalizer,
        errored = { (error: Throwable) =>
          val outcome =
            TestOutcome("Unexpected failure",
                        0.seconds,
                        Result.from(error),
                        Chain.empty)

          Async[F].guarantee(outcomes
                               .update(_.append(SuiteName(fqn) -> outcome))
                               .productR(broker.send(TestFinished(outcome))),
                             finalizer)
        }
      )).handleErrorWith { case scala.util.control.NonFatal(_) =>
        effect.unit // avoid non-fatal errors propagating up
      }
    }
  }

  trait SuiteEventBroker {
    def send(suiteEvent: SuiteEvent): F[Unit]
  }

  class ConcurrentQueueEventBroker(
      concurrentQueue: ConcurrentLinkedQueue[SuiteEvent])
      extends SuiteEventBroker {
    def send(suiteEvent: SuiteEvent): F[Unit] = {
      Sync[F].delay(concurrentQueue.add(suiteEvent)).void
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy