or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

cross-platform.mdevent-handling.mdindex.mdsbt-framework.mdtest-runners.md
tile.json

cross-platform.mddocs/

Cross-Platform Support

This document covers ZIO Test SBT's support for JavaScript and Native platforms, including specialized runners and communication protocols.

Platform Architecture

ZIO Test SBT supports three platforms with unified APIs but platform-specific optimizations:

  • JVM: Full-featured implementation with signal handling and advanced runtime features
  • JavaScript (Scala.js): Asynchronous execution with master/slave runner patterns
  • Native (Scala Native): Optimized for native compilation with efficient concurrency primitives

Summary Protocol

JavaScript and Native platforms use string-based communication for distributed testing scenarios.

object SummaryProtocol {
  def serialize(summary: zio.test.Summary): String
  def deserialize(s: String): Option[zio.test.Summary]
  def escape(token: String): String
  def unescape(token: String): String
}

serialize

Converts a ZIO test summary to a serialized string format.

val summary = zio.test.Summary(success = 5, fail = 1, ignore = 2, failureDetails = "Test failed")
val serialized = SummaryProtocol.serialize(summary)
// Returns: "5\t1\t2\tTest failed" (tab-separated values)

Format: success\tfail\tignore\tfailureDetails

deserialize

Parses a serialized summary string back to a Summary object.

val serialized = "5\t1\t2\tTest failed"
val summary = SummaryProtocol.deserialize(serialized)
// Returns: Some(Summary(5, 1, 2, "Test failed"))

Returns: Some(Summary) if parsing succeeds, None if format is invalid

escape / unescape

Handle tab character escaping in summary strings.

val withTabs = "Message\twith\ttabs"
val escaped = SummaryProtocol.escape(withTabs)
// Returns: "Message\\twith\\ttabs"

val unescaped = SummaryProtocol.unescape(escaped)  
// Returns: "Message\twith\ttabs"

JavaScript Platform

Framework Implementation

JavaScript implementation supports distributed testing with master/slave runners.

final class ZTestFramework extends sbt.testing.Framework {
  override final val name: String
  val fingerprints: Array[sbt.testing.Fingerprint]
  
  override def runner(args: Array[String], remoteArgs: Array[String], testClassLoader: ClassLoader): sbt.testing.Runner
  override def slaveRunner(args: Array[String], remoteArgs: Array[String], testClassLoader: ClassLoader, send: String => Unit): sbt.testing.Runner
}

Master Runner

Manages single-process test execution.

final class ZMasterTestRunnerJS(
  args: Array[String],
  remoteArgs: Array[String],
  testClassLoader: ClassLoader
) extends ZTestRunnerJS(args, remoteArgs, testClassLoader, "master") {
  override val sendSummary: SendSummary
}

Summary Handling: Collects summaries in a mutable buffer for local aggregation.

override val sendSummary: SendSummary = SendSummary.fromSend { summary =>
  summaries += summary
  ()
}

Slave Runner

Handles distributed test execution with custom summary transmission.

final class ZSlaveTestRunnerJS(
  args: Array[String],
  remoteArgs: Array[String], 
  testClassLoader: ClassLoader,
  val sendSummary: SendSummary
) extends ZTestRunnerJS(args, remoteArgs, testClassLoader, "slave")

Summary Handling: Uses provided SendSummary that typically serializes and sends summaries to master process.

Base JavaScript Runner

sealed abstract class ZTestRunnerJS(
  val args: Array[String],
  val remoteArgs: Array[String],
  testClassLoader: ClassLoader,
  runnerType: String
) extends sbt.testing.Runner {
  
  def sendSummary: SendSummary
  val summaries: scala.collection.mutable.Buffer[zio.test.Summary]
  
  def done(): String
  def tasks(defs: Array[sbt.testing.TaskDef]): Array[sbt.testing.Task]
  def receiveMessage(summary: String): Option[String]
  def serializeTask(task: sbt.testing.Task, serializer: sbt.testing.TaskDef => String): String
  def deserializeTask(task: String, deserializer: String => sbt.testing.TaskDef): sbt.testing.Task
}

receiveMessage

Handles incoming summary messages from distributed testing.

override def receiveMessage(summary: String): Option[String] = {
  SummaryProtocol.deserialize(summary).foreach(s => summaries += s)
  None
}

Task Serialization

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

override def deserializeTask(task: String, deserializer: String => sbt.testing.TaskDef): sbt.testing.Task =
  ZTestTask(deserializer(task), testClassLoader, runnerType, sendSummary, TestArgs.parse(args))

JavaScript Test Task

Asynchronous test execution with callback-based completion.

sealed class ZTestTask(
  taskDef: sbt.testing.TaskDef,
  testClassLoader: ClassLoader,
  runnerType: String,
  sendSummary: SendSummary,
  testArgs: zio.test.TestArgs,
  spec: zio.test.ZIOSpecAbstract
) extends BaseTestTask(taskDef, testClassLoader, sendSummary, testArgs, spec, zio.Runtime.default, zio.Console.ConsoleLive)

Asynchronous Execution

def execute(eventHandler: sbt.testing.EventHandler, loggers: Array[sbt.testing.Logger], continuation: Array[sbt.testing.Task] => Unit): Unit = {
  val fiber = Runtime.default.unsafe.fork { /* test execution logic */ }
  fiber.unsafe.addObserver { exit =>
    exit match {
      case Exit.Failure(cause) => Console.err.println(s"$runnerType failed. $cause")
      case _ =>
    }
    continuation(Array())
  }
}

