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

org.scalajs.testing.adapter.TestAdapter.scala Maven / Gradle / Ivy

The newest version!
/*
 * Scala.js (https://www.scala-js.org/)
 *
 * Copyright EPFL.
 *
 * Licensed under Apache License 2.0
 * (https://www.apache.org/licenses/LICENSE-2.0).
 *
 * See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 */

package org.scalajs.testing.adapter

import scala.concurrent._
import scala.concurrent.duration._
import scala.collection.concurrent.TrieMap

import org.scalajs.logging._

import org.scalajs.jsenv._
import org.scalajs.jsenv.JSUtils.escapeJS

import org.scalajs.testing.common._

import sbt.testing.Framework

final class TestAdapter(jsEnv: JSEnv, input: Seq[Input], config: TestAdapter.Config) {

  import TestAdapter._

  require(input.nonEmpty,
      "Attempted to create a TestAdapter with empty input. " +
      "This will not work, since the TestAdapter expects replies from the JS end.")

  /** Map of ThreadId -> ManagedRunner */
  private[this] val runners = TrieMap.empty[Long, ManagedRunner]

  /** State management. May only be accessed under synchronization. */
  private[this] var closed = false
  private[this] var nextRunID = 0
  private[this] var runs = Set.empty[RunMux.RunID]

  /** A custom execution context that delegates to the global one for execution,
   *  but handles failures internally.
   */
  private implicit val executionContext: ExecutionContext =
    ExecutionContext.fromExecutor(ExecutionContext.global, reportFailure)

  /** Creates an `sbt.testing.Framework` for each framework that can be found.
   *
   *  The returned Frameworks bind to this TestAdapter and are only valid until
   *  [[close]] is called.
   */
  def loadFrameworks(frameworkNames: List[List[String]]): List[Option[Framework]] = {
    getRunnerForThread().com
      .call(JSEndpoints.detectFrameworks)(frameworkNames)
      .map(_.map(_.map(info => new FrameworkAdapter(info, this))))
      .await()
  }

  /** Releases all resources. All associated runs must be done. */
  def close(): Unit = synchronized {
    val runInfo =
      if (runs.isEmpty) "All runs have completed."
      else s"Incomplete runs: $runs"

    val msg = "TestAdapter.close() was called. " + runInfo

    if (runs.nonEmpty)
      config.logger.warn(msg)

    /* This is the exception callers will see if they are still pending.
     * That's why it is an IllegalStateException.
     */
    val cause = new IllegalStateException(msg)
    stopEverything(cause)
  }

  /** Called when a throwable bubbles up the execution stack.
   *
   *  We terminate everything if this happens to make sure nothing hangs waiting
   *  on an async operation to complete.
   */
  private def reportFailure(cause: Throwable): Unit = {
    val msg = "Failure in async execution. Aborting all test runs."
    val error = new AssertionError(msg, cause)
    config.logger.error(msg)
    config.logger.trace(error)
    stopEverything(error)
  }

  private def stopEverything(cause: Throwable): Unit = synchronized {
    if (!closed) {
      closed = true
      runners.values.foreach(_.com.close(cause))
      runners.clear()
    }
  }

  private[adapter] def runStarting(): RunMux.RunID = synchronized {
    require(!closed, "We are closed. Cannot create new run.")
    val runID = nextRunID
    nextRunID += 1
    runs += runID
    runID
  }

  /** Called by [[RunnerAdapter]] when the run is completed. */
  private[adapter] def runDone(runID: RunMux.RunID): Unit = synchronized {
    require(runs.contains(runID), s"Tried to remove nonexistent run $runID")
    runs -= runID
  }

  private[adapter] def getRunnerForThread(): ManagedRunner = {
    val threadId = Thread.currentThread().getId()

    // Note that this is thread safe, since each thread can only operate on
    // the value associated to its thread id.
    runners.getOrElseUpdate(threadId, startManagedRunner(threadId))
  }

  private def startManagedRunner(threadId: Long): ManagedRunner = synchronized {
    // Prevent runners from being started after we are closed.
    // Otherwise we might leak runners.
    require(!closed, "We are closed. Cannot create new runner.")

    val com = new JSEnvRPC(jsEnv, input, config.logger, config.env)
    val mux = new RunMuxRPC(com)

    new ManagedRunner(threadId, com, mux)
  }
}

object TestAdapter {
  final class Config private (
      val logger: Logger,
      val env: Map[String, String]
  ) {
    private def this() = {
      this(
          logger = NullLogger,
          env = Map.empty
      )
    }

    def withLogger(logger: Logger): Config =
      copy(logger = logger)

    def withEnv(env: Map[String, String]): Config =
      copy(env = env)

    private def copy(
        logger: Logger = logger,
        env: Map[String, String] = env
    ): Config = {
      new Config(logger, env)
    }
  }

  object Config {
    def apply(): Config = new Config()
  }

  private[adapter] final class ManagedRunner(
      val id: Long,
      val com: RPCCore,
      val mux: RunMuxRPC
  )
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy