@@ -408,3 +408,343 @@ extension Tag {
408408 #expect( manager. fractionCompleted == 1.0 )
409409 }
410410}
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