Native Platform

Framework Implementation

Similar to JavaScript but optimized for native compilation.

final class ZTestFramework extends sbt.testing.Framework {
  override def name(): String
  override def fingerprints(): Array[sbt.testing.Fingerprint]
  override def runner(args: Array[String], remoteArgs: Array[String], testClassLoader: ClassLoader): sbt.testing.Runner
  override def slaveRunner(args: Array[String], remoteArgs: Array[String], testClassLoader: ClassLoader, send: String => Unit): sbt.testing.Runner
}

Native Runners

final class ZMasterTestRunner(
  args: Array[String],
  remoteArgs: Array[String],
  testClassLoader: ClassLoader
) extends ZTestRunnerNative(args, remoteArgs, testClassLoader, "master")

final class ZSlaveTestRunner(
  args: Array[String],
  remoteArgs: Array[String],
  testClassLoader: ClassLoader,
  sendSummary: SendSummary
) extends ZTestRunnerNative(args, remoteArgs, testClassLoader, "slave")

Base Native Runner

Uses ConcurrentLinkedQueue for thread-safe summary collection.

sealed abstract class ZTestRunnerNative(
  val args: Array[String],
  remoteArgs0: Array[String],
  testClassLoader: ClassLoader,
  runnerType: String
) extends sbt.testing.Runner {
  
  def remoteArgs(): Array[String]
  val summaries: java.util.concurrent.ConcurrentLinkedQueue[zio.test.Summary]
  
  def done(): String
}

Summary Collection

def done(): String = {
  val log = new StringBuilder
  var summary = summaries.poll()
  var total = 0
  var ignore = 0
  val isEmpty = summary eq null

  while (summary ne null) {
    total += summary.total
    ignore += summary.ignore
    val details = summary.failureDetails
    if (!details.isBlank) {
      log append colored(details)
      log append '\n'
    }
    summary = summaries.poll()
  }

  if (isEmpty || total == ignore)
    s"${Console.YELLOW}No tests were executed${Console.RESET}"
  else
    log.append("Done").result()
}

Native Test Task

Blocking execution model suitable for native compilation.

sealed class ZTestTask(
  taskDef: sbt.testing.TaskDef,
  testClassLoader: ClassLoader,
  runnerType: String,
  sendSummary: SendSummary,
  testArgs: zio.test.TestArgs,
  spec: zio.test.ZIOSpecAbstract
) extends BaseTestTask(taskDef, testClassLoader, sendSummary, testArgs, spec, zio.Runtime.default, zio.Console.ConsoleLive)

Blocking Execution

override def execute(eventHandler: sbt.testing.EventHandler, loggers: Array[sbt.testing.Logger]): Array[sbt.testing.Task] = {
  var resOutter: CancelableFuture[Unit] = null
  try {
    resOutter = Runtime.default.unsafe.runToFuture { /* test execution logic */ }
    Await.result(resOutter, Duration.Inf)
    Array()
  } catch {
    case t: Throwable =>
      if (resOutter != null) resOutter.cancel()
      throw t
  }
}

Platform-Specific Optimizations

JVM Platform

  • Signal Handling: SIGINFO/SIGUSR1 for fiber dumping
  • Advanced Runtime: Full ZIO runtime with all features
  • Parallel Execution: Thread-based parallelism
  • Memory Management: JVM garbage collection

JavaScript Platform

  • Asynchronous Execution: Callback-based completion
  • Single-Threaded: Event loop concurrency
  • Master/Slave Pattern: Distributed testing support
  • Lightweight Runtime: Reduced ZIO runtime for browser/Node.js

Native Platform

  • Optimized Runtime: Minimal ZIO runtime for native compilation
  • Blocking Execution: Synchronous execution model
  • Memory Efficiency: Manual memory management optimizations
  • Fast Startup: Ahead-of-time compilation benefits

Usage Examples

Platform Detection

// Detect current platform
val platform = scala.util.Properties.propOrEmpty("java.specification.name") match {
  case name if name.contains("Java") => "JVM"
  case _ => 
    if (scala.util.Properties.propOrEmpty("scala.scalajs.runtime.name").nonEmpty) "JavaScript"
    else "Native"
}

println(s"Running on: $platform")

Cross-Platform Runner Configuration

import zio.test.sbt._

// Create appropriate runner for platform
val runner: sbt.testing.Runner = platform match {
  case "JVM" => new ZTestRunnerJVM(args, remoteArgs, classLoader)
  case "JavaScript" => new ZMasterTestRunnerJS(args, remoteArgs, classLoader)
  case "Native" => new ZMasterTestRunner(args, remoteArgs, classLoader)
}

Summary Protocol Usage

// Master process: serialize and send summary
val summary = zio.test.Summary(success = 10, fail = 2, ignore = 1, failureDetails = "Some tests failed")
val serialized = SummaryProtocol.serialize(summary)
sendToSlave(serialized)

// Slave process: receive and deserialize summary
def receiveSummary(data: String): Unit = {
  SummaryProtocol.deserialize(data) match {
    case Some(summary) => 
      println(s"Received: ${summary.total} tests, ${summary.fail} failures")
    case None => 
      println("Invalid summary format")
  }
}