0
# Test Execution
1
2
Test execution system providing task creation, lifecycle management, and support for both synchronous and asynchronous JavaScript execution patterns.
3
4
## Capabilities
5
6
### Runner Interface
7
8
Manages the lifecycle of a test run, creating tasks from task definitions and coordinating distributed execution.
9
10
```scala { .api }
11
/**
12
* Represents one run of a suite of tests.
13
* Has a lifecycle: instantiation -> tasks() calls -> done()
14
* After done() is called, the Runner enters "spent" mode and tasks() throws IllegalStateException.
15
*/
16
trait Runner {
17
/**
18
* Returns an array of tasks that when executed will run tests and suites.
19
* @param taskDefs the TaskDefs for requested tasks
20
* @return an array of Tasks
21
* @throws IllegalStateException if invoked after done() has been called
22
*/
23
def tasks(taskDefs: Array[TaskDef]): Array[Task]
24
25
/**
26
* Indicates the client is done with this Runner instance.
27
* Must not call task() methods after this.
28
* In Scala.js, client must not call before all Task.execute completions.
29
* @return a possibly multi-line summary string, or empty string if no summary
30
* @throws IllegalStateException if called more than once
31
*/
32
def done(): String
33
34
/**
35
* Remote args that will be passed to Runner in a sub-process as remoteArgs.
36
* @return array of strings for sub-process remoteArgs
37
*/
38
def remoteArgs(): Array[String]
39
40
/** Returns the arguments that were used to create this Runner. */
41
def args: Array[String]
42
43
/**
44
* Scala.js specific: Message handling for controller/worker communication.
45
* For controller: invoked when worker sends message, can return response in Some
46
* For worker: invoked when controller responds, return value ignored
47
* @param msg message received
48
* @return optional response message (controller only)
49
*/
50
def receiveMessage(msg: String): Option[String]
51
52
/**
53
* Scala.js specific: Serialize a task for distribution to another runner.
54
* After calling this method, the passed task becomes invalid.
55
* @param task task to serialize
56
* @param serializer function to serialize TaskDef
57
* @return serialized task string
58
*/
59
def serializeTask(task: Task, serializer: TaskDef => String): String
60
61
/**
62
* Scala.js specific: Deserialize a task from another runner.
63
* The resulting task associates with this runner.
64
* @param task serialized task string
65
* @param deserializer function to deserialize TaskDef
66
* @return deserialized Task
67
*/
68
def deserializeTask(task: String, deserializer: String => TaskDef): Task
69
}
70
```
71
72
**Usage Examples:**
73
74
```scala
75
import sbt.testing._
76
77
class MyTestRunner(
78
val args: Array[String],
79
val remoteArgs: Array[String],
80
testClassLoader: ClassLoader
81
) extends Runner {
82
83
private var isSpent = false
84
private val completedTasks = mutable.Set[Task]()
85
86
def tasks(taskDefs: Array[TaskDef]): Array[Task] = {
87
if (isSpent) {
88
throw new IllegalStateException("Runner is spent - done() already called")
89
}
90
91
taskDefs.flatMap { taskDef =>
92
// Create task if we can handle this test class
93
if (canHandle(taskDef)) {
94
Some(new MyTask(taskDef, this))
95
} else {
96
None // Reject this task
97
}
98
}
99
}
100
101
def done(): String = {
102
if (isSpent) {
103
throw new IllegalStateException("done() already called")
104
}
105
isSpent = true
106
107
val summary = s"Completed ${completedTasks.size} tasks"
108
cleanup()
109
summary
110
}
111
112
// Message handling for distributed testing
113
def receiveMessage(msg: String): Option[String] = {
114
msg match {
115
case "status" => Some(s"completed:${completedTasks.size}")
116
case "shutdown" =>
117
cleanup()
118
Some("acknowledged")
119
case _ => None
120
}
121
}
122
123
// Task serialization for worker distribution
124
def serializeTask(task: Task, serializer: TaskDef => String): String = {
125
val taskDefStr = serializer(task.taskDef())
126
s"MyTask:$taskDefStr"
127
}
128
129
def deserializeTask(task: String, deserializer: String => TaskDef): Task = {
130
task match {
131
case s"MyTask:$taskDefStr" =>
132
val taskDef = deserializer(taskDefStr)
133
new MyTask(taskDef, this)
134
case _ =>
135
throw new IllegalArgumentException(s"Cannot deserialize task: $task")
136
}
137
}
138
}
139
```
140
141
### Task Interface
142
143
Represents an executable unit of work that runs tests and can produce additional tasks.
144
145
```scala { .api }
146
/**
147
* A task to execute.
148
* Can be any job, but primarily intended for running tests and/or supplying more tasks.
149
*/
150
trait Task {
151
/**
152
* A possibly zero-length array of string tags associated with this task.
153
* Used for task scheduling and resource management.
154
* @return array of this task's tags
155
*/
156
def tags(): Array[String]
157
158
/**
159
* Executes this task, possibly returning new tasks to execute.
160
* @param eventHandler event handler to fire events during the run
161
* @param loggers array of loggers to emit log messages during the run
162
* @return possibly empty array of new tasks for the client to execute
163
*/
164
def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task]
165
166
/**
167
* Scala.js specific: Async execute with continuation support.
168
* This method will be called instead of synchronous execute in JavaScript environments.
169
* @param eventHandler event handler to fire events during the run
170
* @param loggers array of loggers to emit log messages during the run
171
* @param continuation called with result tasks when execution completes
172
*/
173
def execute(eventHandler: EventHandler, loggers: Array[Logger],
174
continuation: Array[Task] => Unit): Unit
175
176
/**
177
* Returns the TaskDef that was used to request this Task.
178
* @return the TaskDef used to request this Task
179
*/
180
def taskDef(): TaskDef
181
}
182
```
183
184
**Usage Examples:**
185
186
```scala
187
class MyTask(taskDef: TaskDef, runner: Runner) extends Task {
188
189
def tags(): Array[String] = {
190
// Tag CPU-intensive tests
191
if (isCpuIntensive(taskDef.fullyQualifiedName())) {
192
Array("cpu-intensive")
193
} else {
194
Array.empty
195
}
196
}
197
198
// Synchronous execution (JVM)
199
def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task] = {
200
val logger = loggers.headOption.getOrElse(NoOpLogger)
201
202
try {
203
logger.info(s"Running test: ${taskDef.fullyQualifiedName()}")
204
205
// Load and execute test class
206
val testInstance = loadTestClass(taskDef.fullyQualifiedName())
207
val results = runTests(testInstance, taskDef.selectors())
208
209
// Fire events for each test result
210
results.foreach { result =>
211
val event = createEvent(result, taskDef)
212
eventHandler.handle(event)
213
}
214
215
// Return any nested tasks
216
createNestedTasks(results)
217
218
} catch {
219
case t: Throwable =>
220
logger.error(s"Test execution failed: ${t.getMessage}")
221
val errorEvent = createErrorEvent(t, taskDef)
222
eventHandler.handle(errorEvent)
223
Array.empty
224
}
225
}
226
227
// Asynchronous execution (JavaScript)
228
def execute(eventHandler: EventHandler, loggers: Array[Logger],
229
continuation: Array[Task] => Unit): Unit = {
230
val logger = loggers.headOption.getOrElse(NoOpLogger)
231
232
// Use JavaScript's async capabilities
233
scala.scalajs.js.timers.setTimeout(0) {
234
try {
235
logger.info(s"Async running test: ${taskDef.fullyQualifiedName()}")
236
237
val testInstance = loadTestClass(taskDef.fullyQualifiedName())
238
239
// Run async tests with Promise-based execution
240
runAsyncTests(testInstance, taskDef.selectors()) { results =>
241
results.foreach { result =>
242
val event = createEvent(result, taskDef)
243
eventHandler.handle(event)
244
}
245
246
val nestedTasks = createNestedTasks(results)
247
continuation(nestedTasks)
248
}
249
250
} catch {
251
case t: Throwable =>
252
logger.error(s"Async test execution failed: ${t.getMessage}")
253
val errorEvent = createErrorEvent(t, taskDef)
254
eventHandler.handle(errorEvent)
255
continuation(Array.empty)
256
}
257
}
258
}
259
260
def taskDef(): TaskDef = taskDef
261
}
262
```
263
264
### TaskDef Class
265
266
Bundles information used to request a Task from a test framework.
267
268
```scala { .api }
269
/**
270
* Information bundle used to request a Task from a test framework.
271
* @param fullyQualifiedName fully qualified name of the test class
272
* @param fingerprint indicates how the test suite was identified
273
* @param explicitlySpecified whether test class was explicitly specified by user
274
* @param selectors array of Selectors determining suites and tests to run
275
*/
276
final class TaskDef(
277
fullyQualifiedName: String,
278
fingerprint: Fingerprint,
279
explicitlySpecified: Boolean,
280
selectors: Array[Selector]
281
) extends Serializable {
282
283
/** The fully qualified name of the test class requested by this TaskDef. */
284
def fullyQualifiedName(): String
285
286
/** The fingerprint that the test class requested by this TaskDef matches. */
287
def fingerprint(): Fingerprint
288
289
/**
290
* Indicates whether the test class was "explicitly specified" by the user.
291
* True for commands like: test-only com.mycompany.myproject.WholeNameSpec
292
* False for commands like: test-only *WholeNameSpec or test
293
*/
294
def explicitlySpecified(): Boolean
295
296
/**
297
* Selectors describing the nature of the Task requested by this TaskDef.
298
* Can indicate direct user requests or "rerun" of previously run tests.
299
*/
300
def selectors(): Array[Selector]
301
}
302
```
303
304
**Usage:**
305
306
```scala
307
// Create TaskDef for explicit test class
308
val taskDef = new TaskDef(
309
fullyQualifiedName = "com.example.MyTestSuite",
310
fingerprint = mySubclassFingerprint,
311
explicitlySpecified = true,
312
selectors = Array(new SuiteSelector())
313
)
314
315
// Create TaskDef for specific test method
316
val testTaskDef = new TaskDef(
317
fullyQualifiedName = "com.example.MyTestSuite",
318
fingerprint = mySubclassFingerprint,
319
explicitlySpecified = false,
320
selectors = Array(new TestSelector("shouldCalculateCorrectly"))
321
)
322
```
323
324
## Execution Lifecycle
325
326
1. **Task Creation**: Runner.tasks() creates Task instances from TaskDef array
327
2. **Task Scheduling**: Client schedules tasks based on tags and resources
328
3. **Synchronous/Async Execution**: Task.execute() called with appropriate method
329
4. **Event Reporting**: Tasks fire events through EventHandler during execution
330
5. **Nested Task Generation**: Tasks can return additional tasks for execution
331
6. **Completion**: All tasks complete and Runner.done() is called
332
7. **Cleanup**: Runner resources cleaned up and becomes "spent"
333
334
## Error Handling
335
336
### IllegalStateException
337
338
- **Runner.tasks()**: Thrown if called after Runner.done()
339
- **Runner.done()**: Thrown if called multiple times
340
- **JavaScript execution**: Thrown if done() called before all task continuations complete
341
342
```scala
343
// Proper error handling in runner
344
def tasks(taskDefs: Array[TaskDef]): Array[Task] = {
345
synchronized {
346
if (isSpent) {
347
throw new IllegalStateException("Runner is spent")
348
}
349
// ... create tasks
350
}
351
}
352
```