0
# Cross-Platform Support
1
2
This document covers ZIO Test SBT's support for JavaScript and Native platforms, including specialized runners and communication protocols.
3
4
## Platform Architecture
5
6
ZIO Test SBT supports three platforms with unified APIs but platform-specific optimizations:
7
8
- **JVM**: Full-featured implementation with signal handling and advanced runtime features
9
- **JavaScript (Scala.js)**: Asynchronous execution with master/slave runner patterns
10
- **Native (Scala Native)**: Optimized for native compilation with efficient concurrency primitives
11
12
## Summary Protocol
13
14
JavaScript and Native platforms use string-based communication for distributed testing scenarios.
15
16
```scala { .api }
17
object SummaryProtocol {
18
def serialize(summary: zio.test.Summary): String
19
def deserialize(s: String): Option[zio.test.Summary]
20
def escape(token: String): String
21
def unescape(token: String): String
22
}
23
```
24
25
### serialize
26
27
Converts a ZIO test summary to a serialized string format.
28
29
```scala
30
val summary = zio.test.Summary(success = 5, fail = 1, ignore = 2, failureDetails = "Test failed")
31
val serialized = SummaryProtocol.serialize(summary)
32
// Returns: "5\t1\t2\tTest failed" (tab-separated values)
33
```
34
35
**Format**: `success\tfail\tignore\tfailureDetails`
36
37
### deserialize
38
39
Parses a serialized summary string back to a `Summary` object.
40
41
```scala
42
val serialized = "5\t1\t2\tTest failed"
43
val summary = SummaryProtocol.deserialize(serialized)
44
// Returns: Some(Summary(5, 1, 2, "Test failed"))
45
```
46
47
**Returns**: `Some(Summary)` if parsing succeeds, `None` if format is invalid
48
49
### escape / unescape
50
51
Handle tab character escaping in summary strings.
52
53
```scala
54
val withTabs = "Message\twith\ttabs"
55
val escaped = SummaryProtocol.escape(withTabs)
56
// Returns: "Message\\twith\\ttabs"
57
58
val unescaped = SummaryProtocol.unescape(escaped)
59
// Returns: "Message\twith\ttabs"
60
```
61
62
## JavaScript Platform
63
64
### Framework Implementation
65
66
JavaScript implementation supports distributed testing with master/slave runners.
67
68
```scala { .api }
69
final class ZTestFramework extends sbt.testing.Framework {
70
override final val name: String
71
val fingerprints: Array[sbt.testing.Fingerprint]
72
73
override def runner(args: Array[String], remoteArgs: Array[String], testClassLoader: ClassLoader): sbt.testing.Runner
74
override def slaveRunner(args: Array[String], remoteArgs: Array[String], testClassLoader: ClassLoader, send: String => Unit): sbt.testing.Runner
75
}
76
```
77
78
### Master Runner
79
80
Manages single-process test execution.
81
82
```scala { .api }
83
final class ZMasterTestRunnerJS(
84
args: Array[String],
85
remoteArgs: Array[String],
86
testClassLoader: ClassLoader
87
) extends ZTestRunnerJS(args, remoteArgs, testClassLoader, "master") {
88
override val sendSummary: SendSummary
89
}
90
```
91
92
**Summary Handling**: Collects summaries in a mutable buffer for local aggregation.
93
94
```scala
95
override val sendSummary: SendSummary = SendSummary.fromSend { summary =>
96
summaries += summary
97
()
98
}
99
```
100
101
### Slave Runner
102
103
Handles distributed test execution with custom summary transmission.
104
105
```scala { .api }
106
final class ZSlaveTestRunnerJS(
107
args: Array[String],
108
remoteArgs: Array[String],
109
testClassLoader: ClassLoader,
110
val sendSummary: SendSummary
111
) extends ZTestRunnerJS(args, remoteArgs, testClassLoader, "slave")
112
```
113
114
**Summary Handling**: Uses provided `SendSummary` that typically serializes and sends summaries to master process.
115
116
### Base JavaScript Runner
117
118
```scala { .api }
119
sealed abstract class ZTestRunnerJS(
120
val args: Array[String],
121
val remoteArgs: Array[String],
122
testClassLoader: ClassLoader,
123
runnerType: String
124
) extends sbt.testing.Runner {
125
126
def sendSummary: SendSummary
127
val summaries: scala.collection.mutable.Buffer[zio.test.Summary]
128
129
def done(): String
130
def tasks(defs: Array[sbt.testing.TaskDef]): Array[sbt.testing.Task]
131
def receiveMessage(summary: String): Option[String]
132
def serializeTask(task: sbt.testing.Task, serializer: sbt.testing.TaskDef => String): String
133
def deserializeTask(task: String, deserializer: String => sbt.testing.TaskDef): sbt.testing.Task
134
}
135
```
136
137
#### receiveMessage
138
139
Handles incoming summary messages from distributed testing.
140
141
```scala
142
override def receiveMessage(summary: String): Option[String] = {
143
SummaryProtocol.deserialize(summary).foreach(s => summaries += s)
144
None
145
}
146
```
147
148
#### Task Serialization
149
150
```scala
151
override def serializeTask(task: sbt.testing.Task, serializer: sbt.testing.TaskDef => String): String =
152
serializer(task.taskDef())
153
154
override def deserializeTask(task: String, deserializer: String => sbt.testing.TaskDef): sbt.testing.Task =
155
ZTestTask(deserializer(task), testClassLoader, runnerType, sendSummary, TestArgs.parse(args))
156
```
157
158
### JavaScript Test Task
159
160
Asynchronous test execution with callback-based completion.
161
162
```scala { .api }
163
sealed class ZTestTask(
164
taskDef: sbt.testing.TaskDef,
165
testClassLoader: ClassLoader,
166
runnerType: String,
167
sendSummary: SendSummary,
168
testArgs: zio.test.TestArgs,
169
spec: zio.test.ZIOSpecAbstract
170
) extends BaseTestTask(taskDef, testClassLoader, sendSummary, testArgs, spec, zio.Runtime.default, zio.Console.ConsoleLive)
171
```
172
173
#### Asynchronous Execution
174
175
```scala
176
def execute(eventHandler: sbt.testing.EventHandler, loggers: Array[sbt.testing.Logger], continuation: Array[sbt.testing.Task] => Unit): Unit = {
177
val fiber = Runtime.default.unsafe.fork { /* test execution logic */ }
178
fiber.unsafe.addObserver { exit =>
179
exit match {
180
case Exit.Failure(cause) => Console.err.println(s"$runnerType failed. $cause")
181
case _ =>
182
}
183
continuation(Array())
184
}
185
}
186
```
187
188
## Native Platform
189
190
### Framework Implementation
191
192
Similar to JavaScript but optimized for native compilation.
193
194
```scala { .api }
195
final class ZTestFramework extends sbt.testing.Framework {
196
override def name(): String
197
override def fingerprints(): Array[sbt.testing.Fingerprint]
198
override def runner(args: Array[String], remoteArgs: Array[String], testClassLoader: ClassLoader): sbt.testing.Runner
199
override def slaveRunner(args: Array[String], remoteArgs: Array[String], testClassLoader: ClassLoader, send: String => Unit): sbt.testing.Runner
200
}
201
```
202
203
### Native Runners
204
205
```scala { .api }
206
final class ZMasterTestRunner(
207
args: Array[String],
208
remoteArgs: Array[String],
209
testClassLoader: ClassLoader
210
) extends ZTestRunnerNative(args, remoteArgs, testClassLoader, "master")
211
212
final class ZSlaveTestRunner(
213
args: Array[String],
214
remoteArgs: Array[String],
215
testClassLoader: ClassLoader,
216
sendSummary: SendSummary
217
) extends ZTestRunnerNative(args, remoteArgs, testClassLoader, "slave")
218
```
219
220
### Base Native Runner
221
222
Uses `ConcurrentLinkedQueue` for thread-safe summary collection.
223
224
```scala { .api }
225
sealed abstract class ZTestRunnerNative(
226
val args: Array[String],
227
remoteArgs0: Array[String],
228
testClassLoader: ClassLoader,
229
runnerType: String
230
) extends sbt.testing.Runner {
231
232
def remoteArgs(): Array[String]
233
val summaries: java.util.concurrent.ConcurrentLinkedQueue[zio.test.Summary]
234
235
def done(): String
236
}
237
```
238
239
#### Summary Collection
240
241
```scala
242
def done(): String = {
243
val log = new StringBuilder
244
var summary = summaries.poll()
245
var total = 0
246
var ignore = 0
247
val isEmpty = summary eq null
248
249
while (summary ne null) {
250
total += summary.total
251
ignore += summary.ignore
252
val details = summary.failureDetails
253
if (!details.isBlank) {
254
log append colored(details)
255
log append '\n'
256
}
257
summary = summaries.poll()
258
}
259
260
if (isEmpty || total == ignore)
261
s"${Console.YELLOW}No tests were executed${Console.RESET}"
262
else
263
log.append("Done").result()
264
}
265
```
266
267
### Native Test Task
268
269
Blocking execution model suitable for native compilation.
270
271
```scala { .api }
272
sealed class ZTestTask(
273
taskDef: sbt.testing.TaskDef,
274
testClassLoader: ClassLoader,
275
runnerType: String,
276
sendSummary: SendSummary,
277
testArgs: zio.test.TestArgs,
278
spec: zio.test.ZIOSpecAbstract
279
) extends BaseTestTask(taskDef, testClassLoader, sendSummary, testArgs, spec, zio.Runtime.default, zio.Console.ConsoleLive)
280
```
281
282
#### Blocking Execution
283
284
```scala
285
override def execute(eventHandler: sbt.testing.EventHandler, loggers: Array[sbt.testing.Logger]): Array[sbt.testing.Task] = {
286
var resOutter: CancelableFuture[Unit] = null
287
try {
288
resOutter = Runtime.default.unsafe.runToFuture { /* test execution logic */ }
289
Await.result(resOutter, Duration.Inf)
290
Array()
291
} catch {
292
case t: Throwable =>
293
if (resOutter != null) resOutter.cancel()
294
throw t
295
}
296
}
297
```
298
299
## Platform-Specific Optimizations
300
301
### JVM Platform
302
- **Signal Handling**: SIGINFO/SIGUSR1 for fiber dumping
303
- **Advanced Runtime**: Full ZIO runtime with all features
304
- **Parallel Execution**: Thread-based parallelism
305
- **Memory Management**: JVM garbage collection
306
307
### JavaScript Platform
308
- **Asynchronous Execution**: Callback-based completion
309
- **Single-Threaded**: Event loop concurrency
310
- **Master/Slave Pattern**: Distributed testing support
311
- **Lightweight Runtime**: Reduced ZIO runtime for browser/Node.js
312
313
### Native Platform
314
- **Optimized Runtime**: Minimal ZIO runtime for native compilation
315
- **Blocking Execution**: Synchronous execution model
316
- **Memory Efficiency**: Manual memory management optimizations
317
- **Fast Startup**: Ahead-of-time compilation benefits
318
319
## Usage Examples
320
321
### Platform Detection
322
323
```scala
324
// Detect current platform
325
val platform = scala.util.Properties.propOrEmpty("java.specification.name") match {
326
case name if name.contains("Java") => "JVM"
327
case _ =>
328
if (scala.util.Properties.propOrEmpty("scala.scalajs.runtime.name").nonEmpty) "JavaScript"
329
else "Native"
330
}
331
332
println(s"Running on: $platform")
333
```
334
335
### Cross-Platform Runner Configuration
336
337
```scala
338
import zio.test.sbt._
339
340
// Create appropriate runner for platform
341
val runner: sbt.testing.Runner = platform match {
342
case "JVM" => new ZTestRunnerJVM(args, remoteArgs, classLoader)
343
case "JavaScript" => new ZMasterTestRunnerJS(args, remoteArgs, classLoader)
344
case "Native" => new ZMasterTestRunner(args, remoteArgs, classLoader)
345
}
346
```
347
348
### Summary Protocol Usage
349
350
```scala
351
// Master process: serialize and send summary
352
val summary = zio.test.Summary(success = 10, fail = 2, ignore = 1, failureDetails = "Some tests failed")
353
val serialized = SummaryProtocol.serialize(summary)
354
sendToSlave(serialized)
355
356
// Slave process: receive and deserialize summary
357
def receiveSummary(data: String): Unit = {
358
SummaryProtocol.deserialize(data) match {
359
case Some(summary) =>
360
println(s"Received: ${summary.total} tests, ${summary.fail} failures")
361
case None =>
362
println("Invalid summary format")
363
}
364
}
365
```