0
# Event Handling
1
2
Event system for reporting test results, progress, and metadata with support for different test selection patterns and structured error reporting.
3
4
## Capabilities
5
6
### Event Interface
7
8
Core interface for test events fired during test execution.
9
10
```scala { .api }
11
/**
12
* An event fired by the test framework during a run.
13
* Contains complete information about test execution results.
14
*/
15
trait Event {
16
/**
17
* The fully qualified name of a class that can rerun the suite or test.
18
* @return class name for rerunning this test/suite
19
*/
20
def fullyQualifiedName(): String
21
22
/**
23
* The fingerprint of the test class whose fully qualified name is returned.
24
* If fingerprint.isModule indicates module, fullyQualifiedName excludes trailing $.
25
* @return fingerprint used to identify the test class
26
*/
27
def fingerprint(): Fingerprint
28
29
/**
30
* Additional information identifying the suite or test about which an event was fired.
31
* @return selector providing specific test/suite identification
32
*/
33
def selector(): Selector
34
35
/**
36
* Indicates the test result: success, failure, error, skipped, ignored, canceled, pending.
37
* @return status representing the test outcome
38
*/
39
def status(): Status
40
41
/**
42
* An OptionalThrowable associated with this Event.
43
* Contains exception information for failures and errors.
44
* @return optional throwable with error details
45
*/
46
def throwable(): OptionalThrowable
47
48
/**
49
* Execution time in milliseconds, or -1 if no duration available.
50
* @return duration in milliseconds or -1
51
*/
52
def duration(): Long
53
}
54
```
55
56
**Usage Examples:**
57
58
```scala
59
// Create success event for test method
60
class MyEvent(
61
className: String,
62
fp: Fingerprint,
63
sel: Selector,
64
stat: Status,
65
opt: OptionalThrowable,
66
dur: Long
67
) extends Event {
68
def fullyQualifiedName() = className
69
def fingerprint() = fp
70
def selector() = sel
71
def status() = stat
72
def throwable() = opt
73
def duration() = dur
74
}
75
76
// Create events for different outcomes
77
val successEvent = new MyEvent(
78
className = "com.example.MathTest",
79
fp = testFingerprint,
80
sel = new TestSelector("shouldAddNumbers"),
81
stat = Status.Success,
82
opt = new OptionalThrowable(), // empty
83
dur = 150L
84
)
85
86
val failureEvent = new MyEvent(
87
className = "com.example.MathTest",
88
fp = testFingerprint,
89
sel = new TestSelector("shouldDivideByZero"),
90
stat = Status.Failure,
91
opt = new OptionalThrowable(new AssertionError("Division by zero should throw")),
92
dur = 75L
93
)
94
95
val suiteEvent = new MyEvent(
96
className = "com.example.MathTest",
97
fp = testFingerprint,
98
sel = new SuiteSelector(),
99
stat = Status.Success,
100
opt = new OptionalThrowable(),
101
dur = 1200L
102
)
103
```
104
105
### EventHandler Interface
106
107
Interface for handling events fired by test frameworks during execution.
108
109
```scala { .api }
110
/**
111
* Interface implemented by clients that handle events fired by test frameworks.
112
* An event handler is passed to test framework via Task.execute method.
113
*/
114
trait EventHandler {
115
/**
116
* Handle an event fired by the test framework.
117
* @param event the event to handle
118
*/
119
def handle(event: Event): Unit
120
}
121
```
122
123
**Usage Examples:**
124
125
```scala
126
// Simple logging event handler
127
class LoggingEventHandler(logger: Logger) extends EventHandler {
128
def handle(event: Event): Unit = {
129
val testName = event.selector() match {
130
case suite: SuiteSelector => s"Suite: ${event.fullyQualifiedName()}"
131
case test: TestSelector => s"Test: ${event.fullyQualifiedName()}.${test.testName()}"
132
case nested: NestedTestSelector =>
133
s"Nested: ${event.fullyQualifiedName()}.${nested.suiteId()}.${nested.testName()}"
134
case _ => s"Unknown: ${event.fullyQualifiedName()}"
135
}
136
137
event.status() match {
138
case Status.Success =>
139
logger.info(s"✓ $testName (${event.duration()}ms)")
140
case Status.Failure =>
141
logger.error(s"✗ $testName (${event.duration()}ms)")
142
if (event.throwable().isDefined()) {
143
logger.trace(event.throwable().get())
144
}
145
case Status.Error =>
146
logger.error(s"⚠ $testName - ERROR (${event.duration()}ms)")
147
if (event.throwable().isDefined()) {
148
logger.trace(event.throwable().get())
149
}
150
case Status.Skipped =>
151
logger.warn(s"⚬ $testName - SKIPPED")
152
case Status.Ignored =>
153
logger.warn(s"⚬ $testName - IGNORED")
154
case Status.Canceled =>
155
logger.warn(s"⚬ $testName - CANCELED")
156
case Status.Pending =>
157
logger.warn(s"⚬ $testName - PENDING")
158
}
159
}
160
}
161
162
// Aggregating event handler
163
class TestResultCollector extends EventHandler {
164
private val results = mutable.Map[String, TestResult]()
165
166
def handle(event: Event): Unit = {
167
val key = s"${event.fullyQualifiedName()}.${selectorKey(event.selector())}"
168
results(key) = TestResult(
169
className = event.fullyQualifiedName(),
170
selector = event.selector(),
171
status = event.status(),
172
duration = event.duration(),
173
throwable = if (event.throwable().isDefined()) Some(event.throwable().get()) else None
174
)
175
}
176
177
def getResults(): Map[String, TestResult] = results.toMap
178
179
def getSummary(): TestSummary = {
180
val allResults = results.values.toSeq
181
TestSummary(
182
total = allResults.size,
183
passed = allResults.count(_.status == Status.Success),
184
failed = allResults.count(_.status == Status.Failure),
185
errors = allResults.count(_.status == Status.Error),
186
skipped = allResults.count(r =>
187
r.status == Status.Skipped || r.status == Status.Ignored || r.status == Status.Canceled),
188
pending = allResults.count(_.status == Status.Pending),
189
duration = allResults.map(_.duration).sum
190
)
191
}
192
}
193
```
194
195
### Status Enumeration
196
197
Represents the outcome of test execution with comprehensive status types.
198
199
```scala { .api }
200
/**
201
* Represents the status of running a test.
202
* Test frameworks can decide which statuses to use and their meanings.
203
*/
204
class Status private (name: String, ordinal: Int) extends Enum[Status](name, ordinal)
205
206
object Status {
207
/** Indicates a test succeeded. */
208
final val Success: Status
209
210
/** Indicates an "error" occurred during test execution. */
211
final val Error: Status
212
213
/** Indicates a "failure" occurred during test execution. */
214
final val Failure: Status
215
216
/** Indicates a test was skipped for any reason. */
217
final val Skipped: Status
218
219
/** Indicates a test was ignored (temporarily disabled with intention to fix). */
220
final val Ignored: Status
221
222
/**
223
* Indicates a test was canceled (unable to complete due to unmet precondition,
224
* such as database being offline).
225
*/
226
final val Canceled: Status
227
228
/**
229
* Indicates a test was declared as pending (with test code and/or
230
* production code as yet unimplemented).
231
*/
232
final val Pending: Status
233
234
/** Returns array of all status values. */
235
def values(): Array[Status]
236
237
/**
238
* Returns status by name.
239
* @param name status name
240
* @return Status instance
241
* @throws IllegalArgumentException if name not found
242
*/
243
def valueOf(name: String): Status
244
}
245
```
246
247
**Usage Examples:**
248
249
```scala
250
// Check status types
251
def handleTestResult(event: Event): Unit = {
252
event.status() match {
253
case Status.Success =>
254
println("Test passed!")
255
case Status.Failure =>
256
println(s"Test failed: ${event.throwable().get().getMessage}")
257
case Status.Error =>
258
println(s"Test error: ${event.throwable().get().getMessage}")
259
case Status.Skipped | Status.Ignored | Status.Canceled =>
260
println("Test was not executed")
261
case Status.Pending =>
262
println("Test is pending implementation")
263
}
264
}
265
266
// Create events with different statuses
267
val statuses = Status.values()
268
println(s"Available statuses: ${statuses.map(_.name).mkString(", ")}")
269
270
// Status by name lookup
271
val failureStatus = Status.valueOf("Failure")
272
assert(failureStatus == Status.Failure)
273
```
274
275
### OptionalThrowable
276
277
Container for optional exception information associated with test events.
278
279
```scala { .api }
280
/**
281
* An optional Throwable container.
282
* Used to carry exception information with test events.
283
*/
284
final class OptionalThrowable(exception: Throwable) extends Serializable {
285
/** Creates empty OptionalThrowable with no exception. */
286
def this()
287
288
/**
289
* Indicates whether this OptionalThrowable contains a Throwable.
290
* @return true if contains a Throwable
291
*/
292
def isDefined(): Boolean
293
294
/**
295
* Indicates whether this OptionalThrowable is empty.
296
* @return true if contains no Throwable
297
*/
298
def isEmpty(): Boolean
299
300
/**
301
* Returns the contained Throwable if defined.
302
* @return contained Throwable
303
* @throws IllegalStateException if not defined
304
*/
305
def get(): Throwable
306
}
307
```
308
309
**Usage Examples:**
310
311
```scala
312
// Create OptionalThrowable with exception
313
val withException = new OptionalThrowable(new RuntimeException("Test failed"))
314
if (withException.isDefined()) {
315
println(s"Exception: ${withException.get().getMessage}")
316
}
317
318
// Create empty OptionalThrowable
319
val empty = new OptionalThrowable()
320
assert(empty.isEmpty())
321
322
// Safe exception handling
323
def safeGetException(opt: OptionalThrowable): Option[Throwable] = {
324
if (opt.isDefined()) Some(opt.get()) else None
325
}
326
327
// Create events with optional exceptions
328
val successEvent = createEvent(Status.Success, new OptionalThrowable())
329
val failureEvent = createEvent(
330
Status.Failure,
331
new OptionalThrowable(new AssertionError("Expected 5 but was 3"))
332
)
333
```
334
335
## Event Patterns
336
337
### Event Creation in Tasks
338
339
Tasks should create and fire events for all test outcomes:
340
341
```scala
342
class MyTask(taskDef: TaskDef) extends Task {
343
def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task] = {
344
val startTime = System.currentTimeMillis()
345
346
try {
347
// Execute test logic
348
val result = runTest(taskDef.fullyQualifiedName(), taskDef.selectors())
349
val duration = System.currentTimeMillis() - startTime
350
351
result match {
352
case TestSuccess =>
353
val event = new MyEvent(
354
taskDef.fullyQualifiedName(),
355
taskDef.fingerprint(),
356
new SuiteSelector(),
357
Status.Success,
358
new OptionalThrowable(),
359
duration
360
)
361
eventHandler.handle(event)
362
363
case TestFailure(assertion) =>
364
val event = new MyEvent(
365
taskDef.fullyQualifiedName(),
366
taskDef.fingerprint(),
367
new SuiteSelector(),
368
Status.Failure,
369
new OptionalThrowable(assertion),
370
duration
371
)
372
eventHandler.handle(event)
373
}
374
375
Array.empty // No nested tasks
376
377
} catch {
378
case t: Throwable =>
379
val duration = System.currentTimeMillis() - startTime
380
val errorEvent = new MyEvent(
381
taskDef.fullyQualifiedName(),
382
taskDef.fingerprint(),
383
new SuiteSelector(),
384
Status.Error,
385
new OptionalThrowable(t),
386
duration
387
)
388
eventHandler.handle(errorEvent)
389
Array.empty
390
}
391
}
392
}
393
```
394
395
### Selector-Specific Events
396
397
Different selector types should fire appropriate events:
398
399
```scala
400
def fireEventsForSelectors(
401
selectors: Array[Selector],
402
className: String,
403
fingerprint: Fingerprint,
404
eventHandler: EventHandler
405
): Unit = {
406
407
selectors.foreach {
408
case _: SuiteSelector =>
409
// Fire suite-level event
410
val event = createSuiteEvent(className, fingerprint, Status.Success)
411
eventHandler.handle(event)
412
413
case testSel: TestSelector =>
414
// Fire individual test event
415
val event = createTestEvent(className, fingerprint, testSel, Status.Success)
416
eventHandler.handle(event)
417
418
case nestedSel: NestedTestSelector =>
419
// Fire nested test event
420
val event = createNestedTestEvent(className, fingerprint, nestedSel, Status.Success)
421
eventHandler.handle(event)
422
423
case wildcardSel: TestWildcardSelector =>
424
// Fire events for matching tests
425
val matchingTests = findTestsMatching(wildcardSel.testWildcard())
426
matchingTests.foreach { testName =>
427
val testSelector = new TestSelector(testName)
428
val event = createTestEvent(className, fingerprint, testSelector, Status.Success)
429
eventHandler.handle(event)
430
}
431
}
432
}
433
```
434
435
## Error Handling
436
437
### Exception Information
438
439
Events should include detailed exception information for failures and errors:
440
441
```scala
442
def createFailureEvent(
443
className: String,
444
fingerprint: Fingerprint,
445
selector: Selector,
446
exception: Throwable,
447
duration: Long
448
): Event = {
449
450
val status = exception match {
451
case _: AssertionError => Status.Failure
452
case _: Exception => Status.Error
453
case _ => Status.Error
454
}
455
456
new MyEvent(
457
className,
458
fingerprint,
459
selector,
460
status,
461
new OptionalThrowable(exception),
462
duration
463
)
464
}
465
```
466
467
### Event Handler Error Recovery
468
469
Event handlers should handle processing errors gracefully:
470
471
```scala
472
class RobustEventHandler extends EventHandler {
473
def handle(event: Event): Unit = {
474
try {
475
processEvent(event)
476
} catch {
477
case t: Throwable =>
478
// Log error but don't propagate to avoid breaking test execution
479
System.err.println(s"Event handler error: ${t.getMessage}")
480
t.printStackTrace()
481
}
482
}
483
484
private def processEvent(event: Event): Unit = {
485
// Event processing logic that might throw
486
}
487
}
488
```