This document covers ZIO Test SBT's support for JavaScript and Native platforms, including specialized runners and communication protocols.
ZIO Test SBT supports three platforms with unified APIs but platform-specific optimizations:
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
}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
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
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 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
}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
()
}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.
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
}Handles incoming summary messages from distributed testing.
override def receiveMessage(summary: String): Option[String] = {
SummaryProtocol.deserialize(summary).foreach(s => summaries += s)
None
}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))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)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())
}
}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
}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")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
}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()
}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)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
}
}// 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")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)
}// 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")
}
}