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
—
Test execution system providing task creation, lifecycle management, and support for both synchronous and asynchronous JavaScript execution patterns.
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")
}
}
}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
}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"))
)// 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