This document covers the event handling system that converts ZIO test execution events to SBT-compatible formats.
Represents a test event in SBT's format, converting ZIO test results to SBT's event model.
final case class ZTestEvent(
fullyQualifiedName0: String,
selector0: sbt.testing.Selector,
status0: sbt.testing.Status,
maybeThrowable: Option[Throwable],
duration0: Long,
fingerprint0: sbt.testing.Fingerprint
) extends sbt.testing.Event {
def duration(): Long
def fingerprint(): sbt.testing.Fingerprint
def fullyQualifiedName(): String
def selector(): sbt.testing.Selector
def status(): sbt.testing.Status
def throwable(): sbt.testing.OptionalThrowable
}fullyQualifiedName0: Full class name of the testselector0: Test selector (typically TestSelector with test name)status0: Test status (Success, Failure, Ignored)maybeThrowable: Optional exception for failed testsduration0: Test execution duration in millisecondsfingerprint0: Test fingerprint (always ZioSpecFingerprint)All methods implement the SBT Event interface:
Returns test execution duration.
event.duration()
// Returns: Long (milliseconds)Returns the test fingerprint.
event.fingerprint()
// Returns: sbt.testing.Fingerprint (ZioSpecFingerprint)Returns the fully qualified test class name.
event.fullyQualifiedName()
// Returns: StringReturns the test selector identifying the specific test.
event.selector()
// Returns: sbt.testing.SelectorReturns the test execution status.
event.status()
// Returns: sbt.testing.StatusReturns optional throwable for failed tests.
event.throwable()
// Returns: sbt.testing.OptionalThrowableProvides factory methods and conversion utilities.
object ZTestEvent {
def convertEvent(
test: zio.test.ExecutionEvent.Test[_],
taskDef: sbt.testing.TaskDef,
renderer: zio.test.render.TestRenderer
): sbt.testing.Event
}Converts a ZIO test execution event to an SBT event.
Parameters:
test: ZIO test execution event containing resultstaskDef: SBT task definition for contextrenderer: Test renderer for formatting failure messagesReturns: SBT-compatible Event
Conversion Logic:
TestSuccess.Succeeded → Status.SuccessTestSuccess.Ignored → Status.IgnoredStatus.FailureTestSelector from test label hierarchyimport zio.test.sbt._
import zio.test.render.ConsoleRenderer
val zioTestResult: zio.test.ExecutionEvent.Test[_] = ???
val taskDef: sbt.testing.TaskDef = ???
val sbtEvent = ZTestEvent.convertEvent(
test = zioTestResult,
taskDef = taskDef,
renderer = ConsoleRenderer
)
println(s"Test: ${sbtEvent.fullyQualifiedName()}")
println(s"Status: ${sbtEvent.status()}")
println(s"Duration: ${sbtEvent.duration()}ms")Handles ZIO test execution events and forwards them to SBT's event system.
class ZTestEventHandlerSbt(
eventHandler: sbt.testing.EventHandler,
taskDef: sbt.testing.TaskDef,
renderer: zio.test.render.TestRenderer
) extends zio.test.ZTestEventHandler {
val semaphore: zio.Semaphore
def handle(event: zio.test.ExecutionEvent): zio.UIO[Unit]
}eventHandler: SBT's event handler for forwarding eventstaskDef: Task definition for contextrenderer: Test renderer for formatting outputsemaphore: Ensures thread-safe event handling (initialized with permit 1)Processes ZIO execution events and converts them to SBT events.
def handle(event: zio.test.ExecutionEvent): zio.UIO[Unit]Supported Event Types:
Test initiation events - no action taken.
case ExecutionEvent.TestStarted(_, _, _, _, _) => zio.ZIO.unitCompleted test events - converted to SBT events.
case evt @ ExecutionEvent.Test(_, _, _, _, _, _, _) =>
val zTestEvent = ZTestEvent.convertEvent(evt, taskDef, renderer)
semaphore.withPermit(zio.ZIO.succeed(eventHandler.handle(zTestEvent)))Test section boundaries - no action taken.
case ExecutionEvent.SectionStart(_, _, _) => zio.ZIO.unit
case ExecutionEvent.SectionEnd(_, _, _) => zio.ZIO.unitOutput flushing events - no action taken.
case ExecutionEvent.TopLevelFlush(_) => zio.ZIO.unitRuntime failure events - converted to SBT failure events.
case ExecutionEvent.RuntimeFailure(_, _, failure, _) =>
failure match {
case TestFailure.Assertion(_, _) => zio.ZIO.unit // Handled via Test events
case TestFailure.Runtime(cause, annotations) =>
val zTestEvent = ZTestEvent(
taskDef.fullyQualifiedName(),
taskDef.selectors().head,
sbt.testing.Status.Failure,
cause.dieOption,
annotations.get(zio.test.TestAnnotation.timing).toMillis,
ZioSpecFingerprint
)
semaphore.withPermit(zio.ZIO.succeed(eventHandler.handle(zTestEvent)))
}The event handling system follows this flow:
ExecutionEventsZTestEventHandlerSbt receives ZIO eventsZTestEvent.convertEvent transforms ZIO events to SBT formatEventHandlerimport zio.test.sbt._
import sbt.testing._
// Create custom event handler
class CustomEventHandler extends EventHandler {
def handle(event: Event): Unit = {
event.status() match {
case Status.Success => println(s"✓ ${event.fullyQualifiedName()}")
case Status.Failure => println(s"✗ ${event.fullyQualifiedName()}")
case Status.Ignored => println(s"○ ${event.fullyQualifiedName()}")
}
}
}
// Use with ZTestEventHandlerSbt
val customHandler = new CustomEventHandler()
val taskDef: TaskDef = ???
val renderer = zio.test.render.ConsoleRenderer
val zioHandler = new ZTestEventHandlerSbt(customHandler, taskDef, renderer)// Create filtering event handler
class FilteringEventHandler(underlying: EventHandler, filter: Event => Boolean) extends EventHandler {
def handle(event: Event): Unit = {
if (filter(event)) {
underlying.handle(event)
}
}
}
// Only handle failures
val failureFilter = (event: Event) => event.status() == Status.Failure
val filteringHandler = new FilteringEventHandler(originalHandler, failureFilter)// Create test event manually
val testEvent = ZTestEvent(
fullyQualifiedName0 = "com.example.MyTest",
selector0 = new TestSelector("should pass"),
status0 = Status.Success,
maybeThrowable = None,
duration0 = 150L,
fingerprint0 = ZioSpecFingerprint
)
// Handle the event
eventHandler.handle(testEvent)