CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-scala-js--scalajs-test-interface

Scala.js version of the sbt testing interface that provides a standardized API for test frameworks to integrate with SBT and run tests in a Scala.js (JavaScript) environment

Pending
Overview
Eval results
Files

execution.mddocs/

Test Execution

Test execution system providing task creation, lifecycle management, and support for both synchronous and asynchronous JavaScript execution patterns.

Capabilities

Runner Interface

Manages the lifecycle of a test run, creating tasks from task definitions and coordinating distributed execution.

/**
 * Represents one run of a suite of tests.
 * Has a lifecycle: instantiation -> tasks() calls -> done()
 * After done() is called, the Runner enters "spent" mode and tasks() throws IllegalStateException.
 */
trait Runner {
  /**
   * Returns an array of tasks that when executed will run tests and suites.
   * @param taskDefs the TaskDefs for requested tasks
   * @return an array of Tasks
   * @throws IllegalStateException if invoked after done() has been called
   */
  def tasks(taskDefs: Array[TaskDef]): Array[Task]
  
  /**
   * Indicates the client is done with this Runner instance.
   * Must not call task() methods after this.
   * In Scala.js, client must not call before all Task.execute completions.
   * @return a possibly multi-line summary string, or empty string if no summary
   * @throws IllegalStateException if called more than once
   */
  def done(): String
  
  /**
   * Remote args that will be passed to Runner in a sub-process as remoteArgs.
   * @return array of strings for sub-process remoteArgs
   */
  def remoteArgs(): Array[String]
  
  /** Returns the arguments that were used to create this Runner. */
  def args: Array[String]
  
  /**
   * Scala.js specific: Message handling for controller/worker communication.
   * For controller: invoked when worker sends message, can return response in Some
   * For worker: invoked when controller responds, return value ignored
   * @param msg message received
   * @return optional response message (controller only)
   */
  def receiveMessage(msg: String): Option[String]
  
  /**
   * Scala.js specific: Serialize a task for distribution to another runner.
   * After calling this method, the passed task becomes invalid.
   * @param task task to serialize
   * @param serializer function to serialize TaskDef
   * @return serialized task string
   */
  def serializeTask(task: Task, serializer: TaskDef => String): String
  
  /**
   * Scala.js specific: Deserialize a task from another runner.
   * The resulting task associates with this runner.
   * @param task serialized task string
   * @param deserializer function to deserialize TaskDef
   * @return deserialized Task
   */
  def deserializeTask(task: String, deserializer: String => TaskDef): Task
}

Usage Examples:

import sbt.testing._

class MyTestRunner(
  val args: Array[String],
  val remoteArgs: Array[String],
  testClassLoader: ClassLoader
) extends Runner {
  
  private var isSpent = false
  private val completedTasks = mutable.Set[Task]()
  
  def tasks(taskDefs: Array[TaskDef]): Array[Task] = {
    if (isSpent) {
      throw new IllegalStateException("Runner is spent - done() already called")
    }
    
    taskDefs.flatMap { taskDef =>
      // Create task if we can handle this test class
      if (canHandle(taskDef)) {
        Some(new MyTask(taskDef, this))
      } else {
        None // Reject this task
      }
    }
  }
  
  def done(): String = {
    if (isSpent) {
      throw new IllegalStateException("done() already called")
    }
    isSpent = true
    
    val summary = s"Completed ${completedTasks.size} tasks"
    cleanup()
    summary
  }
  
  // Message handling for distributed testing
  def receiveMessage(msg: String): Option[String] = {
    msg match {
      case "status" => Some(s"completed:${completedTasks.size}")
      case "shutdown" => 
        cleanup()
        Some("acknowledged")
      case _ => None
    }
  }
  
  // Task serialization for worker distribution
  def serializeTask(task: Task, serializer: TaskDef => String): String = {
    val taskDefStr = serializer(task.taskDef())
    s"MyTask:$taskDefStr"
  }
  
  def deserializeTask(task: String, deserializer: String => TaskDef): Task = {
    task match {
      case s"MyTask:$taskDefStr" =>
        val taskDef = deserializer(taskDefStr)
        new MyTask(taskDef, this)
      case _ =>
        throw new IllegalArgumentException(s"Cannot deserialize task: $task")
    }
  }
}

Task Interface

Represents an executable unit of work that runs tests and can produce additional tasks.

/**
 * A task to execute.
 * Can be any job, but primarily intended for running tests and/or supplying more tasks.
 */
trait Task {
  /**
   * A possibly zero-length array of string tags associated with this task.
   * Used for task scheduling and resource management.
   * @return array of this task's tags
   */
  def tags(): Array[String]
  
  /**
   * Executes this task, possibly returning new tasks to execute.
   * @param eventHandler event handler to fire events during the run
   * @param loggers array of loggers to emit log messages during the run
   * @return possibly empty array of new tasks for the client to execute
   */
  def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task]
  
  /**
   * Scala.js specific: Async execute with continuation support.
   * This method will be called instead of synchronous execute in JavaScript environments.
   * @param eventHandler event handler to fire events during the run
   * @param loggers array of loggers to emit log messages during the run
   * @param continuation called with result tasks when execution completes
   */
  def execute(eventHandler: EventHandler, loggers: Array[Logger],
             continuation: Array[Task] => Unit): Unit
  
  /**
   * Returns the TaskDef that was used to request this Task.
   * @return the TaskDef used to request this Task
   */
  def taskDef(): TaskDef
}

