@@ -408,3 +408,343 @@ extension Tag {
408
408
#expect( manager. fractionCompleted == 1.0 )
409
409
}
410
410
}
411
+
412
+ // MARK: - Thread Safety and Concurrent Access Tests
413
+ @Suite ( " Progress Manager Thread Safety Tests " , . tags( . progressManager) ) struct ProgressManagerThreadSafetyTests {
414
+
415
+ @Test func concurrentBasicPropertiesAccess( ) async throws {
416
+ let manager = ProgressManager ( totalCount: 10 )
417
+ manager. complete ( count: 5 )
418
+
419
+ await withThrowingTaskGroup ( of: Void . self) { group in
420
+
421
+ group. addTask {
422
+ for _ in 1 ... 10 {
423
+ let fraction = manager. fractionCompleted
424
+ #expect( fraction == 0.5 )
425
+ }
426
+ }
427
+
428
+ group. addTask {
429
+ for _ in 1 ... 10 {
430
+ let completed = manager. completedCount
431
+ #expect( completed == 5 )
432
+ }
433
+ }
434
+
435
+ group. addTask {
436
+ for _ in 1 ... 10 {
437
+ let total = manager. totalCount
438
+ #expect( total == 10 )
439
+ }
440
+ }
441
+
442
+ group. addTask {
443
+ for _ in 1 ... 10 {
444
+ let isFinished = manager. isFinished
445
+ #expect( isFinished == false )
446
+ }
447
+ }
448
+
449
+ group. addTask {
450
+ for _ in 1 ... 10 {
451
+ let isIndeterminate = manager. isIndeterminate
452
+ #expect( isIndeterminate == false )
453
+ }
454
+ }
455
+ }
456
+ }
457
+
458
+ @Test func concurrentMultipleChildrenUpdatesAndParentReads( ) async throws {
459
+ let manager = ProgressManager ( totalCount: 100 )
460
+ let child1 = manager. subprogress ( assigningCount: 30 ) . start ( totalCount: 10 )
461
+ let child2 = manager. subprogress ( assigningCount: 40 ) . start ( totalCount: 8 )
462
+ let child3 = manager. subprogress ( assigningCount: 30 ) . start ( totalCount: 6 )
463
+
464
+ await withTaskGroup ( of: Void . self) { group in
465
+ group. addTask {
466
+ for _ in 1 ... 10 {
467
+ child1. complete ( count: 1 )
468
+ }
469
+ }
470
+
471
+ group. addTask {
472
+ for _ in 1 ... 8 {
473
+ child2. complete ( count: 1 )
474
+ }
475
+ }
476
+
477
+ group. addTask {
478
+ for _ in 1 ... 6 {
479
+ child3. complete ( count: 1 )
480
+ }
481
+ }
482
+
483
+ group. addTask {
484
+ for _ in 1 ... 50 {
485
+ let _ = manager. fractionCompleted
486
+ let _ = manager. completedCount
487
+ let _ = manager. isFinished
488
+ }
489
+ }
490
+
491
+ group. addTask {
492
+ for _ in 1 ... 30 {
493
+ let _ = child1. fractionCompleted
494
+ let _ = child2. completedCount
495
+ let _ = child3. isFinished
496
+ }
497
+ }
498
+ }
499
+
500
+ #expect( child1. isFinished == true )
501
+ #expect( child2. isFinished == true )
502
+ #expect( child3. isFinished == true )
503
+ #expect( manager. fractionCompleted == 1.0 )
504
+ }
505
+
506
+ @Test func concurrentSingleChildUpdatesAndParentReads( ) async throws {
507
+ let manager = ProgressManager ( totalCount: 50 )
508
+ let child = manager. subprogress ( assigningCount: 50 ) . start ( totalCount: 100 )
509
+
510
+ await withThrowingTaskGroup ( of: Void . self) { group in
511
+ group. addTask {
512
+ for i in 1 ... 100 {
513
+ child. complete ( count: 1 )
514
+ if i % 10 == 0 {
515
+ try ? await Task . sleep ( nanoseconds: 1_000_000 )
516
+ }
517
+ }
518
+ }
519
+
520
+ group. addTask {
521
+ for _ in 1 ... 200 {
522
+ let _ = manager. fractionCompleted
523
+ let _ = manager. completedCount
524
+ let _ = manager. totalCount
525
+ let _ = manager. isFinished
526
+ let _ = manager. isIndeterminate
527
+ }
528
+ }
529
+
530
+ group. addTask {
531
+ for _ in 1 ... 150 {
532
+ let _ = child. fractionCompleted
533
+ let _ = child. completedCount
534
+ let _ = child. isFinished
535
+ }
536
+ }
537
+ }
538
+
539
+ #expect( child. isFinished == true )
540
+ #expect( manager. fractionCompleted == 1.0 )
541
+ }
542
+
543
+ @Test func concurrentGrandchildrenUpdates( ) async throws {
544
+ let parent = ProgressManager ( totalCount: 60 )
545
+ let child1 = parent. subprogress ( assigningCount: 20 ) . start ( totalCount: 10 )
546
+ let child2 = parent. subprogress ( assigningCount: 20 ) . start ( totalCount: 8 )
547
+ let child3 = parent. subprogress ( assigningCount: 20 ) . start ( totalCount: 6 )
548
+
549
+ let grandchild1 = child1. subprogress ( assigningCount: 5 ) . start ( totalCount: 4 )
550
+ let grandchild2 = child2. subprogress ( assigningCount: 4 ) . start ( totalCount: 3 )
551
+ let grandchild3 = child3. subprogress ( assigningCount: 3 ) . start ( totalCount: 2 )
552
+
553
+ await withTaskGroup ( of: Void . self) { group in
554
+ group. addTask {
555
+ for _ in 1 ... 4 {
556
+ grandchild1. complete ( count: 1 )
557
+ }
558
+ }
559
+
560
+ group. addTask {
561
+ for _ in 1 ... 3 {
562
+ grandchild2. complete ( count: 1 )
563
+ }
564
+ }
565
+
566
+ group. addTask {
567
+ for _ in 1 ... 2 {
568
+ grandchild3. complete ( count: 1 )
569
+ }
570
+ }
571
+
572
+ group. addTask {
573
+ for _ in 1 ... 5 {
574
+ child1. complete ( count: 1 )
575
+ }
576
+ }
577
+
578
+ group. addTask {
579
+ for _ in 1 ... 4 {
580
+ child2. complete ( count: 1 )
581
+ }
582
+ }
583
+
584
+ group. addTask {
585
+ for _ in 1 ... 3 {
586
+ child3. complete ( count: 1 )
587
+ }
588
+ }
589
+
590
+ group. addTask {
591
+ for _ in 1 ... 100 {
592
+ let _ = parent. fractionCompleted
593
+ let _ = child1. fractionCompleted
594
+ let _ = grandchild1. completedCount
595
+ }
596
+ }
597
+ }
598
+
599
+ #expect( grandchild1. isFinished == true )
600
+ #expect( grandchild2. isFinished == true )
601
+ #expect( grandchild3. isFinished == true )
602
+ #expect( parent. isFinished == true )
603
+ }
604
+
605
+ @Test func concurrentReadDuringIndeterminateToDeterminateTransition( ) async throws {
606
+ let manager = ProgressManager ( totalCount: nil )
607
+
608
+ await withThrowingTaskGroup ( of: Void . self) { group in
609
+ group. addTask {
610
+ for _ in 1 ... 50 {
611
+ let _ = manager. fractionCompleted
612
+ let _ = manager. isIndeterminate
613
+ }
614
+ }
615
+
616
+ group. addTask {
617
+ for _ in 1 ... 10 {
618
+ manager. complete ( count: 1 )
619
+ }
620
+ }
621
+
622
+ // Task 3: Change to determinate after a delay
623
+ group. addTask {
624
+ try ? await Task . sleep ( nanoseconds: 1_000_000 )
625
+ manager. withProperties { p in
626
+ p. totalCount = 20
627
+ }
628
+
629
+ for _ in 1 ... 30 {
630
+ let _ = manager. fractionCompleted
631
+ let _ = manager. isIndeterminate
632
+ }
633
+ }
634
+ }
635
+
636
+ #expect( manager. totalCount == 20 )
637
+ #expect( manager. completedCount == 10 )
638
+ #expect( manager. isIndeterminate == false )
639
+ }
640
+
641
+ @Test func concurrentReadDuringExcessiveCompletion( ) async throws {
642
+ let manager = ProgressManager ( totalCount: 5 )
643
+
644
+ await withThrowingTaskGroup ( of: Void . self) { group in
645
+ group. addTask {
646
+ for _ in 1 ... 20 {
647
+ manager. complete ( count: 1 )
648
+ try ? await Task . sleep ( nanoseconds: 100_000 )
649
+ }
650
+ }
651
+
652
+ group. addTask {
653
+ for _ in 1 ... 100 {
654
+ let fraction = manager. fractionCompleted
655
+ let completed = manager. completedCount
656
+
657
+ #expect( completed >= 0 && completed <= 20 )
658
+ #expect( fraction >= 0.0 && fraction <= 4.0 )
659
+ }
660
+ }
661
+ }
662
+
663
+ #expect( manager. completedCount == 20 )
664
+ #expect( manager. fractionCompleted == 4.0 )
665
+ #expect( manager. isFinished == true )
666
+ }
667
+
668
+ @Test func concurrentChildrenDeinitializationAndParentReads( ) async throws {
669
+ let manager = ProgressManager ( totalCount: 100 )
670
+
671
+ await withThrowingTaskGroup ( of: Void . self) { group in
672
+ // Create and destroy children rapidly
673
+ for batch in 1 ... 10 {
674
+ group. addTask {
675
+ for i in 1 ... 5 {
676
+ func createAndDestroyChild( ) {
677
+ let child = manager. subprogress ( assigningCount: 2 ) . start ( totalCount: 3 )
678
+ child. complete ( count: 2 + ( i % 2 ) ) // Complete 2 or 3
679
+ // child deinits here
680
+ }
681
+
682
+ createAndDestroyChild ( )
683
+ try ? await Task . sleep ( nanoseconds: 200_000 * UInt64( batch) )
684
+ }
685
+ }
686
+ }
687
+
688
+ // Continuously read manager state during child lifecycle
689
+ group. addTask {
690
+ for _ in 1 ... 300 {
691
+ let fraction = manager. fractionCompleted
692
+ let completed = manager. completedCount
693
+
694
+ // Properties should be stable and valid
695
+ #expect( fraction >= 0.0 )
696
+ #expect( completed >= 0 )
697
+
698
+ try ? await Task . sleep ( nanoseconds: 50_000 )
699
+ }
700
+ }
701
+ }
702
+
703
+ // Manager should reach completion
704
+ #expect( manager. fractionCompleted == 1.0 )
705
+ #expect( manager. completedCount == 100 )
706
+ }
707
+
708
+ @Test func concurrentReadAndWriteAndCycleDetection( ) async throws {
709
+ let manager1 = ProgressManager ( totalCount: 10 )
710
+ let manager2 = ProgressManager ( totalCount: 10 )
711
+ let manager3 = ProgressManager ( totalCount: 10 )
712
+
713
+ // Create initial chain: manager1 -> manager2 -> manager3
714
+ manager1. assign ( count: 5 , to: manager2. reporter)
715
+ manager2. assign ( count: 5 , to: manager3. reporter)
716
+
717
+ await withTaskGroup ( of: Void . self) { group in
718
+ // Task 1: Try to detect cycles continuously
719
+ group. addTask {
720
+ for _ in 1 ... 50 {
721
+ let wouldCycle1 = manager1. isCycle ( reporter: manager3. reporter)
722
+ let wouldCycle2 = manager2. isCycle ( reporter: manager1. reporter)
723
+ let wouldCycle3 = manager3. isCycle ( reporter: manager2. reporter)
724
+
725
+ #expect( wouldCycle1 == false ) // No cycle yet
726
+ #expect( wouldCycle2 == true ) // Would create cycle
727
+ #expect( wouldCycle3 == true ) // Would create cycle
728
+ }
729
+ }
730
+
731
+ // Task 2: Complete work in all managers
732
+ group. addTask {
733
+ for _ in 1 ... 5 {
734
+ manager1. complete ( count: 1 )
735
+ manager2. complete ( count: 1 )
736
+ manager3. complete ( count: 1 )
737
+ }
738
+ }
739
+
740
+ // Task 3: Access properties during cycle detection
741
+ group. addTask {
742
+ for _ in 1 ... 100 {
743
+ let _ = manager1. fractionCompleted
744
+ let _ = manager2. completedCount
745
+ let _ = manager3. isFinished
746
+ }
747
+ }
748
+ }
749
+ }
750
+ }
0 commit comments