Skip to content

Commit 02ab998

Browse files
committed
add cycle detection to interop
1 parent 62a16a1 commit 02ab998

File tree

2 files changed

+39
-3
lines changed

2 files changed

+39
-3
lines changed

Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,52 @@ extension Progress {
5656
/// - reporter: A `ProgressReporter` instance.
5757
/// - count: Number of units delegated from `self`'s `totalCount`.
5858
public func addChild(_ reporter: ProgressReporter, withPendingUnitCount count: Int) {
59-
59+
60+
// Need to detect cycle here
61+
precondition(self.isCycle(reporter: reporter) == false, "Creating a cycle is not allowed.")
62+
6063
// Make intermediary & add it to NSProgress parent's children list
6164
let ghostProgressParent = Progress(totalUnitCount: Int64(reporter.manager.totalCount ?? 0))
6265
ghostProgressParent.completedUnitCount = Int64(reporter.manager.completedCount)
6366
self.addChild(ghostProgressParent, withPendingUnitCount: Int64(count))
6467

6568
// Make observation instance
6669
let observation = _ProgressParentProgressReporterChild(intermediary: ghostProgressParent, reporter: reporter)
67-
70+
6871
reporter.manager.setInteropObservationForMonitor(observation: observation)
6972
reporter.manager.setMonitorInterop(to: true)
7073
}
74+
75+
// MARK: Cycle detection
76+
func isCycle(reporter: ProgressReporter, visited: Set<ProgressManager> = []) -> Bool {
77+
if self._parent() == nil {
78+
return false
79+
}
80+
81+
if !(self._parent() is _NSProgressParentBridge) {
82+
return self._parent().isCycle(reporter: reporter)
83+
}
84+
85+
// then check against ProgressManager
86+
let unwrappedParent = (self._parent() as? _NSProgressParentBridge)?.actualParent
87+
if let unwrappedParent = unwrappedParent {
88+
if unwrappedParent === reporter.manager {
89+
return true
90+
}
91+
let updatedVisited = visited.union([unwrappedParent])
92+
return unwrappedParent.parents.withLock { parents in
93+
for (parent, _) in parents {
94+
if !updatedVisited.contains(parent) {
95+
if parent.isCycle(reporter: reporter, visited: updatedVisited) {
96+
return true
97+
}
98+
}
99+
}
100+
return false
101+
}
102+
}
103+
return false
104+
}
71105
}
72106

73107
private final class _ProgressParentProgressManagerChild: Sendable {
@@ -152,7 +186,7 @@ extension ProgressManager {
152186
// Subclass of Foundation.Progress
153187
internal final class _NSProgressParentBridge: Progress, @unchecked Sendable {
154188

155-
let actualParent: ProgressManager
189+
internal let actualParent: ProgressManager
156190

157191
init(managerParent: ProgressManager) {
158192
self.actualParent = managerParent

Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,8 @@ class TestProgressManagerInterop: XCTestCase {
587587
XCTAssertEqual(manager.fractionCompleted, 0.25)
588588
XCTAssertEqual(parentManager1.fractionCompleted, 0.25)
589589
XCTAssertEqual(parentManager2.fractionCompleted, 0.25)
590+
591+
// progress.addChild(parentManager1.reporter, withPendingUnitCount: 1) // this should trigger cycle detection
590592
}
591593
}
592594
#endif

0 commit comments

Comments
 (0)