Usage Examples:

class MyTask(taskDef: TaskDef, runner: Runner) extends Task {
  
  def tags(): Array[String] = {
    // Tag CPU-intensive tests
    if (isCpuIntensive(taskDef.fullyQualifiedName())) {
      Array("cpu-intensive")
    } else {
      Array.empty
    }
  }
  
  // Synchronous execution (JVM)
  def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task] = {
    val logger = loggers.headOption.getOrElse(NoOpLogger)
    
    try {
      logger.info(s"Running test: ${taskDef.fullyQualifiedName()}")
      
      // Load and execute test class
      val testInstance = loadTestClass(taskDef.fullyQualifiedName())
      val results = runTests(testInstance, taskDef.selectors())
      
      // Fire events for each test result
      results.foreach { result =>
        val event = createEvent(result, taskDef)
        eventHandler.handle(event)
      }
      
      // Return any nested tasks
      createNestedTasks(results)
      
    } catch {
      case t: Throwable =>
        logger.error(s"Test execution failed: ${t.getMessage}")
        val errorEvent = createErrorEvent(t, taskDef)
        eventHandler.handle(errorEvent)
        Array.empty
    }
  }
  
  // Asynchronous execution (JavaScript)
  def execute(eventHandler: EventHandler, loggers: Array[Logger],
             continuation: Array[Task] => Unit): Unit = {
    val logger = loggers.headOption.getOrElse(NoOpLogger)
    
    // Use JavaScript's async capabilities
    scala.scalajs.js.timers.setTimeout(0) {
      try {
        logger.info(s"Async running test: ${taskDef.fullyQualifiedName()}")
        
        val testInstance = loadTestClass(taskDef.fullyQualifiedName())
        
        // Run async tests with Promise-based execution
        runAsyncTests(testInstance, taskDef.selectors()) { results =>
          results.foreach { result =>
            val event = createEvent(result, taskDef)
            eventHandler.handle(event)
          }
          
          val nestedTasks = createNestedTasks(results)
          continuation(nestedTasks)
        }
        
      } catch {
        case t: Throwable =>
          logger.error(s"Async test execution failed: ${t.getMessage}")
          val errorEvent = createErrorEvent(t, taskDef)
          eventHandler.handle(errorEvent)
          continuation(Array.empty)
      }
    }
  }
  
  def taskDef(): TaskDef = taskDef
}

TaskDef Class

Bundles information used to request a Task from a test framework.

/**
 * Information bundle used to request a Task from a test framework.
 * @param fullyQualifiedName fully qualified name of the test class
 * @param fingerprint indicates how the test suite was identified
 * @param explicitlySpecified whether test class was explicitly specified by user
 * @param selectors array of Selectors determining suites and tests to run
 */
final class TaskDef(
  fullyQualifiedName: String,
  fingerprint: Fingerprint,
  explicitlySpecified: Boolean,
  selectors: Array[Selector]
) extends Serializable {
  
  /** The fully qualified name of the test class requested by this TaskDef. */
  def fullyQualifiedName(): String
  
  /** The fingerprint that the test class requested by this TaskDef matches. */
  def fingerprint(): Fingerprint
  
  /** 
   * Indicates whether the test class was "explicitly specified" by the user.
   * True for commands like: test-only com.mycompany.myproject.WholeNameSpec
   * False for commands like: test-only *WholeNameSpec or test
   */
  def explicitlySpecified(): Boolean
  
  /** 
   * Selectors describing the nature of the Task requested by this TaskDef.
   * Can indicate direct user requests or "rerun" of previously run tests.
   */
  def selectors(): Array[Selector]
}

Usage:

// Create TaskDef for explicit test class
val taskDef = new TaskDef(
  fullyQualifiedName = "com.example.MyTestSuite",
  fingerprint = mySubclassFingerprint,
  explicitlySpecified = true,
  selectors = Array(new SuiteSelector())
)

// Create TaskDef for specific test method
val testTaskDef = new TaskDef(
  fullyQualifiedName = "com.example.MyTestSuite",
  fingerprint = mySubclassFingerprint,
  explicitlySpecified = false,
  selectors = Array(new TestSelector("shouldCalculateCorrectly"))
)

Execution Lifecycle

  1. Task Creation: Runner.tasks() creates Task instances from TaskDef array
  2. Task Scheduling: Client schedules tasks based on tags and resources
  3. Synchronous/Async Execution: Task.execute() called with appropriate method
  4. Event Reporting: Tasks fire events through EventHandler during execution
  5. Nested Task Generation: Tasks can return additional tasks for execution
  6. Completion: All tasks complete and Runner.done() is called
  7. Cleanup: Runner resources cleaned up and becomes "spent"

Error Handling

IllegalStateException

  • Runner.tasks(): Thrown if called after Runner.done()
  • Runner.done(): Thrown if called multiple times
  • JavaScript execution: Thrown if done() called before all task continuations complete
// Proper error handling in runner
def tasks(taskDefs: Array[TaskDef]): Array[Task] = {
  synchronized {
    if (isSpent) {
      throw new IllegalStateException("Runner is spent")
    }
    // ... create tasks
  }
}

Install with Tessl CLI

npx tessl i tessl/maven-org-scala-js--scalajs-test-interface

docs

discovery.md

events.md

execution.md

framework.md

index.md

logging.md

tile.json