@@ -29,6 +29,10 @@ class TestProcess : XCTestCase {
29
29
( " test_no_environment " , test_no_environment) ,
30
30
( " test_custom_environment " , test_custom_environment) ,
31
31
( " test_run " , test_run) ,
32
+ ( " test_preStartEndState " , test_preStartEndState) ,
33
+ ( " test_interrupt " , test_interrupt) ,
34
+ ( " test_terminate " , test_terminate) ,
35
+ ( " test_suspend_resume " , test_suspend_resume) ,
32
36
]
33
37
#endif
34
38
}
@@ -385,6 +389,135 @@ class TestProcess : XCTestCase {
385
389
fm. changeCurrentDirectoryPath ( cwd)
386
390
}
387
391
392
+ func test_preStartEndState( ) {
393
+ let process = Process ( )
394
+ XCTAssertNil ( process. executableURL)
395
+ XCTAssertNotNil ( process. currentDirectoryURL)
396
+ XCTAssertNil ( process. arguments)
397
+ XCTAssertNil ( process. environment)
398
+ XCTAssertFalse ( process. isRunning)
399
+ XCTAssertEqual ( process. processIdentifier, 0 )
400
+ XCTAssertEqual ( process. qualityOfService, . default)
401
+
402
+ process. executableURL = URL ( fileURLWithPath: " /bin/cat " , isDirectory: false )
403
+ _ = try ? process. run ( )
404
+ XCTAssertTrue ( process. isRunning)
405
+ XCTAssertTrue ( process. processIdentifier > 0 )
406
+ process. terminate ( )
407
+ process. waitUntilExit ( )
408
+ XCTAssertFalse ( process. isRunning)
409
+ XCTAssertTrue ( process. processIdentifier > 0 )
410
+ XCTAssertEqual ( process. terminationReason, . uncaughtSignal)
411
+ XCTAssertEqual ( process. terminationStatus, SIGTERM)
412
+ }
413
+
414
+ func test_interrupt( ) {
415
+ let helper = _SignalHelperRunner ( )
416
+ do {
417
+ try helper. start ( )
418
+ } catch {
419
+ XCTFail ( " Cant run xdgTestHelper: \( error) " )
420
+ return
421
+ }
422
+ if !helper. waitForReady ( ) {
423
+ XCTFail ( " Didnt receive Ready from sub-process " )
424
+ return
425
+ }
426
+
427
+ let now = DispatchTime . now ( ) . uptimeNanoseconds
428
+ let timeout = DispatchTime ( uptimeNanoseconds: now + 2_000_000_000 )
429
+
430
+ var count = 3
431
+ while count > 0 {
432
+ helper. process. interrupt ( )
433
+ guard helper. semaphore. wait ( timeout: timeout) == . success else {
434
+ helper. process. terminate ( )
435
+ XCTFail ( " Timedout waiting for signal " )
436
+ return
437
+ }
438
+
439
+ if helper. sigIntCount == 3 {
440
+ break
441
+ }
442
+ count -= 1
443
+ }
444
+ helper. process. terminate ( )
445
+ XCTAssertEqual ( helper. sigIntCount, 3 )
446
+ helper. process. waitUntilExit ( )
447
+ let terminationReason = helper. process. terminationReason
448
+ XCTAssertEqual ( terminationReason, Process . TerminationReason. exit)
449
+ let status = helper. process. terminationStatus
450
+ XCTAssertEqual ( status, 99 )
451
+ }
452
+
453
+ func test_terminate( ) {
454
+ let cat = URL ( fileURLWithPath: " /bin/cat " , isDirectory: false )
455
+ guard let process = try ? Process . run ( cat, arguments: [ ] ) else {
456
+ XCTFail ( " Cant run /bin/cat " )
457
+ return
458
+ }
459
+
460
+ process. terminate ( )
461
+ process. waitUntilExit ( )
462
+ let terminationReason = process. terminationReason
463
+ XCTAssertEqual ( terminationReason, Process . TerminationReason. uncaughtSignal)
464
+ XCTAssertEqual ( process. terminationStatus, SIGTERM)
465
+ }
466
+
467
+ func test_suspend_resume( ) {
468
+ let helper = _SignalHelperRunner ( )
469
+ do {
470
+ try helper. start ( )
471
+ } catch {
472
+ XCTFail ( " Cant run xdgTestHelper: \( error) " )
473
+ return
474
+ }
475
+ if !helper. waitForReady ( ) {
476
+ XCTFail ( " Didnt receive Ready from sub-process " )
477
+ return
478
+ }
479
+ let now = DispatchTime . now ( ) . uptimeNanoseconds
480
+ let timeout = DispatchTime ( uptimeNanoseconds: now + 2_000_000_000 )
481
+
482
+ func waitForSemaphore( ) -> Bool {
483
+ guard helper. semaphore. wait ( timeout: timeout) == . success else {
484
+ helper. process. terminate ( )
485
+ XCTFail ( " Timedout waiting for signal " )
486
+ return false
487
+ }
488
+ return true
489
+ }
490
+
491
+ XCTAssertTrue ( helper. process. isRunning)
492
+ XCTAssertTrue ( helper. process. suspend ( ) )
493
+ XCTAssertTrue ( helper. process. isRunning)
494
+ XCTAssertTrue ( helper. process. resume ( ) )
495
+ if waitForSemaphore ( ) == false { return }
496
+ XCTAssertEqual ( helper. sigContCount, 1 )
497
+
498
+ XCTAssertTrue ( helper. process. resume ( ) )
499
+ XCTAssertTrue ( helper. process. suspend ( ) )
500
+ XCTAssertTrue ( helper. process. resume ( ) )
501
+ XCTAssertEqual ( helper. sigContCount, 1 )
502
+
503
+ XCTAssertTrue ( helper. process. suspend ( ) )
504
+ XCTAssertTrue ( helper. process. suspend ( ) )
505
+ XCTAssertTrue ( helper. process. resume ( ) )
506
+ if waitForSemaphore ( ) == false { return }
507
+
508
+ helper. process. suspend ( )
509
+ helper. process. resume ( )
510
+ if waitForSemaphore ( ) == false { return }
511
+ XCTAssertEqual ( helper. sigContCount, 3 )
512
+
513
+ helper. process. terminate ( )
514
+ helper. process. waitUntilExit ( )
515
+ XCTAssertFalse ( helper. process. isRunning)
516
+ XCTAssertFalse ( helper. process. suspend ( ) )
517
+ XCTAssertTrue ( helper. process. resume ( ) )
518
+ XCTAssertTrue ( helper. process. resume ( ) )
519
+ }
520
+
388
521
#endif
389
522
}
390
523
@@ -394,6 +527,89 @@ private enum Error: Swift.Error {
394
527
case InvalidEnvironmentVariable( String )
395
528
}
396
529
530
+ // Run xdgTestHelper, wait for 'Ready' from the sub-process, then signal a semaphore.
531
+ // Read lines from a pipe and store in a queue.
532
+ class _SignalHelperRunner {
533
+ let process = Process ( )
534
+ let semaphore = DispatchSemaphore ( value: 0 )
535
+
536
+ private let outputPipe = Pipe ( )
537
+ private let sQueue = DispatchQueue ( label: " io queue " )
538
+ private let source : DispatchSourceRead
539
+
540
+ private var gotReady = false
541
+ private var bytesIn = Data ( )
542
+ private var _sigIntCount = 0
543
+ private var _sigContCount = 0
544
+ var sigIntCount : Int { return sQueue. sync { return _sigIntCount } }
545
+ var sigContCount : Int { return sQueue. sync { return _sigContCount } }
546
+
547
+
548
+ init ( ) {
549
+ process. executableURL = xdgTestHelperURL ( )
550
+ process. environment = ProcessInfo . processInfo. environment
551
+ process. arguments = [ " --signal-test " ]
552
+ process. standardOutput = outputPipe. fileHandleForWriting
553
+
554
+ source = DispatchSource . makeReadSource ( fileDescriptor: outputPipe. fileHandleForReading. fileDescriptor, queue: sQueue)
555
+ let workItem = DispatchWorkItem ( block: { [ weak self] in
556
+ if let strongSelf = self {
557
+ let newLine = UInt8 ( ascii: " \n " )
558
+
559
+ strongSelf. bytesIn. append ( strongSelf. outputPipe. fileHandleForReading. availableData)
560
+ if strongSelf. bytesIn. isEmpty {
561
+ return
562
+ }
563
+ // Split the incoming data into lines.
564
+ while let index = strongSelf. bytesIn. index ( of: newLine) {
565
+ if index >= strongSelf. bytesIn. startIndex {
566
+ // dont include the newline when converting to string
567
+ let line = String ( data: strongSelf. bytesIn [ strongSelf. bytesIn. startIndex..< index] , encoding: String . Encoding. utf8) ?? " "
568
+ strongSelf. bytesIn. removeSubrange ( strongSelf. bytesIn. startIndex... index)
569
+
570
+ if strongSelf. gotReady == false && line == " Ready " {
571
+ strongSelf. semaphore. signal ( )
572
+ strongSelf. gotReady = true ;
573
+ }
574
+ else if strongSelf. gotReady == true {
575
+ if line == " Signal: SIGINT " {
576
+ strongSelf. _sigIntCount += 1
577
+ strongSelf. semaphore. signal ( )
578
+ }
579
+ else if line == " Signal: SIGCONT " {
580
+ strongSelf. _sigContCount += 1
581
+ strongSelf. semaphore. signal ( )
582
+ }
583
+ }
584
+ }
585
+ }
586
+ }
587
+ } )
588
+ source. setEventHandler ( handler: workItem)
589
+ }
590
+
591
+ deinit {
592
+ source. cancel ( )
593
+ process. terminate ( )
594
+ process. waitUntilExit ( )
595
+ }
596
+
597
+ func start( ) throws {
598
+ source. resume ( )
599
+ try process. run ( )
600
+ }
601
+
602
+ func waitForReady( ) -> Bool {
603
+ let now = DispatchTime . now ( ) . uptimeNanoseconds
604
+ let timeout = DispatchTime ( uptimeNanoseconds: now + 2_000_000_000 )
605
+ guard semaphore. wait ( timeout: timeout) == . success else {
606
+ process. terminate ( )
607
+ return false
608
+ }
609
+ return true
610
+ }
611
+ }
612
+
397
613
#if !os(Android)
398
614
private func runTask( _ arguments: [ String ] , environment: [ String : String ] ? = nil , currentDirectoryPath: String ? = nil ) throws -> ( String , String ) {
399
615
let process = Process ( )
0 commit comments