@@ -41,6 +41,7 @@ public enum TestMode: Argument, Equatable, CustomStringConvertible {
41
41
case version
42
42
case listTests
43
43
case run( String ? )
44
+ case runInParallel
44
45
45
46
public init ? ( argument: String , pop: @escaping ( ) -> String ? ) throws {
46
47
switch argument {
@@ -53,6 +54,8 @@ public enum TestMode: Argument, Equatable, CustomStringConvertible {
53
54
self = . run( specifier)
54
55
case " --version " :
55
56
self = . version
57
+ case " --parallel " :
58
+ self = . runInParallel
56
59
default :
57
60
return nil
58
61
}
@@ -67,6 +70,8 @@ public enum TestMode: Argument, Equatable, CustomStringConvertible {
67
70
case . run( let specifier) :
68
71
return specifier ?? " "
69
72
case . version: return " --version "
73
+ case . runInParallel:
74
+ return " --parallel "
70
75
}
71
76
}
72
77
}
@@ -141,7 +146,7 @@ public class SwiftTestTool: SwiftTool<TestMode, TestToolOptions> {
141
146
142
147
case . listTests:
143
148
let testPath = try buildTestsIfNeeded ( options)
144
- let testSuites = try getTestSuites ( path: testPath)
149
+ let testSuites = try SwiftTestTool . getTestSuites ( path: testPath)
145
150
// Print the tests.
146
151
for testSuite in testSuites {
147
152
for testCase in testSuite. tests {
@@ -153,8 +158,14 @@ public class SwiftTestTool: SwiftTool<TestMode, TestToolOptions> {
153
158
154
159
case . run( let specifier) :
155
160
let testPath = try buildTestsIfNeeded ( options)
156
- let success = test ( path: testPath, xctestArg: specifier)
161
+ let success : Bool = TestRunner ( path: testPath, xctestArg: specifier) . test ( )
157
162
exit ( success ? 0 : 1 )
163
+
164
+ case . runInParallel:
165
+ let testPath = try buildTestsIfNeeded ( options)
166
+ let runner = ParallelTestRunner ( testPath: testPath)
167
+ try runner. run ( )
168
+ exit ( runner. success ? 0 : 1 )
158
169
}
159
170
}
160
171
@@ -244,39 +255,11 @@ public class SwiftTestTool: SwiftTool<TestMode, TestToolOptions> {
244
255
return ( mode ?? . run( nil ) , options)
245
256
}
246
257
247
- /// Executes the XCTest binary with given arguments.
248
- ///
249
- /// - Parameters:
250
- /// - path: Path to a valid XCTest binary.
251
- /// - xctestArg: Arguments to pass to the XCTest binary.
252
- ///
253
- /// - Returns: True if execution exited with return code 0.
254
- private func test( path: AbsolutePath , xctestArg: String ? = nil ) -> Bool {
255
- var args : [ String ] = [ ]
256
- #if os(macOS)
257
- args = [ " xcrun " , " xctest " ]
258
- if let xctestArg = xctestArg {
259
- args += [ " -XCTest " , xctestArg]
260
- }
261
- args += [ path. asString]
262
- #else
263
- args += [ path. asString]
264
- if let xctestArg = xctestArg {
265
- args += [ xctestArg]
266
- }
267
- #endif
268
-
269
- // Execute the XCTest with inherited environment as it is convenient to pass senstive
270
- // information like username, password etc to test cases via environment variables.
271
- let result : Void ? = try ? system ( args, environment: ProcessInfo . processInfo. environment)
272
- return result != nil
273
- }
274
-
275
258
/// Locates XCTestHelper tool inside the libexec directory and bin directory.
276
259
/// Note: It is a fatalError if we are not able to locate the tool.
277
260
///
278
261
/// - Returns: Path to XCTestHelper tool.
279
- private func xctestHelperPath( ) -> AbsolutePath {
262
+ private static func xctestHelperPath( ) -> AbsolutePath {
280
263
let xctestHelperBin = " swiftpm-xctest-helper "
281
264
let binDirectory = AbsolutePath ( CommandLine . arguments. first!, relativeTo: currentWorkingDirectory) . parentDirectory
282
265
// XCTestHelper tool is installed in libexec.
@@ -303,11 +286,11 @@ public class SwiftTestTool: SwiftTool<TestMode, TestToolOptions> {
303
286
/// - Throws: TestError, SystemError, Utility.Errror
304
287
///
305
288
/// - Returns: Array of TestSuite
306
- private func getTestSuites( path: AbsolutePath ) throws -> [ TestSuite ] {
289
+ fileprivate static func getTestSuites( path: AbsolutePath ) throws -> [ TestSuite ] {
307
290
// Run the correct tool.
308
291
#if os(macOS)
309
292
let tempFile = try TemporaryFile ( )
310
- let args = [ xctestHelperPath ( ) . asString, path. asString, tempFile. path. asString]
293
+ let args = [ SwiftTestTool . xctestHelperPath ( ) . asString, path. asString, tempFile. path. asString]
311
294
try system ( args, environment: [ " DYLD_FRAMEWORK_PATH " : try platformFrameworksPath ( ) . asString] )
312
295
// Read the temporary file's content.
313
296
let data = try fopen ( tempFile. path) . readFileContents ( )
@@ -320,6 +303,212 @@ public class SwiftTestTool: SwiftTool<TestMode, TestToolOptions> {
320
303
}
321
304
}
322
305
306
+ /// A class to run tests on a XCTest binary.
307
+ ///
308
+ /// Note: Executes the XCTest with inherited environment as it is convenient to pass senstive
309
+ /// information like username, password etc to test cases via enviornment variables.
310
+ final class TestRunner {
311
+ /// Path to valid XCTest binary.
312
+ private let path : AbsolutePath
313
+
314
+ /// Arguments to pass to XCTest if any.
315
+ private let xctestArg : String ?
316
+
317
+ /// Creates an instance of TestRunner.
318
+ ///
319
+ /// - Parameters:
320
+ /// - path: Path to valid XCTest binary.
321
+ /// - xctestArg: Arguments to pass to XCTest.
322
+ init ( path: AbsolutePath , xctestArg: String ? = nil ) {
323
+ self . path = path
324
+ self . xctestArg = xctestArg
325
+ }
326
+
327
+ /// Constructs arguments to execute XCTest.
328
+ private func args( ) -> [ String ] {
329
+ var args : [ String ] = [ ]
330
+ #if os(macOS)
331
+ args = [ " xcrun " , " xctest " ]
332
+ if let xctestArg = xctestArg {
333
+ args += [ " -XCTest " , xctestArg]
334
+ }
335
+ args += [ path. asString]
336
+ #else
337
+ args += [ path. asString]
338
+ if let xctestArg = xctestArg {
339
+ args += [ xctestArg]
340
+ }
341
+ #endif
342
+ return args
343
+ }
344
+
345
+ /// Current inherited enviornment variables.
346
+ private var environment : [ String : String ] {
347
+ return ProcessInfo . processInfo. environment
348
+ }
349
+
350
+ /// Executes the tests without printing anything on standard streams.
351
+ ///
352
+ /// - Returns: A tuple with first bool member indicating if test execution returned code 0
353
+ /// and second argument containing the output of the execution.
354
+ func test( ) -> ( Bool , String ) {
355
+ var output = " "
356
+ var success = true
357
+ do {
358
+ try popen ( args ( ) , redirectStandardError: true , environment: environment) { line in
359
+ output += line
360
+ }
361
+ } catch {
362
+ success = false
363
+ }
364
+ return ( success, output)
365
+ }
366
+
367
+ /// Executes and returns execution status. Prints test output on standard streams.
368
+ func test( ) -> Bool {
369
+ let result : Void ? = try ? system ( args ( ) , environment: environment)
370
+ return result != nil
371
+ }
372
+ }
373
+
374
+ /// A class to run tests in parallel.
375
+ final class ParallelTestRunner {
376
+ /// A structure representing an individual unit test.
377
+ struct UnitTest {
378
+ /// The name of the unit test.
379
+ let name : String
380
+
381
+ /// The name of the test case.
382
+ let testCase : String
383
+
384
+ /// The specifier argument which can be passed to XCTest.
385
+ var specifier : String {
386
+ return testCase + " / " + name
387
+ }
388
+ }
389
+
390
+ /// An enum representing result of a unit test execution.
391
+ enum TestResult {
392
+ case success( UnitTest )
393
+ case failure( UnitTest , output: String )
394
+ }
395
+
396
+ /// Path to XCTest binary.
397
+ private let testPath : AbsolutePath
398
+
399
+ /// The queue containing list of tests to run (producer).
400
+ private let pendingTests = SynchronizedQueue < UnitTest ? > ( )
401
+
402
+ /// The queue containing tests which are finished running.
403
+ private let finishedTests = SynchronizedQueue < TestResult ? > ( )
404
+
405
+ /// Number of parallel workers to spawn.
406
+ private var numJobs : Int {
407
+ return ProcessInfo . processInfo. activeProcessorCount
408
+ }
409
+
410
+ /// Instance of progress bar. Animating progress bar if stream is a terminal otherwise
411
+ /// a simple progress bar.
412
+ private let progressBar : ProgressBarProtocol
413
+
414
+ /// Number of tests that will be executed.
415
+ private var numTests = 0
416
+
417
+ /// Number of the current tests that has been executed.
418
+ private var numCurrentTest = 0
419
+
420
+ /// True if all tests executed successfully.
421
+ private( set) var success : Bool = true
422
+
423
+ init ( testPath: AbsolutePath ) {
424
+ self . testPath = testPath
425
+ progressBar = createProgressBar ( forStream: stdoutStream, header: " Tests " )
426
+ }
427
+
428
+ /// Updates the progress bar status.
429
+ private func updateProgress( for test: UnitTest ) {
430
+ numCurrentTest += 1
431
+ progressBar. update ( percent: 100 * numCurrentTest/ numTests, text: test. specifier)
432
+ }
433
+
434
+ func enqueueTests( ) throws {
435
+ // Find all the test suites present in the test binary.
436
+ let testSuites = try SwiftTestTool . getTestSuites ( path: testPath)
437
+ // FIXME: Add a count property in SynchronizedQueue.
438
+ var numTests = 0
439
+ // Enqueue all the tests.
440
+ for testSuite in testSuites {
441
+ for testCase in testSuite. tests {
442
+ for test in testCase. tests {
443
+ numTests += 1
444
+ pendingTests. enqueue ( UnitTest ( name: test, testCase: testCase. name) )
445
+ }
446
+ }
447
+ }
448
+ self . numTests = numTests
449
+ self . numCurrentTest = 0
450
+ // Enqueue the sentinels, we stop a thread when it encounters a sentinel in the queue.
451
+ for _ in 0 ..< numJobs {
452
+ pendingTests. enqueue ( nil )
453
+ }
454
+ }
455
+
456
+ /// Executes the tests spawning parallel workers. Blocks calling thread until all workers are finished.
457
+ func run( ) throws {
458
+ // Enqueue all the tests.
459
+ try enqueueTests ( )
460
+
461
+ // Create the worker threads.
462
+ let workers : [ Thread ] = ( 0 ..< numJobs) . map { _ in
463
+ let thread = Thread {
464
+ // Dequeue a specifier and run it till we encounter nil.
465
+ while let test = self . pendingTests. dequeue ( ) {
466
+ let testRunner = TestRunner ( path: self . testPath, xctestArg: test. specifier)
467
+ let ( success, output) = testRunner. test ( )
468
+ if success {
469
+ self . finishedTests. enqueue ( . success( test) )
470
+ } else {
471
+ self . success = false
472
+ self . finishedTests. enqueue ( . failure( test, output: output) )
473
+ }
474
+ }
475
+ }
476
+ thread. start ( )
477
+ return thread
478
+ }
479
+
480
+ // Holds the output of test cases which failed.
481
+ var failureOutput = [ String] ( )
482
+ // Report (consume) the tests which have finished running.
483
+ while let result = finishedTests. dequeue ( ) {
484
+ switch result {
485
+ case . success( let test) :
486
+ updateProgress ( for: test)
487
+ case . failure( let test, let output) :
488
+ updateProgress ( for: test)
489
+ failureOutput. append ( output)
490
+ }
491
+ // We can't enqueue a sentinel into finished tests queue because we won't know
492
+ // which test is last one so exit this when all the tests have finished running.
493
+ if numCurrentTest == numTests { break }
494
+ }
495
+
496
+ // Wait till all threads finish execution.
497
+ workers. forEach { $0. join ( ) }
498
+ progressBar. complete ( )
499
+ printFailures ( failureOutput)
500
+ }
501
+
502
+ /// Prints the output of the tests that failed.
503
+ private func printFailures( _ failureOutput: [ String ] ) {
504
+ stdoutStream <<< " \n "
505
+ for error in failureOutput {
506
+ stdoutStream <<< error
507
+ }
508
+ stdoutStream. flush ( )
509
+ }
510
+ }
511
+
323
512
/// A struct to hold the XCTestSuite data.
324
513
struct TestSuite {
325
514
0 commit comments